dead_end 2.0.0 → 3.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.
- 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 +26 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -2
- data/README.md +109 -21
- data/exe/dead_end +3 -66
- data/lib/dead_end/around_block_scan.rb +1 -1
- data/lib/dead_end/auto.rb +1 -21
- data/lib/dead_end/clean_document.rb +23 -32
- data/lib/dead_end/cli.rb +129 -0
- data/lib/dead_end/code_block.rb +18 -2
- data/lib/dead_end/code_frontier.rb +39 -13
- data/lib/dead_end/code_search.rb +7 -7
- data/lib/dead_end/display_invalid_blocks.rb +39 -78
- data/lib/dead_end/explain_syntax.rb +103 -0
- data/lib/dead_end/insertion_sort.rb +46 -0
- data/lib/dead_end/left_right_lex_count.rb +157 -0
- data/lib/dead_end/lex_all.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 +156 -1
- metadata +7 -5
- data/lib/dead_end/fyi.rb +0 -8
- data/lib/dead_end/internals.rb +0 -154
- data/lib/dead_end/who_dis_syntax_error.rb +0 -74
data/lib/dead_end/code_block.rb
CHANGED
@@ -70,8 +70,24 @@ module DeadEnd
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def valid?
|
73
|
-
|
74
|
-
|
73
|
+
if @valid == UNSET
|
74
|
+
# Performance optimization
|
75
|
+
#
|
76
|
+
# If all the lines were previously hidden
|
77
|
+
# and we expand to capture additional empty
|
78
|
+
# lines then the result cannot be invalid
|
79
|
+
#
|
80
|
+
# That means there's no reason to re-check all
|
81
|
+
# lines with ripper (which is expensive).
|
82
|
+
# Benchmark in commit message
|
83
|
+
@valid = if lines.all? { |l| l.hidden? || l.empty? }
|
84
|
+
true
|
85
|
+
else
|
86
|
+
DeadEnd.valid?(lines.map(&:original).join)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
@valid
|
90
|
+
end
|
75
91
|
end
|
76
92
|
|
77
93
|
def to_s
|
@@ -52,20 +52,44 @@ module DeadEnd
|
|
52
52
|
class CodeFrontier
|
53
53
|
def initialize(code_lines:)
|
54
54
|
@code_lines = code_lines
|
55
|
-
@frontier =
|
55
|
+
@frontier = InsertionSort.new
|
56
56
|
@unvisited_lines = @code_lines.sort_by(&:indent_index)
|
57
|
+
@has_run = false
|
58
|
+
@check_next = true
|
57
59
|
end
|
58
60
|
|
59
61
|
def count
|
60
|
-
@frontier.
|
62
|
+
@frontier.to_a.length
|
63
|
+
end
|
64
|
+
|
65
|
+
# Performance optimization
|
66
|
+
#
|
67
|
+
# Parsing with ripper is expensive
|
68
|
+
# If we know we don't have any blocks with invalid
|
69
|
+
# syntax, then we know we cannot have found
|
70
|
+
# the incorrect syntax yet.
|
71
|
+
#
|
72
|
+
# When an invalid block is added onto the frontier
|
73
|
+
# check document state
|
74
|
+
private def can_skip_check?
|
75
|
+
check_next = @check_next
|
76
|
+
@check_next = false
|
77
|
+
|
78
|
+
if check_next
|
79
|
+
false
|
80
|
+
else
|
81
|
+
true
|
82
|
+
end
|
61
83
|
end
|
62
84
|
|
63
85
|
# Returns true if the document is valid with all lines
|
64
86
|
# removed. By default it checks all blocks in present in
|
65
87
|
# the frontier array, but can be used for arbitrary arrays
|
66
88
|
# of codeblocks as well
|
67
|
-
def holds_all_syntax_errors?(block_array = @frontier)
|
68
|
-
|
89
|
+
def holds_all_syntax_errors?(block_array = @frontier, can_cache: true)
|
90
|
+
return false if can_cache && can_skip_check?
|
91
|
+
|
92
|
+
without_lines = block_array.to_a.flat_map do |block|
|
69
93
|
block.lines
|
70
94
|
end
|
71
95
|
|
@@ -77,7 +101,7 @@ module DeadEnd
|
|
77
101
|
|
78
102
|
# Returns a code block with the largest indentation possible
|
79
103
|
def pop
|
80
|
-
@frontier.pop
|
104
|
+
@frontier.to_a.pop
|
81
105
|
end
|
82
106
|
|
83
107
|
def next_indent_line
|
@@ -85,15 +109,15 @@ module DeadEnd
|
|
85
109
|
end
|
86
110
|
|
87
111
|
def expand?
|
88
|
-
return false if @frontier.empty?
|
89
|
-
return true if @unvisited_lines.empty?
|
112
|
+
return false if @frontier.to_a.empty?
|
113
|
+
return true if @unvisited_lines.to_a.empty?
|
90
114
|
|
91
|
-
frontier_indent = @frontier.last.current_indent
|
115
|
+
frontier_indent = @frontier.to_a.last.current_indent
|
92
116
|
unvisited_indent = next_indent_line.indent
|
93
117
|
|
94
118
|
if ENV["DEBUG"]
|
95
119
|
puts "```"
|
96
|
-
puts @frontier.last.to_s
|
120
|
+
puts @frontier.to_a.last.to_s
|
97
121
|
puts "```"
|
98
122
|
puts " @frontier indent: #{frontier_indent}"
|
99
123
|
puts " @unvisited indent: #{unvisited_indent}"
|
@@ -117,11 +141,13 @@ module DeadEnd
|
|
117
141
|
register_indent_block(block)
|
118
142
|
|
119
143
|
# Make sure we don't double expand, if a code block fully engulfs another code block, keep the bigger one
|
120
|
-
@frontier.reject! { |b|
|
144
|
+
@frontier.to_a.reject! { |b|
|
121
145
|
b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
|
122
146
|
}
|
147
|
+
|
148
|
+
@check_next = true if block.invalid?
|
123
149
|
@frontier << block
|
124
|
-
@frontier.sort!
|
150
|
+
# @frontier.sort!
|
125
151
|
|
126
152
|
self
|
127
153
|
end
|
@@ -141,8 +167,8 @@ module DeadEnd
|
|
141
167
|
# Given that we know our syntax error exists somewhere in our frontier, we want to find
|
142
168
|
# the smallest possible set of blocks that contain all the syntax errors
|
143
169
|
def detect_invalid_blocks
|
144
|
-
self.class.combination(@frontier.select(&:invalid?)).detect do |block_array|
|
145
|
-
holds_all_syntax_errors?(block_array)
|
170
|
+
self.class.combination(@frontier.to_a.select(&:invalid?)).detect do |block_array|
|
171
|
+
holds_all_syntax_errors?(block_array, can_cache: false)
|
146
172
|
end || []
|
147
173
|
end
|
148
174
|
end
|
data/lib/dead_end/code_search.rb
CHANGED
@@ -43,8 +43,7 @@ module DeadEnd
|
|
43
43
|
|
44
44
|
def initialize(source, record_dir: ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
|
45
45
|
if record_dir
|
46
|
-
@
|
47
|
-
@record_dir = Pathname(record_dir).join(@time).tap { |p| p.mkpath }
|
46
|
+
@record_dir = DeadEnd.record_dir(record_dir)
|
48
47
|
@write_count = 0
|
49
48
|
end
|
50
49
|
|
@@ -73,12 +72,13 @@ module DeadEnd
|
|
73
72
|
puts " block indent: #{block.current_indent}"
|
74
73
|
end
|
75
74
|
@record_dir.join(filename).open(mode: "a") do |f|
|
76
|
-
|
77
|
-
|
75
|
+
document = DisplayCodeWithLineNumbers.new(
|
76
|
+
lines: @code_lines.select(&:visible?),
|
78
77
|
terminal: false,
|
79
|
-
|
80
|
-
)
|
81
|
-
|
78
|
+
highlight_lines: block.lines
|
79
|
+
).call
|
80
|
+
|
81
|
+
f.write(document)
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
@@ -8,98 +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
|
-
EOM
|
36
|
-
end
|
37
|
-
|
38
|
-
private def found_invalid_blocks
|
39
|
-
@io.puts
|
40
|
-
if banner
|
41
|
-
@io.puts banner
|
30
|
+
if filename
|
31
|
+
@io.puts("--> #{filename}")
|
42
32
|
@io.puts
|
43
33
|
end
|
44
|
-
@
|
45
|
-
|
46
|
-
|
34
|
+
@blocks.each do |block|
|
35
|
+
display_block(block)
|
36
|
+
end
|
47
37
|
|
48
|
-
|
49
|
-
EOM
|
38
|
+
self
|
50
39
|
end
|
51
40
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
This code has a missing `end`. Ensure that all
|
59
|
-
syntax keywords (`def`, `do`, etc.) have a matching `end`.
|
60
|
-
EOM
|
61
|
-
when :unmatched_syntax
|
62
|
-
case @invalid_obj.unmatched_symbol
|
63
|
-
when :end
|
64
|
-
<<~EOM
|
65
|
-
DeadEnd: Unmatched `end` detected
|
66
|
-
|
67
|
-
This code has an unmatched `end`. Ensure that all `end` lines
|
68
|
-
in your code have a matching syntax keyword (`def`, `do`, etc.)
|
69
|
-
and that you don't have any extra `end` lines.
|
70
|
-
EOM
|
71
|
-
when :|
|
72
|
-
<<~EOM
|
73
|
-
DeadEnd: Unmatched `|` character detected
|
41
|
+
private def display_block(block)
|
42
|
+
# Build explanation
|
43
|
+
explain = ExplainSyntax.new(
|
44
|
+
code_lines: block.lines
|
45
|
+
).call
|
74
46
|
|
75
|
-
|
47
|
+
# Enhance code output
|
48
|
+
# Also handles several ambiguious cases
|
49
|
+
lines = CaptureCodeContext.new(
|
50
|
+
blocks: block,
|
51
|
+
code_lines: @code_lines
|
52
|
+
).call
|
76
53
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
54
|
+
# Build code output
|
55
|
+
document = DisplayCodeWithLineNumbers.new(
|
56
|
+
lines: lines,
|
57
|
+
terminal: @terminal,
|
58
|
+
highlight_lines: block.lines
|
59
|
+
).call
|
82
60
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
else
|
87
|
-
"DeadEnd: Unmatched `#{@invalid_obj.unmatched_symbol}` detected"
|
88
|
-
end
|
61
|
+
# Output syntax error explanation
|
62
|
+
explain.errors.each do |e|
|
63
|
+
@io.puts e
|
89
64
|
end
|
90
|
-
|
91
|
-
|
92
|
-
def indent(string, with: " ")
|
93
|
-
string.each_line.map { |l| with + l }.join
|
94
|
-
end
|
65
|
+
@io.puts
|
95
66
|
|
96
|
-
|
97
|
-
|
98
|
-
string << code_with_context
|
99
|
-
string
|
67
|
+
# Output code
|
68
|
+
@io.puts(document)
|
100
69
|
end
|
101
70
|
|
102
|
-
def code_with_context
|
71
|
+
private def code_with_context
|
103
72
|
lines = CaptureCodeContext.new(
|
104
73
|
blocks: @blocks,
|
105
74
|
code_lines: @code_lines
|
@@ -111,13 +80,5 @@ module DeadEnd
|
|
111
80
|
highlight_lines: @invalid_lines
|
112
81
|
).call
|
113
82
|
end
|
114
|
-
|
115
|
-
def code_with_lines
|
116
|
-
DisplayCodeWithLineNumbers.new(
|
117
|
-
lines: @code_lines.select(&:visible?),
|
118
|
-
terminal: @terminal,
|
119
|
-
highlight_lines: @invalid_lines
|
120
|
-
).call
|
121
|
-
end
|
122
83
|
end
|
123
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
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Sort elements on insert
|
5
|
+
#
|
6
|
+
# Instead of constantly calling `sort!`, put
|
7
|
+
# the element where it belongs the first time
|
8
|
+
# around
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# sorted = InsertionSort.new
|
13
|
+
# sorted << 33
|
14
|
+
# sorted << 44
|
15
|
+
# sorted << 1
|
16
|
+
# puts sorted.to_a
|
17
|
+
# # => [1, 44, 33]
|
18
|
+
#
|
19
|
+
class InsertionSort
|
20
|
+
def initialize
|
21
|
+
@array = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def <<(value)
|
25
|
+
insert_in = @array.length
|
26
|
+
@array.each.with_index do |existing, index|
|
27
|
+
case value <=> existing
|
28
|
+
when -1
|
29
|
+
insert_in = index
|
30
|
+
break
|
31
|
+
when 0
|
32
|
+
insert_in = index
|
33
|
+
break
|
34
|
+
when 1
|
35
|
+
# Keep going
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
@array.insert(insert_in, value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_a
|
43
|
+
@array
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Find mis-matched syntax based on lexical count
|
5
|
+
#
|
6
|
+
# Used for detecting missing pairs of elements
|
7
|
+
# each keyword needs an end, each '{' needs a '}'
|
8
|
+
# etc.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# left_right = LeftRightLexCount.new
|
13
|
+
# left_right.count_kw
|
14
|
+
# left_right.missing.first
|
15
|
+
# # => "end"
|
16
|
+
#
|
17
|
+
# left_right = LeftRightLexCount.new
|
18
|
+
# source = "{ a: b, c: d" # Note missing '}'
|
19
|
+
# LexAll.new(source: source).each do |lex|
|
20
|
+
# left_right.count_lex(lex)
|
21
|
+
# end
|
22
|
+
# left_right.missing.first
|
23
|
+
# # => "}"
|
24
|
+
class LeftRightLexCount
|
25
|
+
def initialize
|
26
|
+
@kw_count = 0
|
27
|
+
@end_count = 0
|
28
|
+
|
29
|
+
@count_for_char = {
|
30
|
+
"{" => 0,
|
31
|
+
"}" => 0,
|
32
|
+
"[" => 0,
|
33
|
+
"]" => 0,
|
34
|
+
"(" => 0,
|
35
|
+
")" => 0,
|
36
|
+
"|" => 0
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def count_kw
|
41
|
+
@kw_count += 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def count_end
|
45
|
+
@end_count += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
# Count source code characters
|
49
|
+
#
|
50
|
+
# Example:
|
51
|
+
#
|
52
|
+
# left_right = LeftRightLexCount.new
|
53
|
+
# left_right.count_lex(LexValue.new(1, :on_lbrace, "{", Ripper::EXPR_BEG))
|
54
|
+
# left_right.count_for_char("{")
|
55
|
+
# # => 1
|
56
|
+
# left_right.count_for_char("}")
|
57
|
+
# # => 0
|
58
|
+
def count_lex(lex)
|
59
|
+
case lex.type
|
60
|
+
when :on_tstring_content
|
61
|
+
# ^^^
|
62
|
+
# Means it's a string or a symbol `"{"` rather than being
|
63
|
+
# part of a data structure (like a hash) `{ a: b }`
|
64
|
+
when :on_embexpr_beg
|
65
|
+
# ^^^
|
66
|
+
# Embedded string expressions like `"#{foo} <-embed"`
|
67
|
+
# are parsed with chars:
|
68
|
+
#
|
69
|
+
# `#{` as :on_embexpr_beg
|
70
|
+
# `}` as :on_embexpr_end
|
71
|
+
#
|
72
|
+
# We cannot ignore both :on_emb_expr_beg and :on_embexpr_end
|
73
|
+
# because sometimes the lexer thinks something is an embed
|
74
|
+
# string end, when it is not like `lol = }` (no clue why).
|
75
|
+
#
|
76
|
+
# When we see `#{` count it as a `{` or we will
|
77
|
+
# have a mis-match count.
|
78
|
+
#
|
79
|
+
case lex.token
|
80
|
+
when "\#{"
|
81
|
+
@count_for_char["{"] += 1
|
82
|
+
end
|
83
|
+
else
|
84
|
+
@end_count += 1 if lex.is_end?
|
85
|
+
@kw_count += 1 if lex.is_kw?
|
86
|
+
@count_for_char[lex.token] += 1 if @count_for_char.key?(lex.token)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def count_for_char(char)
|
91
|
+
@count_for_char[char]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns an array of missing syntax characters
|
95
|
+
# or `"end"` or `"keyword"`
|
96
|
+
#
|
97
|
+
# left_right.missing
|
98
|
+
# # => ["}"]
|
99
|
+
def missing
|
100
|
+
out = missing_pairs
|
101
|
+
out << missing_pipe
|
102
|
+
out << missing_keyword_end
|
103
|
+
out.compact!
|
104
|
+
out
|
105
|
+
end
|
106
|
+
|
107
|
+
PAIRS = {
|
108
|
+
"{" => "}",
|
109
|
+
"[" => "]",
|
110
|
+
"(" => ")"
|
111
|
+
}.freeze
|
112
|
+
|
113
|
+
# Opening characters like `{` need closing characters # like `}`.
|
114
|
+
#
|
115
|
+
# When a mis-match count is detected, suggest the
|
116
|
+
# missing member.
|
117
|
+
#
|
118
|
+
# For example if there are 3 `}` and only two `{`
|
119
|
+
# return `"{"`
|
120
|
+
private def missing_pairs
|
121
|
+
PAIRS.map do |(left, right)|
|
122
|
+
case @count_for_char[left] <=> @count_for_char[right]
|
123
|
+
when 1
|
124
|
+
right
|
125
|
+
when 0
|
126
|
+
nil
|
127
|
+
when -1
|
128
|
+
left
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Keywords need ends and ends need keywords
|
134
|
+
#
|
135
|
+
# If we have more keywords, there's a missing `end`
|
136
|
+
# if we have more `end`-s, there's a missing keyword
|
137
|
+
private def missing_keyword_end
|
138
|
+
case @kw_count <=> @end_count
|
139
|
+
when 1
|
140
|
+
"end"
|
141
|
+
when 0
|
142
|
+
nil
|
143
|
+
when -1
|
144
|
+
"keyword"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Pipes come in pairs.
|
149
|
+
# If there's an odd number of pipes then we
|
150
|
+
# are missing one
|
151
|
+
private def missing_pipe
|
152
|
+
if @count_for_char["|"].odd?
|
153
|
+
"|"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/dead_end/lex_all.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Capture parse errors from ripper
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# puts RipperErrors.new(" def foo").call.errors
|
9
|
+
# # => ["syntax error, unexpected end-of-input, expecting ';' or '\\n'"]
|
10
|
+
class RipperErrors < Ripper
|
11
|
+
attr_reader :errors
|
12
|
+
|
13
|
+
# Comes from ripper, called
|
14
|
+
# on every parse error, msg
|
15
|
+
# is a string
|
16
|
+
def on_parse_error(msg)
|
17
|
+
@errors ||= []
|
18
|
+
@errors << msg
|
19
|
+
end
|
20
|
+
|
21
|
+
def call
|
22
|
+
@run_once ||= begin
|
23
|
+
@errors = []
|
24
|
+
parse
|
25
|
+
true
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/dead_end/version.rb
CHANGED