syntax_suggest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +91 -0
  3. data/.github/workflows/check_changelog.yml +20 -0
  4. data/.gitignore +14 -0
  5. data/.rspec +3 -0
  6. data/.standard.yml +1 -0
  7. data/CHANGELOG.md +158 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +14 -0
  10. data/Gemfile.lock +67 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +229 -0
  13. data/Rakefile +8 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +8 -0
  16. data/dead_end.gemspec +32 -0
  17. data/exe/syntax_suggest +7 -0
  18. data/lib/syntax_suggest/api.rb +199 -0
  19. data/lib/syntax_suggest/around_block_scan.rb +224 -0
  20. data/lib/syntax_suggest/block_expand.rb +74 -0
  21. data/lib/syntax_suggest/capture_code_context.rb +233 -0
  22. data/lib/syntax_suggest/clean_document.rb +304 -0
  23. data/lib/syntax_suggest/cli.rb +129 -0
  24. data/lib/syntax_suggest/code_block.rb +100 -0
  25. data/lib/syntax_suggest/code_frontier.rb +178 -0
  26. data/lib/syntax_suggest/code_line.rb +239 -0
  27. data/lib/syntax_suggest/code_search.rb +139 -0
  28. data/lib/syntax_suggest/core_ext.rb +101 -0
  29. data/lib/syntax_suggest/display_code_with_line_numbers.rb +70 -0
  30. data/lib/syntax_suggest/display_invalid_blocks.rb +84 -0
  31. data/lib/syntax_suggest/explain_syntax.rb +103 -0
  32. data/lib/syntax_suggest/left_right_lex_count.rb +168 -0
  33. data/lib/syntax_suggest/lex_all.rb +55 -0
  34. data/lib/syntax_suggest/lex_value.rb +70 -0
  35. data/lib/syntax_suggest/parse_blocks_from_indent_line.rb +60 -0
  36. data/lib/syntax_suggest/pathname_from_message.rb +59 -0
  37. data/lib/syntax_suggest/priority_engulf_queue.rb +63 -0
  38. data/lib/syntax_suggest/priority_queue.rb +105 -0
  39. data/lib/syntax_suggest/ripper_errors.rb +36 -0
  40. data/lib/syntax_suggest/unvisited_lines.rb +36 -0
  41. data/lib/syntax_suggest/version.rb +5 -0
  42. data/lib/syntax_suggest.rb +3 -0
  43. metadata +88 -0
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxSuggest
4
+ # This class is useful for exploring contents before and after
5
+ # a block
6
+ #
7
+ # It searches above and below the passed in block to match for
8
+ # whatever criteria you give it:
9
+ #
10
+ # Example:
11
+ #
12
+ # def dog # 1
13
+ # puts "bark" # 2
14
+ # puts "bark" # 3
15
+ # end # 4
16
+ #
17
+ # scan = AroundBlockScan.new(
18
+ # code_lines: code_lines
19
+ # block: CodeBlock.new(lines: code_lines[1])
20
+ # )
21
+ #
22
+ # scan.scan_while { true }
23
+ #
24
+ # puts scan.before_index # => 0
25
+ # puts scan.after_index # => 3
26
+ #
27
+ # Contents can also be filtered using AroundBlockScan#skip
28
+ #
29
+ # To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent
30
+ class AroundBlockScan
31
+ def initialize(code_lines:, block:)
32
+ @code_lines = code_lines
33
+ @orig_before_index = block.lines.first.index
34
+ @orig_after_index = block.lines.last.index
35
+ @orig_indent = block.current_indent
36
+ @skip_array = []
37
+ @after_array = []
38
+ @before_array = []
39
+ @stop_after_kw = false
40
+
41
+ @skip_hidden = false
42
+ @skip_empty = false
43
+ end
44
+
45
+ def skip(name)
46
+ case name
47
+ when :hidden?
48
+ @skip_hidden = true
49
+ when :empty?
50
+ @skip_empty = true
51
+ else
52
+ raise "Unsupported skip #{name}"
53
+ end
54
+ self
55
+ end
56
+
57
+ def stop_after_kw
58
+ @stop_after_kw = true
59
+ self
60
+ end
61
+
62
+ def scan_while
63
+ stop_next = false
64
+
65
+ kw_count = 0
66
+ end_count = 0
67
+ index = before_lines.reverse_each.take_while do |line|
68
+ next false if stop_next
69
+ next true if @skip_hidden && line.hidden?
70
+ next true if @skip_empty && line.empty?
71
+
72
+ kw_count += 1 if line.is_kw?
73
+ end_count += 1 if line.is_end?
74
+ if @stop_after_kw && kw_count > end_count
75
+ stop_next = true
76
+ end
77
+
78
+ yield line
79
+ end.last&.index
80
+
81
+ if index && index < before_index
82
+ @before_index = index
83
+ end
84
+
85
+ stop_next = false
86
+ kw_count = 0
87
+ end_count = 0
88
+ index = after_lines.take_while do |line|
89
+ next false if stop_next
90
+ next true if @skip_hidden && line.hidden?
91
+ next true if @skip_empty && line.empty?
92
+
93
+ kw_count += 1 if line.is_kw?
94
+ end_count += 1 if line.is_end?
95
+ if @stop_after_kw && end_count > kw_count
96
+ stop_next = true
97
+ end
98
+
99
+ yield line
100
+ end.last&.index
101
+
102
+ if index && index > after_index
103
+ @after_index = index
104
+ end
105
+ self
106
+ end
107
+
108
+ def capture_neighbor_context
109
+ lines = []
110
+ kw_count = 0
111
+ end_count = 0
112
+ before_lines.reverse_each do |line|
113
+ next if line.empty?
114
+ break if line.indent < @orig_indent
115
+ next if line.indent != @orig_indent
116
+
117
+ kw_count += 1 if line.is_kw?
118
+ end_count += 1 if line.is_end?
119
+ if kw_count != 0 && kw_count == end_count
120
+ lines << line
121
+ break
122
+ end
123
+
124
+ lines << line
125
+ end
126
+
127
+ lines.reverse!
128
+
129
+ kw_count = 0
130
+ end_count = 0
131
+ after_lines.each do |line|
132
+ next if line.empty?
133
+ break if line.indent < @orig_indent
134
+ next if line.indent != @orig_indent
135
+
136
+ kw_count += 1 if line.is_kw?
137
+ end_count += 1 if line.is_end?
138
+ if kw_count != 0 && kw_count == end_count
139
+ lines << line
140
+ break
141
+ end
142
+
143
+ lines << line
144
+ end
145
+
146
+ lines
147
+ end
148
+
149
+ def on_falling_indent
150
+ last_indent = @orig_indent
151
+ before_lines.reverse_each do |line|
152
+ next if line.empty?
153
+ if line.indent < last_indent
154
+ yield line
155
+ last_indent = line.indent
156
+ end
157
+ end
158
+
159
+ last_indent = @orig_indent
160
+ after_lines.each do |line|
161
+ next if line.empty?
162
+ if line.indent < last_indent
163
+ yield line
164
+ last_indent = line.indent
165
+ end
166
+ end
167
+ end
168
+
169
+ def scan_neighbors
170
+ scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
171
+ end
172
+
173
+ def next_up
174
+ @code_lines[before_index.pred]
175
+ end
176
+
177
+ def next_down
178
+ @code_lines[after_index.next]
179
+ end
180
+
181
+ def scan_adjacent_indent
182
+ before_after_indent = []
183
+ before_after_indent << (next_up&.indent || 0)
184
+ before_after_indent << (next_down&.indent || 0)
185
+
186
+ indent = before_after_indent.min
187
+ scan_while { |line| line.not_empty? && line.indent >= indent }
188
+
189
+ self
190
+ end
191
+
192
+ def start_at_next_line
193
+ before_index
194
+ after_index
195
+ @before_index -= 1
196
+ @after_index += 1
197
+ self
198
+ end
199
+
200
+ def code_block
201
+ CodeBlock.new(lines: lines)
202
+ end
203
+
204
+ def lines
205
+ @code_lines[before_index..after_index]
206
+ end
207
+
208
+ def before_index
209
+ @before_index ||= @orig_before_index
210
+ end
211
+
212
+ def after_index
213
+ @after_index ||= @orig_after_index
214
+ end
215
+
216
+ private def before_lines
217
+ @code_lines[0...before_index] || []
218
+ end
219
+
220
+ private def after_lines
221
+ @code_lines[after_index.next..-1] || []
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxSuggest
4
+ # This class is responsible for taking a code block that exists
5
+ # at a far indentaion and then iteratively increasing the block
6
+ # so that it captures everything within the same indentation block.
7
+ #
8
+ # def dog
9
+ # puts "bow"
10
+ # puts "wow"
11
+ # end
12
+ #
13
+ # block = BlockExpand.new(code_lines: code_lines)
14
+ # .call(CodeBlock.new(lines: code_lines[1]))
15
+ #
16
+ # puts block.to_s
17
+ # # => puts "bow"
18
+ # puts "wow"
19
+ #
20
+ #
21
+ # Once a code block has captured everything at a given indentation level
22
+ # then it will expand to capture surrounding indentation.
23
+ #
24
+ # block = BlockExpand.new(code_lines: code_lines)
25
+ # .call(block)
26
+ #
27
+ # block.to_s
28
+ # # => def dog
29
+ # puts "bow"
30
+ # puts "wow"
31
+ # end
32
+ #
33
+ class BlockExpand
34
+ def initialize(code_lines:)
35
+ @code_lines = code_lines
36
+ end
37
+
38
+ def call(block)
39
+ if (next_block = expand_neighbors(block))
40
+ return next_block
41
+ end
42
+
43
+ expand_indent(block)
44
+ end
45
+
46
+ def expand_indent(block)
47
+ AroundBlockScan.new(code_lines: @code_lines, block: block)
48
+ .skip(:hidden?)
49
+ .stop_after_kw
50
+ .scan_adjacent_indent
51
+ .code_block
52
+ end
53
+
54
+ def expand_neighbors(block)
55
+ expanded_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
56
+ .skip(:hidden?)
57
+ .stop_after_kw
58
+ .scan_neighbors
59
+ .scan_while { |line| line.empty? } # Slurp up empties
60
+ .lines
61
+
62
+ if block.lines == expanded_lines
63
+ nil
64
+ else
65
+ CodeBlock.new(lines: expanded_lines)
66
+ end
67
+ end
68
+
69
+ # Managable rspec errors
70
+ def inspect
71
+ "#<SyntaxSuggest::CodeBlock:0x0000123843lol >"
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxSuggest
4
+ # Turns a "invalid block(s)" into useful context
5
+ #
6
+ # There are three main phases in the algorithm:
7
+ #
8
+ # 1. Sanitize/format input source
9
+ # 2. Search for invalid blocks
10
+ # 3. Format invalid blocks into something meaninful
11
+ #
12
+ # This class handles the third part.
13
+ #
14
+ # The algorithm is very good at capturing all of a syntax
15
+ # error in a single block in number 2, however the results
16
+ # can contain ambiguities. Humans are good at pattern matching
17
+ # and filtering and can mentally remove extraneous data, but
18
+ # they can't add extra data that's not present.
19
+ #
20
+ # In the case of known ambiguious cases, this class adds context
21
+ # back to the ambiguitiy so the programmer has full information.
22
+ #
23
+ # Beyond handling these ambiguities, it also captures surrounding
24
+ # code context information:
25
+ #
26
+ # puts block.to_s # => "def bark"
27
+ #
28
+ # context = CaptureCodeContext.new(
29
+ # blocks: block,
30
+ # code_lines: code_lines
31
+ # )
32
+ #
33
+ # lines = context.call.map(&:original)
34
+ # puts lines.join
35
+ # # =>
36
+ # class Dog
37
+ # def bark
38
+ # end
39
+ #
40
+ class CaptureCodeContext
41
+ attr_reader :code_lines
42
+
43
+ def initialize(blocks:, code_lines:)
44
+ @blocks = Array(blocks)
45
+ @code_lines = code_lines
46
+ @visible_lines = @blocks.map(&:visible_lines).flatten
47
+ @lines_to_output = @visible_lines.dup
48
+ end
49
+
50
+ def call
51
+ @blocks.each do |block|
52
+ capture_first_kw_end_same_indent(block)
53
+ capture_last_end_same_indent(block)
54
+ capture_before_after_kws(block)
55
+ capture_falling_indent(block)
56
+ end
57
+
58
+ @lines_to_output.select!(&:not_empty?)
59
+ @lines_to_output.uniq!
60
+ @lines_to_output.sort!
61
+
62
+ @lines_to_output
63
+ end
64
+
65
+ # Shows the context around code provided by "falling" indentation
66
+ #
67
+ # Converts:
68
+ #
69
+ # it "foo" do
70
+ #
71
+ # into:
72
+ #
73
+ # class OH
74
+ # def hello
75
+ # it "foo" do
76
+ # end
77
+ # end
78
+ #
79
+ #
80
+ def capture_falling_indent(block)
81
+ AroundBlockScan.new(
82
+ block: block,
83
+ code_lines: @code_lines
84
+ ).on_falling_indent do |line|
85
+ @lines_to_output << line
86
+ end
87
+ end
88
+
89
+ # Shows surrounding kw/end pairs
90
+ #
91
+ # The purpose of showing these extra pairs is due to cases
92
+ # of ambiguity when only one visible line is matched.
93
+ #
94
+ # For example:
95
+ #
96
+ # 1 class Dog
97
+ # 2 def bark
98
+ # 4 def eat
99
+ # 5 end
100
+ # 6 end
101
+ #
102
+ # In this case either line 2 could be missing an `end` or
103
+ # line 4 was an extra line added by mistake (it happens).
104
+ #
105
+ # When we detect the above problem it shows the issue
106
+ # as only being on line 2
107
+ #
108
+ # 2 def bark
109
+ #
110
+ # Showing "neighbor" keyword pairs gives extra context:
111
+ #
112
+ # 2 def bark
113
+ # 4 def eat
114
+ # 5 end
115
+ #
116
+ def capture_before_after_kws(block)
117
+ return unless block.visible_lines.count == 1
118
+
119
+ around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
120
+ .start_at_next_line
121
+ .capture_neighbor_context
122
+
123
+ around_lines -= block.lines
124
+
125
+ @lines_to_output.concat(around_lines)
126
+ end
127
+
128
+ # When there is an invalid block with a keyword
129
+ # missing an end right before another end,
130
+ # it is unclear where which keyword is missing the
131
+ # end
132
+ #
133
+ # Take this example:
134
+ #
135
+ # class Dog # 1
136
+ # def bark # 2
137
+ # puts "woof" # 3
138
+ # end # 4
139
+ #
140
+ # However due to https://github.com/zombocom/syntax_suggest/issues/32
141
+ # the problem line will be identified as:
142
+ #
143
+ # ❯ class Dog # 1
144
+ #
145
+ # Because lines 2, 3, and 4 are technically valid code and are expanded
146
+ # first, deemed valid, and hidden. We need to un-hide the matching end
147
+ # line 4. Also work backwards and if there's a mis-matched keyword, show it
148
+ # too
149
+ def capture_last_end_same_indent(block)
150
+ return if block.visible_lines.length != 1
151
+ return unless block.visible_lines.first.is_kw?
152
+
153
+ visible_line = block.visible_lines.first
154
+ lines = @code_lines[visible_line.index..block.lines.last.index]
155
+
156
+ # Find first end with same indent
157
+ # (this would return line 4)
158
+ #
159
+ # end # 4
160
+ matching_end = lines.detect { |line| line.indent == block.current_indent && line.is_end? }
161
+ return unless matching_end
162
+
163
+ @lines_to_output << matching_end
164
+
165
+ # Work backwards from the end to
166
+ # see if there are mis-matched
167
+ # keyword/end pairs
168
+ #
169
+ # Return the first mis-matched keyword
170
+ # this would find line 2
171
+ #
172
+ # def bark # 2
173
+ # puts "woof" # 3
174
+ # end # 4
175
+ end_count = 0
176
+ kw_count = 0
177
+ kw_line = @code_lines[visible_line.index..matching_end.index].reverse.detect do |line|
178
+ end_count += 1 if line.is_end?
179
+ kw_count += 1 if line.is_kw?
180
+
181
+ !kw_count.zero? && kw_count >= end_count
182
+ end
183
+ return unless kw_line
184
+ @lines_to_output << kw_line
185
+ end
186
+
187
+ # The logical inverse of `capture_last_end_same_indent`
188
+ #
189
+ # When there is an invalid block with an `end`
190
+ # missing a keyword right after another `end`,
191
+ # it is unclear where which end is missing the
192
+ # keyword.
193
+ #
194
+ # Take this example:
195
+ #
196
+ # class Dog # 1
197
+ # puts "woof" # 2
198
+ # end # 3
199
+ # end # 4
200
+ #
201
+ # the problem line will be identified as:
202
+ #
203
+ # ❯ end # 4
204
+ #
205
+ # This happens because lines 1, 2, and 3 are technically valid code and are expanded
206
+ # first, deemed valid, and hidden. We need to un-hide the matching keyword on
207
+ # line 1. Also work backwards and if there's a mis-matched end, show it
208
+ # too
209
+ def capture_first_kw_end_same_indent(block)
210
+ return if block.visible_lines.length != 1
211
+ return unless block.visible_lines.first.is_end?
212
+
213
+ visible_line = block.visible_lines.first
214
+ lines = @code_lines[block.lines.first.index..visible_line.index]
215
+ matching_kw = lines.reverse.detect { |line| line.indent == block.current_indent && line.is_kw? }
216
+ return unless matching_kw
217
+
218
+ @lines_to_output << matching_kw
219
+
220
+ kw_count = 0
221
+ end_count = 0
222
+ orphan_end = @code_lines[matching_kw.index..visible_line.index].detect do |line|
223
+ kw_count += 1 if line.is_kw?
224
+ end_count += 1 if line.is_end?
225
+
226
+ end_count >= kw_count
227
+ end
228
+
229
+ return unless orphan_end
230
+ @lines_to_output << orphan_end
231
+ end
232
+ end
233
+ end