syntax_suggest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +91 -0
- data/.github/workflows/check_changelog.yml +20 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.standard.yml +1 -0
- data/CHANGELOG.md +158 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +229 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dead_end.gemspec +32 -0
- data/exe/syntax_suggest +7 -0
- data/lib/syntax_suggest/api.rb +199 -0
- data/lib/syntax_suggest/around_block_scan.rb +224 -0
- data/lib/syntax_suggest/block_expand.rb +74 -0
- data/lib/syntax_suggest/capture_code_context.rb +233 -0
- data/lib/syntax_suggest/clean_document.rb +304 -0
- data/lib/syntax_suggest/cli.rb +129 -0
- data/lib/syntax_suggest/code_block.rb +100 -0
- data/lib/syntax_suggest/code_frontier.rb +178 -0
- data/lib/syntax_suggest/code_line.rb +239 -0
- data/lib/syntax_suggest/code_search.rb +139 -0
- data/lib/syntax_suggest/core_ext.rb +101 -0
- data/lib/syntax_suggest/display_code_with_line_numbers.rb +70 -0
- data/lib/syntax_suggest/display_invalid_blocks.rb +84 -0
- data/lib/syntax_suggest/explain_syntax.rb +103 -0
- data/lib/syntax_suggest/left_right_lex_count.rb +168 -0
- data/lib/syntax_suggest/lex_all.rb +55 -0
- data/lib/syntax_suggest/lex_value.rb +70 -0
- data/lib/syntax_suggest/parse_blocks_from_indent_line.rb +60 -0
- data/lib/syntax_suggest/pathname_from_message.rb +59 -0
- data/lib/syntax_suggest/priority_engulf_queue.rb +63 -0
- data/lib/syntax_suggest/priority_queue.rb +105 -0
- data/lib/syntax_suggest/ripper_errors.rb +36 -0
- data/lib/syntax_suggest/unvisited_lines.rb +36 -0
- data/lib/syntax_suggest/version.rb +5 -0
- data/lib/syntax_suggest.rb +3 -0
- 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
|