dead_end 1.1.5 → 2.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 +14 -9
- data/.standard.yml +1 -0
- data/CHANGELOG.md +21 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +29 -2
- data/README.md +1 -19
- data/Rakefile +1 -1
- data/dead_end.gemspec +12 -12
- data/exe/dead_end +3 -3
- data/lib/dead_end/around_block_scan.rb +17 -19
- data/lib/dead_end/auto.rb +3 -52
- data/lib/dead_end/block_expand.rb +6 -5
- data/lib/dead_end/capture_code_context.rb +167 -50
- data/lib/dead_end/clean_document.rb +313 -0
- data/lib/dead_end/code_block.rb +3 -3
- data/lib/dead_end/code_frontier.rb +28 -17
- data/lib/dead_end/code_line.rb +160 -77
- data/lib/dead_end/code_search.rb +37 -48
- data/lib/dead_end/display_code_with_line_numbers.rb +7 -8
- data/lib/dead_end/display_invalid_blocks.rb +10 -9
- data/lib/dead_end/fyi.rb +2 -1
- data/lib/dead_end/internals.rb +23 -27
- data/lib/dead_end/lex_all.rb +16 -32
- data/lib/dead_end/lex_value.rb +62 -0
- data/lib/dead_end/parse_blocks_from_indent_line.rb +3 -4
- data/lib/dead_end/version.rb +1 -1
- data/lib/dead_end/who_dis_syntax_error.rb +14 -9
- metadata +6 -6
- data/.travis.yml +0 -6
- data/lib/dead_end/heredoc_block_parse.rb +0 -30
- data/lib/dead_end/trailing_slash_join.rb +0 -53
@@ -3,11 +3,19 @@
|
|
3
3
|
module DeadEnd
|
4
4
|
# The main function of the frontier is to hold the edges of our search and to
|
5
5
|
# evaluate when we can stop searching.
|
6
|
+
|
7
|
+
# There are three main phases in the algorithm:
|
8
|
+
#
|
9
|
+
# 1. Sanitize/format input source
|
10
|
+
# 2. Search for invalid blocks
|
11
|
+
# 3. Format invalid blocks into something meaninful
|
12
|
+
#
|
13
|
+
# The Code frontier is a critical part of the second step
|
6
14
|
#
|
7
15
|
# ## Knowing where we've been
|
8
16
|
#
|
9
|
-
# Once a code block is generated it is added onto the frontier
|
10
|
-
# sorted
|
17
|
+
# Once a code block is generated it is added onto the frontier. Then it will be
|
18
|
+
# sorted by indentation and frontier can be filtered. Large blocks that fully enclose a
|
11
19
|
# smaller block will cause the smaller block to be evicted.
|
12
20
|
#
|
13
21
|
# CodeFrontier#<<(block) # Adds block to frontier
|
@@ -15,11 +23,11 @@ module DeadEnd
|
|
15
23
|
#
|
16
24
|
# ## Knowing where we can go
|
17
25
|
#
|
18
|
-
# Internally
|
19
|
-
# when called this
|
26
|
+
# Internally the frontier keeps track of "unvisited" lines which are exposed via `next_indent_line`
|
27
|
+
# when called, this method returns, a line of code with the highest indentation.
|
20
28
|
#
|
21
|
-
#
|
22
|
-
# is added back to the frontier,
|
29
|
+
# The returned line of code can be used to build a CodeBlock and then that code block
|
30
|
+
# is added back to the frontier. Then, the lines are removed from the
|
23
31
|
# "unvisited" so we don't double-create the same block.
|
24
32
|
#
|
25
33
|
# CodeFrontier#next_indent_line # Shows next line
|
@@ -27,19 +35,22 @@ module DeadEnd
|
|
27
35
|
#
|
28
36
|
# ## Knowing when to stop
|
29
37
|
#
|
30
|
-
# The frontier
|
31
|
-
#
|
38
|
+
# The frontier knows how to check the entire document for a syntax error. When blocks
|
39
|
+
# are added onto the frontier, they're removed from the document. When all code containing
|
40
|
+
# syntax errors has been added to the frontier, the document will be parsable without a
|
41
|
+
# syntax error and the search can stop.
|
32
42
|
#
|
33
|
-
# CodeFrontier#holds_all_syntax_errors?
|
43
|
+
# CodeFrontier#holds_all_syntax_errors? # Returns true when frontier holds all syntax errors
|
34
44
|
#
|
35
45
|
# ## Filtering false positives
|
36
46
|
#
|
37
|
-
# Once the search is completed, the frontier
|
38
|
-
# the syntax error. To
|
47
|
+
# Once the search is completed, the frontier may have multiple blocks that do not contain
|
48
|
+
# the syntax error. To limit the result to the smallest subset of "invalid blocks" call:
|
39
49
|
#
|
40
50
|
# CodeFrontier#detect_invalid_blocks
|
51
|
+
#
|
41
52
|
class CodeFrontier
|
42
|
-
def initialize(code_lines:
|
53
|
+
def initialize(code_lines:)
|
43
54
|
@code_lines = code_lines
|
44
55
|
@frontier = []
|
45
56
|
@unvisited_lines = @code_lines.sort_by(&:indent_index)
|
@@ -66,7 +77,7 @@ module DeadEnd
|
|
66
77
|
|
67
78
|
# Returns a code block with the largest indentation possible
|
68
79
|
def pop
|
69
|
-
|
80
|
+
@frontier.pop
|
70
81
|
end
|
71
82
|
|
72
83
|
def next_indent_line
|
@@ -78,14 +89,14 @@ module DeadEnd
|
|
78
89
|
return true if @unvisited_lines.empty?
|
79
90
|
|
80
91
|
frontier_indent = @frontier.last.current_indent
|
81
|
-
unvisited_indent= next_indent_line.indent
|
92
|
+
unvisited_indent = next_indent_line.indent
|
82
93
|
|
83
94
|
if ENV["DEBUG"]
|
84
95
|
puts "```"
|
85
96
|
puts @frontier.last.to_s
|
86
97
|
puts "```"
|
87
|
-
puts " @frontier indent:
|
88
|
-
puts " @unvisited indent:
|
98
|
+
puts " @frontier indent: #{frontier_indent}"
|
99
|
+
puts " @unvisited indent: #{unvisited_indent}"
|
89
100
|
end
|
90
101
|
|
91
102
|
# Expand all blocks before moving to unvisited lines
|
@@ -106,7 +117,7 @@ module DeadEnd
|
|
106
117
|
register_indent_block(block)
|
107
118
|
|
108
119
|
# Make sure we don't double expand, if a code block fully engulfs another code block, keep the bigger one
|
109
|
-
@frontier.reject! {|b|
|
120
|
+
@frontier.reject! { |b|
|
110
121
|
b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
|
111
122
|
}
|
112
123
|
@frontier << block
|
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
|
-
|
56
|
-
lex_detect!
|
57
|
-
end
|
58
55
|
|
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 if !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 :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 :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
|
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
|
148
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
|
#
|
@@ -25,65 +33,64 @@ module DeadEnd
|
|
25
33
|
# # => ["def lol\n"]
|
26
34
|
#
|
27
35
|
class CodeSearch
|
28
|
-
private
|
29
|
-
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :frontier
|
39
|
+
|
40
|
+
public
|
41
|
+
|
42
|
+
attr_reader :invalid_blocks, :record_dir, :code_lines
|
30
43
|
|
31
44
|
def initialize(source, record_dir: ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
|
32
|
-
@source = source
|
33
45
|
if record_dir
|
34
|
-
@time = Time.now.strftime(
|
35
|
-
@record_dir = Pathname(record_dir).join(@time).tap {|p| p.mkpath }
|
46
|
+
@time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
|
47
|
+
@record_dir = Pathname(record_dir).join(@time).tap { |p| p.mkpath }
|
36
48
|
@write_count = 0
|
37
49
|
end
|
38
|
-
code_lines = source.lines.map.with_index do |line, i|
|
39
|
-
CodeLine.new(line: line, index: i)
|
40
|
-
end
|
41
50
|
|
42
|
-
@
|
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
|
43
57
|
|
44
58
|
@frontier = CodeFrontier.new(code_lines: @code_lines)
|
45
|
-
@
|
46
|
-
@name_tick = Hash.new {|hash, k| hash[k] = 0 }
|
47
|
-
@tick = 0
|
48
|
-
@block_expand = BlockExpand.new(code_lines: code_lines)
|
59
|
+
@block_expand = BlockExpand.new(code_lines: @code_lines)
|
49
60
|
@parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
|
50
61
|
end
|
51
62
|
|
52
63
|
# Used for debugging
|
53
64
|
def record(block:, name: "record")
|
54
|
-
return
|
65
|
+
return unless @record_dir
|
55
66
|
@name_tick[name] += 1
|
56
67
|
filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt"
|
57
68
|
if ENV["DEBUG"]
|
58
69
|
puts "\n\n==== #{filename} ===="
|
59
|
-
puts "\n```#{block.starts_at}
|
60
|
-
puts
|
70
|
+
puts "\n```#{block.starts_at}..#{block.ends_at}"
|
71
|
+
puts block.to_s
|
61
72
|
puts "```"
|
62
|
-
puts " block indent:
|
73
|
+
puts " block indent: #{block.current_indent}"
|
63
74
|
end
|
64
75
|
@record_dir.join(filename).open(mode: "a") do |f|
|
65
76
|
display = DisplayInvalidBlocks.new(
|
66
77
|
blocks: block,
|
67
78
|
terminal: false,
|
68
|
-
code_lines: @code_lines
|
79
|
+
code_lines: @code_lines
|
69
80
|
)
|
70
|
-
f.write(display.indent
|
81
|
+
f.write(display.indent(display.code_with_lines))
|
71
82
|
end
|
72
83
|
end
|
73
84
|
|
74
|
-
def push(block, name:
|
85
|
+
def push(block, name:)
|
75
86
|
record(block: block, name: name)
|
76
87
|
|
77
|
-
if block.valid?
|
78
|
-
|
79
|
-
frontier << block
|
80
|
-
else
|
81
|
-
frontier << block
|
82
|
-
end
|
88
|
+
block.mark_invisible if block.valid?
|
89
|
+
frontier << block
|
83
90
|
end
|
84
91
|
|
85
92
|
# Removes the block without putting it back in the frontier
|
86
|
-
def sweep(block:, name:
|
93
|
+
def sweep(block:, name:)
|
87
94
|
record(block: block, name: name)
|
88
95
|
|
89
96
|
block.lines.each(&:mark_invisible)
|
@@ -119,26 +126,8 @@ module DeadEnd
|
|
119
126
|
push(block, name: "expand")
|
120
127
|
end
|
121
128
|
|
122
|
-
def sweep_heredocs
|
123
|
-
HeredocBlockParse.new(
|
124
|
-
source: @source,
|
125
|
-
code_lines: @code_lines
|
126
|
-
).call.each do |block|
|
127
|
-
push(block, name: "heredoc")
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def sweep_comments
|
132
|
-
lines = @code_lines.select(&:is_comment?)
|
133
|
-
return if lines.empty?
|
134
|
-
block = CodeBlock.new(lines: lines)
|
135
|
-
sweep(block: block, name: "comments")
|
136
|
-
end
|
137
|
-
|
138
129
|
# Main search loop
|
139
130
|
def call
|
140
|
-
sweep_heredocs
|
141
|
-
sweep_comments
|
142
131
|
until frontier.holds_all_syntax_errors?
|
143
132
|
@tick += 1
|
144
133
|
|
@@ -149,8 +138,8 @@ module DeadEnd
|
|
149
138
|
end
|
150
139
|
end
|
151
140
|
|
152
|
-
@invalid_blocks.concat(frontier.detect_invalid_blocks
|
153
|
-
@invalid_blocks.sort_by! {|block| block.starts_at }
|
141
|
+
@invalid_blocks.concat(frontier.detect_invalid_blocks)
|
142
|
+
@invalid_blocks.sort_by! { |block| block.starts_at }
|
154
143
|
self
|
155
144
|
end
|
156
145
|
end
|
@@ -7,7 +7,6 @@ module DeadEnd
|
|
7
7
|
# even if it is "marked invisible" any filtering of
|
8
8
|
# output should be done before calling this class.
|
9
9
|
#
|
10
|
-
#
|
11
10
|
# DisplayCodeWithLineNumbers.new(
|
12
11
|
# lines: lines,
|
13
12
|
# highlight_lines: [lines[2], lines[3]]
|
@@ -23,10 +22,10 @@ module DeadEnd
|
|
23
22
|
TERMINAL_HIGHLIGHT = "\e[1;3m" # Bold, italics
|
24
23
|
TERMINAL_END = "\e[0m"
|
25
24
|
|
26
|
-
def initialize(lines
|
25
|
+
def initialize(lines:, highlight_lines: [], terminal: false)
|
27
26
|
@lines = Array(lines).sort
|
28
27
|
@terminal = terminal
|
29
|
-
@highlight_line_hash = Array(highlight_lines).each_with_object({}) {|line, h| h[line] = true
|
28
|
+
@highlight_line_hash = Array(highlight_lines).each_with_object({}) { |line, h| h[line] = true }
|
30
29
|
@digit_count = @lines.last&.line_number.to_s.length
|
31
30
|
end
|
32
31
|
|
@@ -48,12 +47,12 @@ module DeadEnd
|
|
48
47
|
end.join
|
49
48
|
end
|
50
49
|
|
51
|
-
private def format(contents
|
52
|
-
string =
|
53
|
-
if highlight
|
54
|
-
|
50
|
+
private def format(contents:, number:, empty:, highlight: false)
|
51
|
+
string = +""
|
52
|
+
string << if highlight
|
53
|
+
"❯ "
|
55
54
|
else
|
56
|
-
|
55
|
+
" "
|
57
56
|
end
|
58
57
|
|
59
58
|
string << number.rjust(@digit_count).to_s
|
@@ -8,7 +8,7 @@ module DeadEnd
|
|
8
8
|
class DisplayInvalidBlocks
|
9
9
|
attr_reader :filename
|
10
10
|
|
11
|
-
def initialize(code_lines
|
11
|
+
def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: false, invalid_obj: WhoDisSyntaxError::Null.new)
|
12
12
|
@terminal = terminal
|
13
13
|
@filename = filename
|
14
14
|
@io = io
|
@@ -37,8 +37,10 @@ module DeadEnd
|
|
37
37
|
|
38
38
|
private def found_invalid_blocks
|
39
39
|
@io.puts
|
40
|
-
|
41
|
-
|
40
|
+
if banner
|
41
|
+
@io.puts banner
|
42
|
+
@io.puts
|
43
|
+
end
|
42
44
|
@io.puts("file: #{filename}") if filename
|
43
45
|
@io.puts <<~EOM
|
44
46
|
simplified:
|
@@ -78,22 +80,21 @@ module DeadEnd
|
|
78
80
|
<<~EOM
|
79
81
|
DeadEnd: Unmatched `}` character detected
|
80
82
|
|
81
|
-
This code has an unmatched `}`. Ensure that opening
|
83
|
+
This code has an unmatched `}`. Ensure that opening curly braces are
|
82
84
|
closed: `{ }`.
|
83
85
|
EOM
|
84
86
|
else
|
85
87
|
"DeadEnd: Unmatched `#{@invalid_obj.unmatched_symbol}` detected"
|
86
88
|
end
|
87
89
|
end
|
88
|
-
|
89
90
|
end
|
90
91
|
|
91
92
|
def indent(string, with: " ")
|
92
|
-
string.each_line.map {|l| with
|
93
|
+
string.each_line.map { |l| with + l }.join
|
93
94
|
end
|
94
95
|
|
95
96
|
def code_block
|
96
|
-
string =
|
97
|
+
string = +""
|
97
98
|
string << code_with_context
|
98
99
|
string
|
99
100
|
end
|
@@ -107,7 +108,7 @@ module DeadEnd
|
|
107
108
|
DisplayCodeWithLineNumbers.new(
|
108
109
|
lines: lines,
|
109
110
|
terminal: @terminal,
|
110
|
-
highlight_lines: @invalid_lines
|
111
|
+
highlight_lines: @invalid_lines
|
111
112
|
).call
|
112
113
|
end
|
113
114
|
|
@@ -115,7 +116,7 @@ module DeadEnd
|
|
115
116
|
DisplayCodeWithLineNumbers.new(
|
116
117
|
lines: @code_lines.select(&:visible?),
|
117
118
|
terminal: @terminal,
|
118
|
-
highlight_lines: @invalid_lines
|
119
|
+
highlight_lines: @invalid_lines
|
119
120
|
).call
|
120
121
|
end
|
121
122
|
end
|
data/lib/dead_end/fyi.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require_relative "../dead_end/internals"
|
2
2
|
|
3
|
-
require_relative "auto
|
3
|
+
require_relative "auto"
|
4
4
|
|
5
5
|
DeadEnd.send(:remove_const, :SEARCH_SOURCE_ON_ERROR_DEFAULT)
|
6
6
|
DeadEnd::SEARCH_SOURCE_ON_ERROR_DEFAULT = false
|
7
7
|
|
8
|
+
warn "DEPRECATED: calling `require 'dead_end/fyi'` is deprecated, `require 'dead_end'` instead"
|