dead_end 1.2.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +9 -0
- data/.github/workflows/check_changelog.yml +14 -7
- data/.standard.yml +1 -1
- data/CHANGELOG.md +29 -0
- data/Gemfile.lock +2 -2
- data/README.md +89 -21
- data/exe/dead_end +3 -66
- data/lib/dead_end/around_block_scan.rb +6 -9
- data/lib/dead_end/auto.rb +1 -21
- data/lib/dead_end/capture_code_context.rb +123 -16
- data/lib/dead_end/clean_document.rb +313 -0
- data/lib/dead_end/cli.rb +118 -0
- data/lib/dead_end/code_block.rb +18 -2
- data/lib/dead_end/code_frontier.rb +53 -16
- data/lib/dead_end/code_line.rb +159 -76
- data/lib/dead_end/code_search.rb +24 -37
- data/lib/dead_end/display_code_with_line_numbers.rb +0 -1
- data/lib/dead_end/display_invalid_blocks.rb +41 -78
- data/lib/dead_end/explain_syntax.rb +103 -0
- data/lib/dead_end/left_right_lex_count.rb +157 -0
- data/lib/dead_end/lex_all.rb +11 -27
- data/lib/dead_end/lex_value.rb +62 -0
- data/lib/dead_end/parse_blocks_from_indent_line.rb +1 -1
- data/lib/dead_end/ripper_errors.rb +30 -0
- data/lib/dead_end/version.rb +1 -1
- data/lib/dead_end.rb +145 -1
- metadata +8 -7
- data/lib/dead_end/fyi.rb +0 -6
- data/lib/dead_end/heredoc_block_parse.rb +0 -34
- data/lib/dead_end/internals.rb +0 -158
- data/lib/dead_end/trailing_slash_join.rb +0 -53
- data/lib/dead_end/who_dis_syntax_error.rb +0 -74
data/lib/dead_end/code_line.rb
CHANGED
@@ -4,44 +4,47 @@ module DeadEnd
|
|
4
4
|
# Represents a single line of code of a given source file
|
5
5
|
#
|
6
6
|
# This object contains metadata about the line such as
|
7
|
-
# amount of indentation
|
7
|
+
# amount of indentation, if it is empty or not, and
|
8
|
+
# lexical data, such as if it has an `end` or a keyword
|
9
|
+
# in it.
|
8
10
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# as
|
12
|
-
#
|
13
|
-
# Visibility of lines can be toggled on and off.
|
11
|
+
# Visibility of lines can be toggled off. Marking a line as invisible
|
12
|
+
# indicates that it should not be used for syntax checks.
|
13
|
+
# It's functionally the same as commenting it out.
|
14
14
|
#
|
15
15
|
# Example:
|
16
16
|
#
|
17
|
-
# line = CodeLine.
|
18
|
-
# line.
|
17
|
+
# line = CodeLine.from_source("def foo\n").first
|
18
|
+
# line.number => 1
|
19
19
|
# line.empty? # => false
|
20
20
|
# line.visible? # => true
|
21
21
|
# line.mark_invisible
|
22
22
|
# line.visible? # => false
|
23
23
|
#
|
24
|
-
# A CodeBlock is made of multiple CodeLines
|
25
|
-
#
|
26
|
-
# Marking a line as invisible indicates that it should not be used
|
27
|
-
# for syntax checks. It's essentially the same as commenting it out
|
28
|
-
#
|
29
|
-
# Marking a line as invisible also lets the overall program know
|
30
|
-
# that it should not check that area for syntax errors.
|
31
24
|
class CodeLine
|
32
25
|
TRAILING_SLASH = ("\\" + $/).freeze
|
33
26
|
|
34
|
-
|
27
|
+
# Returns an array of CodeLine objects
|
28
|
+
# from the source string
|
29
|
+
def self.from_source(source)
|
30
|
+
lex_array_for_line = LexAll.new(source: source).each_with_object(Hash.new { |h, k| h[k] = [] }) { |lex, hash| hash[lex.line] << lex }
|
35
31
|
source.lines.map.with_index do |line, index|
|
36
|
-
CodeLine.new(
|
32
|
+
CodeLine.new(
|
33
|
+
line: line,
|
34
|
+
index: index,
|
35
|
+
lex: lex_array_for_line[index + 1]
|
36
|
+
)
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
attr_reader :line, :index, :
|
40
|
+
attr_reader :line, :index, :lex, :line_number, :indent
|
41
|
+
def initialize(line:, index:, lex:)
|
42
|
+
@lex = lex
|
43
|
+
@line = line
|
44
|
+
@index = index
|
45
|
+
@original = line.freeze
|
46
|
+
@line_number = @index + 1
|
41
47
|
|
42
|
-
def initialize(line:, index:)
|
43
|
-
@original_line = line.freeze
|
44
|
-
@line = @original_line
|
45
48
|
if line.strip.empty?
|
46
49
|
@empty = true
|
47
50
|
@indent = 0
|
@@ -49,102 +52,182 @@ module DeadEnd
|
|
49
52
|
@empty = false
|
50
53
|
@indent = SpaceCount.indent(line)
|
51
54
|
end
|
52
|
-
@index = index
|
53
|
-
@status = nil # valid, invalid, unknown
|
54
|
-
@invalid = false
|
55
55
|
|
56
|
-
lex_detect!
|
57
|
-
end
|
58
|
-
|
59
|
-
private def lex_detect!
|
60
|
-
lex_array = LexAll.new(source: line)
|
61
56
|
kw_count = 0
|
62
57
|
end_count = 0
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
case lex.token
|
67
|
-
when "if", "unless", "while", "until"
|
68
|
-
# Only count if/unless when it's not a "trailing" if/unless
|
69
|
-
# https://github.com/ruby/ruby/blob/06b44f819eb7b5ede1ff69cecb25682b56a1d60c/lib/irb/ruby-lex.rb#L374-L375
|
70
|
-
kw_count += 1 unless lex.expr_label?
|
71
|
-
when "def", "case", "for", "begin", "class", "module", "do"
|
72
|
-
kw_count += 1
|
73
|
-
when "end"
|
74
|
-
end_count += 1
|
75
|
-
end
|
58
|
+
@lex.each do |lex|
|
59
|
+
kw_count += 1 if lex.is_kw?
|
60
|
+
end_count += 1 if lex.is_end?
|
76
61
|
end
|
77
62
|
|
78
|
-
|
79
|
-
|
63
|
+
kw_count -= oneliner_method_count
|
64
|
+
|
80
65
|
@is_kw = (kw_count - end_count) > 0
|
81
66
|
@is_end = (end_count - kw_count) > 0
|
82
|
-
@is_trailing_slash = lex_array.last.token == TRAILING_SLASH
|
83
|
-
end
|
84
|
-
|
85
|
-
alias_method :original, :original_line
|
86
|
-
|
87
|
-
def trailing_slash?
|
88
|
-
@is_trailing_slash
|
89
67
|
end
|
90
68
|
|
69
|
+
# Used for stable sort via indentation level
|
70
|
+
#
|
71
|
+
# Ruby's sort is not "stable" meaning that when
|
72
|
+
# multiple elements have the same value, they are
|
73
|
+
# not guaranteed to return in the same order they
|
74
|
+
# were put in.
|
75
|
+
#
|
76
|
+
# So when multiple code lines have the same indentation
|
77
|
+
# level, they're sorted by their index value which is unique
|
78
|
+
# and consistent.
|
79
|
+
#
|
80
|
+
# This is mostly needed for consistency of the test suite
|
91
81
|
def indent_index
|
92
82
|
@indent_index ||= [indent, index]
|
93
83
|
end
|
84
|
+
alias_method :number, :line_number
|
94
85
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
def is_comment?
|
100
|
-
@is_comment
|
101
|
-
end
|
102
|
-
|
103
|
-
def not_comment?
|
104
|
-
!is_comment?
|
105
|
-
end
|
106
|
-
|
86
|
+
# Returns true if the code line is determined
|
87
|
+
# to contain a keyword that matches with an `end`
|
88
|
+
#
|
89
|
+
# For example: `def`, `do`, `begin`, `ensure`, etc.
|
107
90
|
def is_kw?
|
108
91
|
@is_kw
|
109
92
|
end
|
110
93
|
|
94
|
+
# Returns true if the code line is determined
|
95
|
+
# to contain an `end` keyword
|
111
96
|
def is_end?
|
112
97
|
@is_end
|
113
98
|
end
|
114
99
|
|
100
|
+
# Used to hide lines
|
101
|
+
#
|
102
|
+
# The search alorithm will group lines into blocks
|
103
|
+
# then if those blocks are determined to represent
|
104
|
+
# valid code they will be hidden
|
115
105
|
def mark_invisible
|
116
106
|
@line = ""
|
117
|
-
self
|
118
|
-
end
|
119
|
-
|
120
|
-
def mark_visible
|
121
|
-
@line = @original_line
|
122
|
-
self
|
123
107
|
end
|
124
108
|
|
109
|
+
# Means the line was marked as "invisible"
|
110
|
+
# Confusingly, "empty" lines are visible...they
|
111
|
+
# just don't contain any source code other than a newline ("\n").
|
125
112
|
def visible?
|
126
113
|
!line.empty?
|
127
114
|
end
|
128
115
|
|
116
|
+
# Opposite or `visible?` (note: different than `empty?`)
|
129
117
|
def hidden?
|
130
118
|
!visible?
|
131
119
|
end
|
132
120
|
|
133
|
-
|
134
|
-
|
121
|
+
# An `empty?` line is one that was originally left
|
122
|
+
# empty in the source code, while a "hidden" line
|
123
|
+
# is one that we've since marked as "invisible"
|
124
|
+
def empty?
|
125
|
+
@empty
|
135
126
|
end
|
136
|
-
alias_method :number, :line_number
|
137
127
|
|
128
|
+
# Opposite of `empty?` (note: different than `visible?`)
|
138
129
|
def not_empty?
|
139
130
|
!empty?
|
140
131
|
end
|
141
132
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
133
|
+
# Renders the given line
|
134
|
+
#
|
135
|
+
# Also allows us to represent source code as
|
136
|
+
# an array of code lines.
|
137
|
+
#
|
138
|
+
# When we have an array of code line elements
|
139
|
+
# calling `join` on the array will call `to_s`
|
140
|
+
# on each element, which essentially converts
|
141
|
+
# it back into it's original source string.
|
146
142
|
def to_s
|
147
143
|
line
|
148
144
|
end
|
145
|
+
|
146
|
+
# When the code line is marked invisible
|
147
|
+
# we retain the original value of it's line
|
148
|
+
# this is useful for debugging and for
|
149
|
+
# showing extra context
|
150
|
+
#
|
151
|
+
# DisplayCodeWithLineNumbers will render
|
152
|
+
# all lines given to it, not just visible
|
153
|
+
# lines, it uses the original method to
|
154
|
+
# obtain them.
|
155
|
+
attr_reader :original
|
156
|
+
|
157
|
+
# Comparison operator, needed for equality
|
158
|
+
# and sorting
|
159
|
+
def <=>(other)
|
160
|
+
index <=> other.index
|
161
|
+
end
|
162
|
+
|
163
|
+
# [Not stable API]
|
164
|
+
#
|
165
|
+
# Lines that have a `on_ignored_nl` type token and NOT
|
166
|
+
# a `BEG` type seem to be a good proxy for the ability
|
167
|
+
# to join multiple lines into one.
|
168
|
+
#
|
169
|
+
# This predicate method is used to determine when those
|
170
|
+
# two criteria have been met.
|
171
|
+
#
|
172
|
+
# The one known case this doesn't handle is:
|
173
|
+
#
|
174
|
+
# Ripper.lex <<~EOM
|
175
|
+
# a &&
|
176
|
+
# b ||
|
177
|
+
# c
|
178
|
+
# EOM
|
179
|
+
#
|
180
|
+
# For some reason this introduces `on_ignore_newline` but with BEG type
|
181
|
+
def ignore_newline_not_beg?
|
182
|
+
lex_value = lex.detect { |l| l.type == :on_ignored_nl }
|
183
|
+
!!(lex_value && !lex_value.expr_beg?)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Determines if the given line has a trailing slash
|
187
|
+
#
|
188
|
+
# lines = CodeLine.from_source(<<~EOM)
|
189
|
+
# it "foo" \
|
190
|
+
# EOM
|
191
|
+
# expect(lines.first.trailing_slash?).to eq(true)
|
192
|
+
#
|
193
|
+
def trailing_slash?
|
194
|
+
last = @lex.last
|
195
|
+
return false unless last
|
196
|
+
return false unless last.type == :on_sp
|
197
|
+
|
198
|
+
last.token == TRAILING_SLASH
|
199
|
+
end
|
200
|
+
|
201
|
+
# Endless method detection
|
202
|
+
#
|
203
|
+
# From https://github.com/ruby/irb/commit/826ae909c9c93a2ddca6f9cfcd9c94dbf53d44ab
|
204
|
+
# Detecting a "oneliner" seems to need a state machine.
|
205
|
+
# This can be done by looking mostly at the "state" (last value):
|
206
|
+
#
|
207
|
+
# ENDFN -> BEG (token = '=' ) -> END
|
208
|
+
#
|
209
|
+
private def oneliner_method_count
|
210
|
+
oneliner_count = 0
|
211
|
+
in_oneliner_def = nil
|
212
|
+
|
213
|
+
@lex.each do |lex|
|
214
|
+
if in_oneliner_def.nil?
|
215
|
+
in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN)
|
216
|
+
elsif lex.state.allbits?(Ripper::EXPR_ENDFN)
|
217
|
+
# Continue
|
218
|
+
elsif lex.state.allbits?(Ripper::EXPR_BEG)
|
219
|
+
in_oneliner_def = :BODY if lex.token == "="
|
220
|
+
elsif lex.state.allbits?(Ripper::EXPR_END)
|
221
|
+
# We found an endless method, count it
|
222
|
+
oneliner_count += 1 if in_oneliner_def == :BODY
|
223
|
+
|
224
|
+
in_oneliner_def = nil
|
225
|
+
else
|
226
|
+
in_oneliner_def = nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
oneliner_count
|
231
|
+
end
|
149
232
|
end
|
150
233
|
end
|
data/lib/dead_end/code_search.rb
CHANGED
@@ -3,11 +3,19 @@
|
|
3
3
|
module DeadEnd
|
4
4
|
# Searches code for a syntax error
|
5
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 part.
|
13
|
+
#
|
6
14
|
# The bulk of the heavy lifting is done in:
|
7
15
|
#
|
8
16
|
# - CodeFrontier (Holds information for generating blocks and determining if we can stop searching)
|
9
17
|
# - ParseBlocksFromLine (Creates blocks into the frontier)
|
10
|
-
# - BlockExpand (Expands existing blocks to search more code
|
18
|
+
# - BlockExpand (Expands existing blocks to search more code)
|
11
19
|
#
|
12
20
|
# ## Syntax error detection
|
13
21
|
#
|
@@ -31,28 +39,24 @@ module DeadEnd
|
|
31
39
|
|
32
40
|
public
|
33
41
|
|
34
|
-
public
|
35
|
-
|
36
42
|
attr_reader :invalid_blocks, :record_dir, :code_lines
|
37
43
|
|
38
44
|
def initialize(source, record_dir: ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
|
39
|
-
@source = source
|
40
45
|
if record_dir
|
41
46
|
@time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
|
42
47
|
@record_dir = Pathname(record_dir).join(@time).tap { |p| p.mkpath }
|
43
48
|
@write_count = 0
|
44
49
|
end
|
45
|
-
code_lines = source.lines.map.with_index do |line, i|
|
46
|
-
CodeLine.new(line: line, index: i)
|
47
|
-
end
|
48
50
|
|
49
|
-
@
|
51
|
+
@tick = 0
|
52
|
+
@source = source
|
53
|
+
@name_tick = Hash.new { |hash, k| hash[k] = 0 }
|
54
|
+
@invalid_blocks = []
|
55
|
+
|
56
|
+
@code_lines = CleanDocument.new(source: source).call.lines
|
50
57
|
|
51
58
|
@frontier = CodeFrontier.new(code_lines: @code_lines)
|
52
|
-
@
|
53
|
-
@name_tick = Hash.new { |hash, k| hash[k] = 0 }
|
54
|
-
@tick = 0
|
55
|
-
@block_expand = BlockExpand.new(code_lines: code_lines)
|
59
|
+
@block_expand = BlockExpand.new(code_lines: @code_lines)
|
56
60
|
@parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
|
57
61
|
end
|
58
62
|
|
@@ -63,18 +67,19 @@ module DeadEnd
|
|
63
67
|
filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt"
|
64
68
|
if ENV["DEBUG"]
|
65
69
|
puts "\n\n==== #{filename} ===="
|
66
|
-
puts "\n```#{block.starts_at}
|
70
|
+
puts "\n```#{block.starts_at}..#{block.ends_at}"
|
67
71
|
puts block.to_s
|
68
72
|
puts "```"
|
69
|
-
puts " block indent:
|
73
|
+
puts " block indent: #{block.current_indent}"
|
70
74
|
end
|
71
75
|
@record_dir.join(filename).open(mode: "a") do |f|
|
72
|
-
|
73
|
-
|
76
|
+
document = DisplayCodeWithLineNumbers.new(
|
77
|
+
lines: @code_lines.select(&:visible?),
|
74
78
|
terminal: false,
|
75
|
-
|
76
|
-
)
|
77
|
-
|
79
|
+
highlight_lines: block.lines
|
80
|
+
).call
|
81
|
+
|
82
|
+
f.write(document)
|
78
83
|
end
|
79
84
|
end
|
80
85
|
|
@@ -122,26 +127,8 @@ module DeadEnd
|
|
122
127
|
push(block, name: "expand")
|
123
128
|
end
|
124
129
|
|
125
|
-
def sweep_heredocs
|
126
|
-
HeredocBlockParse.new(
|
127
|
-
source: @source,
|
128
|
-
code_lines: @code_lines
|
129
|
-
).call.each do |block|
|
130
|
-
push(block, name: "heredoc")
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def sweep_comments
|
135
|
-
lines = @code_lines.select(&:is_comment?)
|
136
|
-
return if lines.empty?
|
137
|
-
block = CodeBlock.new(lines: lines)
|
138
|
-
sweep(block: block, name: "comments")
|
139
|
-
end
|
140
|
-
|
141
130
|
# Main search loop
|
142
131
|
def call
|
143
|
-
sweep_heredocs
|
144
|
-
sweep_comments
|
145
132
|
until frontier.holds_all_syntax_errors?
|
146
133
|
@tick += 1
|
147
134
|
|
@@ -8,96 +8,67 @@ module DeadEnd
|
|
8
8
|
class DisplayInvalidBlocks
|
9
9
|
attr_reader :filename
|
10
10
|
|
11
|
-
def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal:
|
12
|
-
@terminal = terminal
|
13
|
-
@filename = filename
|
11
|
+
def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: DEFAULT_VALUE)
|
14
12
|
@io = io
|
15
|
-
|
16
13
|
@blocks = Array(blocks)
|
17
|
-
|
18
|
-
@invalid_lines = @blocks.map(&:lines).flatten
|
14
|
+
@filename = filename
|
19
15
|
@code_lines = code_lines
|
20
16
|
|
21
|
-
@
|
17
|
+
@terminal = terminal == DEFAULT_VALUE ? io.isatty : terminal
|
18
|
+
end
|
19
|
+
|
20
|
+
def document_ok?
|
21
|
+
@blocks.none? { |b| !b.hidden? }
|
22
22
|
end
|
23
23
|
|
24
24
|
def call
|
25
|
-
if
|
26
|
-
found_invalid_blocks
|
27
|
-
else
|
25
|
+
if document_ok?
|
28
26
|
@io.puts "Syntax OK"
|
27
|
+
return self
|
29
28
|
end
|
30
|
-
self
|
31
|
-
end
|
32
29
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
@io.puts banner
|
41
|
-
@io.puts
|
42
|
-
@io.puts("file: #{filename}") if filename
|
43
|
-
@io.puts <<~EOM
|
44
|
-
simplified:
|
30
|
+
if filename
|
31
|
+
@io.puts("--> #{filename}")
|
32
|
+
@io.puts
|
33
|
+
end
|
34
|
+
@blocks.each do |block|
|
35
|
+
display_block(block)
|
36
|
+
end
|
45
37
|
|
46
|
-
|
47
|
-
EOM
|
38
|
+
self
|
48
39
|
end
|
49
40
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
This code has a missing `end`. Ensure that all
|
57
|
-
syntax keywords (`def`, `do`, etc.) have a matching `end`.
|
58
|
-
EOM
|
59
|
-
when :unmatched_syntax
|
60
|
-
case @invalid_obj.unmatched_symbol
|
61
|
-
when :end
|
62
|
-
<<~EOM
|
63
|
-
DeadEnd: Unmatched `end` detected
|
64
|
-
|
65
|
-
This code has an unmatched `end`. Ensure that all `end` lines
|
66
|
-
in your code have a matching syntax keyword (`def`, `do`, etc.)
|
67
|
-
and that you don't have any extra `end` lines.
|
68
|
-
EOM
|
69
|
-
when :|
|
70
|
-
<<~EOM
|
71
|
-
DeadEnd: Unmatched `|` character detected
|
41
|
+
private def display_block(block)
|
42
|
+
# Build explanation
|
43
|
+
explain = ExplainSyntax.new(
|
44
|
+
code_lines: block.lines
|
45
|
+
).call
|
72
46
|
|
73
|
-
|
47
|
+
# Enhance code output
|
48
|
+
# Also handles several ambiguious cases
|
49
|
+
lines = CaptureCodeContext.new(
|
50
|
+
blocks: block,
|
51
|
+
code_lines: @code_lines
|
52
|
+
).call
|
74
53
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
54
|
+
# Build code output
|
55
|
+
document = DisplayCodeWithLineNumbers.new(
|
56
|
+
lines: lines,
|
57
|
+
terminal: @terminal,
|
58
|
+
highlight_lines: block.lines
|
59
|
+
).call
|
80
60
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
else
|
85
|
-
"DeadEnd: Unmatched `#{@invalid_obj.unmatched_symbol}` detected"
|
86
|
-
end
|
61
|
+
# Output syntax error explanation
|
62
|
+
explain.errors.each do |e|
|
63
|
+
@io.puts e
|
87
64
|
end
|
88
|
-
|
89
|
-
|
90
|
-
def indent(string, with: " ")
|
91
|
-
string.each_line.map { |l| with + l }.join
|
92
|
-
end
|
65
|
+
@io.puts
|
93
66
|
|
94
|
-
|
95
|
-
|
96
|
-
string << code_with_context
|
97
|
-
string
|
67
|
+
# Output code
|
68
|
+
@io.puts(document)
|
98
69
|
end
|
99
70
|
|
100
|
-
def code_with_context
|
71
|
+
private def code_with_context
|
101
72
|
lines = CaptureCodeContext.new(
|
102
73
|
blocks: @blocks,
|
103
74
|
code_lines: @code_lines
|
@@ -109,13 +80,5 @@ module DeadEnd
|
|
109
80
|
highlight_lines: @invalid_lines
|
110
81
|
).call
|
111
82
|
end
|
112
|
-
|
113
|
-
def code_with_lines
|
114
|
-
DisplayCodeWithLineNumbers.new(
|
115
|
-
lines: @code_lines.select(&:visible?),
|
116
|
-
terminal: @terminal,
|
117
|
-
highlight_lines: @invalid_lines
|
118
|
-
).call
|
119
|
-
end
|
120
83
|
end
|
121
84
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "left_right_lex_count"
|
4
|
+
|
5
|
+
module DeadEnd
|
6
|
+
# Explains syntax errors based on their source
|
7
|
+
#
|
8
|
+
# example:
|
9
|
+
#
|
10
|
+
# source = "def foo; puts 'lol'" # Note missing end
|
11
|
+
# explain ExplainSyntax.new(
|
12
|
+
# code_lines: CodeLine.from_source(source)
|
13
|
+
# ).call
|
14
|
+
# explain.errors.first
|
15
|
+
# # => "Unmatched keyword, missing `end' ?"
|
16
|
+
#
|
17
|
+
# When the error cannot be determined by lexical counting
|
18
|
+
# then ripper is run against the input and the raw ripper
|
19
|
+
# errors returned.
|
20
|
+
#
|
21
|
+
# Example:
|
22
|
+
#
|
23
|
+
# source = "1 * " # Note missing a second number
|
24
|
+
# explain ExplainSyntax.new(
|
25
|
+
# code_lines: CodeLine.from_source(source)
|
26
|
+
# ).call
|
27
|
+
# explain.errors.first
|
28
|
+
# # => "syntax error, unexpected end-of-input"
|
29
|
+
class ExplainSyntax
|
30
|
+
INVERSE = {
|
31
|
+
"{" => "}",
|
32
|
+
"}" => "{",
|
33
|
+
"[" => "]",
|
34
|
+
"]" => "[",
|
35
|
+
"(" => ")",
|
36
|
+
")" => "(",
|
37
|
+
"|" => "|"
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
def initialize(code_lines:)
|
41
|
+
@code_lines = code_lines
|
42
|
+
@left_right = LeftRightLexCount.new
|
43
|
+
@missing = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def call
|
47
|
+
@code_lines.each do |line|
|
48
|
+
line.lex.each do |lex|
|
49
|
+
@left_right.count_lex(lex)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns an array of missing elements
|
57
|
+
#
|
58
|
+
# For example this:
|
59
|
+
#
|
60
|
+
# ExplainSyntax.new(code_lines: lines).missing
|
61
|
+
# # => ["}"]
|
62
|
+
#
|
63
|
+
# Would indicate that the source is missing
|
64
|
+
# a `}` character in the source code
|
65
|
+
def missing
|
66
|
+
@missing ||= @left_right.missing
|
67
|
+
end
|
68
|
+
|
69
|
+
# Converts a missing string to
|
70
|
+
# an human understandable explanation.
|
71
|
+
#
|
72
|
+
# Example:
|
73
|
+
#
|
74
|
+
# explain.why("}")
|
75
|
+
# # => "Unmatched `{', missing `}' ?"
|
76
|
+
#
|
77
|
+
def why(miss)
|
78
|
+
case miss
|
79
|
+
when "keyword"
|
80
|
+
"Unmatched `end', missing keyword (`do', `def`, `if`, etc.) ?"
|
81
|
+
when "end"
|
82
|
+
"Unmatched keyword, missing `end' ?"
|
83
|
+
else
|
84
|
+
inverse = INVERSE.fetch(miss) {
|
85
|
+
raise "Unknown explain syntax char or key: #{miss.inspect}"
|
86
|
+
}
|
87
|
+
"Unmatched `#{inverse}', missing `#{miss}' ?"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns an array of syntax error messages
|
92
|
+
#
|
93
|
+
# If no missing pairs are found it falls back
|
94
|
+
# on the original ripper error messages
|
95
|
+
def errors
|
96
|
+
if missing.empty?
|
97
|
+
return RipperErrors.new(@code_lines.map(&:original).join).call.errors
|
98
|
+
end
|
99
|
+
|
100
|
+
missing.map { |miss| why(miss) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|