syntax_suggest 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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