dead_end 2.0.2 → 3.0.3
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/CHANGELOG.md +25 -0
- data/CODE_OF_CONDUCT.md +2 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -2
- data/README.md +118 -23
- data/exe/dead_end +3 -77
- data/lib/dead_end/auto.rb +1 -21
- data/lib/dead_end/clean_document.rb +21 -30
- data/lib/dead_end/cli.rb +129 -0
- data/lib/dead_end/code_block.rb +18 -2
- data/lib/dead_end/code_frontier.rb +48 -14
- data/lib/dead_end/code_line.rb +27 -21
- data/lib/dead_end/code_search.rb +7 -7
- data/lib/dead_end/display_invalid_blocks.rb +37 -46
- 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 +168 -0
- data/lib/dead_end/lex_all.rb +12 -8
- data/lib/dead_end/lex_value.rb +2 -0
- data/lib/dead_end/pathname_from_message.rb +47 -0
- data/lib/dead_end/ripper_errors.rb +36 -0
- data/lib/dead_end/version.rb +1 -1
- data/lib/dead_end.rb +161 -1
- metadata +8 -6
- data/lib/dead_end/banner.rb +0 -58
- data/lib/dead_end/fyi.rb +0 -8
- data/lib/dead_end/internals.rb +0 -157
- data/lib/dead_end/who_dis_syntax_error.rb +0 -83
@@ -52,20 +52,46 @@ 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
|
+
@visited_lines = {}
|
58
|
+
|
59
|
+
@has_run = false
|
60
|
+
@check_next = true
|
57
61
|
end
|
58
62
|
|
59
63
|
def count
|
60
|
-
@frontier.
|
64
|
+
@frontier.to_a.length
|
65
|
+
end
|
66
|
+
|
67
|
+
# Performance optimization
|
68
|
+
#
|
69
|
+
# Parsing with ripper is expensive
|
70
|
+
# If we know we don't have any blocks with invalid
|
71
|
+
# syntax, then we know we cannot have found
|
72
|
+
# the incorrect syntax yet.
|
73
|
+
#
|
74
|
+
# When an invalid block is added onto the frontier
|
75
|
+
# check document state
|
76
|
+
private def can_skip_check?
|
77
|
+
check_next = @check_next
|
78
|
+
@check_next = false
|
79
|
+
|
80
|
+
if check_next
|
81
|
+
false
|
82
|
+
else
|
83
|
+
true
|
84
|
+
end
|
61
85
|
end
|
62
86
|
|
63
87
|
# Returns true if the document is valid with all lines
|
64
88
|
# removed. By default it checks all blocks in present in
|
65
89
|
# the frontier array, but can be used for arbitrary arrays
|
66
90
|
# of codeblocks as well
|
67
|
-
def holds_all_syntax_errors?(block_array = @frontier)
|
68
|
-
|
91
|
+
def holds_all_syntax_errors?(block_array = @frontier, can_cache: true)
|
92
|
+
return false if can_cache && can_skip_check?
|
93
|
+
|
94
|
+
without_lines = block_array.to_a.flat_map do |block|
|
69
95
|
block.lines
|
70
96
|
end
|
71
97
|
|
@@ -77,7 +103,7 @@ module DeadEnd
|
|
77
103
|
|
78
104
|
# Returns a code block with the largest indentation possible
|
79
105
|
def pop
|
80
|
-
@frontier.pop
|
106
|
+
@frontier.to_a.pop
|
81
107
|
end
|
82
108
|
|
83
109
|
def next_indent_line
|
@@ -85,15 +111,15 @@ module DeadEnd
|
|
85
111
|
end
|
86
112
|
|
87
113
|
def expand?
|
88
|
-
return false if @frontier.empty?
|
89
|
-
return true if @unvisited_lines.empty?
|
114
|
+
return false if @frontier.to_a.empty?
|
115
|
+
return true if @unvisited_lines.to_a.empty?
|
90
116
|
|
91
|
-
frontier_indent = @frontier.last.current_indent
|
117
|
+
frontier_indent = @frontier.to_a.last.current_indent
|
92
118
|
unvisited_indent = next_indent_line.indent
|
93
119
|
|
94
120
|
if ENV["DEBUG"]
|
95
121
|
puts "```"
|
96
|
-
puts @frontier.last.to_s
|
122
|
+
puts @frontier.to_a.last.to_s
|
97
123
|
puts "```"
|
98
124
|
puts " @frontier indent: #{frontier_indent}"
|
99
125
|
puts " @unvisited indent: #{unvisited_indent}"
|
@@ -104,7 +130,13 @@ module DeadEnd
|
|
104
130
|
end
|
105
131
|
|
106
132
|
def register_indent_block(block)
|
107
|
-
|
133
|
+
block.lines.each do |line|
|
134
|
+
next if @visited_lines[line]
|
135
|
+
@visited_lines[line] = true
|
136
|
+
|
137
|
+
index = @unvisited_lines.bsearch_index { |l| line.indent_index <=> l.indent_index }
|
138
|
+
@unvisited_lines.delete_at(index)
|
139
|
+
end
|
108
140
|
self
|
109
141
|
end
|
110
142
|
|
@@ -117,11 +149,13 @@ module DeadEnd
|
|
117
149
|
register_indent_block(block)
|
118
150
|
|
119
151
|
# Make sure we don't double expand, if a code block fully engulfs another code block, keep the bigger one
|
120
|
-
@frontier.reject! { |b|
|
152
|
+
@frontier.to_a.reject! { |b|
|
121
153
|
b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
|
122
154
|
}
|
155
|
+
|
156
|
+
@check_next = true if block.invalid?
|
123
157
|
@frontier << block
|
124
|
-
@frontier.sort!
|
158
|
+
# @frontier.sort!
|
125
159
|
|
126
160
|
self
|
127
161
|
end
|
@@ -141,8 +175,8 @@ module DeadEnd
|
|
141
175
|
# Given that we know our syntax error exists somewhere in our frontier, we want to find
|
142
176
|
# the smallest possible set of blocks that contain all the syntax errors
|
143
177
|
def detect_invalid_blocks
|
144
|
-
self.class.combination(@frontier.select(&:invalid?)).detect do |block_array|
|
145
|
-
holds_all_syntax_errors?(block_array)
|
178
|
+
self.class.combination(@frontier.to_a.select(&:invalid?)).detect do |block_array|
|
179
|
+
holds_all_syntax_errors?(block_array, can_cache: false)
|
146
180
|
end || []
|
147
181
|
end
|
148
182
|
end
|
data/lib/dead_end/code_line.rb
CHANGED
@@ -26,9 +26,10 @@ module DeadEnd
|
|
26
26
|
|
27
27
|
# Returns an array of CodeLine objects
|
28
28
|
# from the source string
|
29
|
-
def self.from_source(source)
|
30
|
-
|
31
|
-
source
|
29
|
+
def self.from_source(source, lines: nil)
|
30
|
+
lines ||= source.lines
|
31
|
+
lex_array_for_line = LexAll.new(source: source, source_lines: lines).each_with_object(Hash.new { |h, k| h[k] = [] }) { |lex, hash| hash[lex.line] << lex }
|
32
|
+
lines.map.with_index do |line, index|
|
32
33
|
CodeLine.new(
|
33
34
|
line: line,
|
34
35
|
index: index,
|
@@ -42,28 +43,20 @@ module DeadEnd
|
|
42
43
|
@lex = lex
|
43
44
|
@line = line
|
44
45
|
@index = index
|
45
|
-
@original = line
|
46
|
+
@original = line
|
46
47
|
@line_number = @index + 1
|
48
|
+
strip_line = line.dup
|
49
|
+
strip_line.lstrip!
|
47
50
|
|
48
|
-
if
|
51
|
+
if strip_line.empty?
|
49
52
|
@empty = true
|
50
53
|
@indent = 0
|
51
54
|
else
|
52
55
|
@empty = false
|
53
|
-
@indent =
|
56
|
+
@indent = line.length - strip_line.length
|
54
57
|
end
|
55
58
|
|
56
|
-
|
57
|
-
end_count = 0
|
58
|
-
@lex.each do |lex|
|
59
|
-
kw_count += 1 if lex.is_kw?
|
60
|
-
end_count += 1 if lex.is_end?
|
61
|
-
end
|
62
|
-
|
63
|
-
kw_count -= oneliner_method_count
|
64
|
-
|
65
|
-
@is_kw = (kw_count - end_count) > 0
|
66
|
-
@is_end = (end_count - kw_count) > 0
|
59
|
+
set_kw_end
|
67
60
|
end
|
68
61
|
|
69
62
|
# Used for stable sort via indentation level
|
@@ -179,8 +172,7 @@ module DeadEnd
|
|
179
172
|
#
|
180
173
|
# For some reason this introduces `on_ignore_newline` but with BEG type
|
181
174
|
def ignore_newline_not_beg?
|
182
|
-
|
183
|
-
!!(lex_value && !lex_value.expr_beg?)
|
175
|
+
@ignore_newline_not_beg
|
184
176
|
end
|
185
177
|
|
186
178
|
# Determines if the given line has a trailing slash
|
@@ -206,11 +198,22 @@ module DeadEnd
|
|
206
198
|
#
|
207
199
|
# ENDFN -> BEG (token = '=' ) -> END
|
208
200
|
#
|
209
|
-
private def
|
201
|
+
private def set_kw_end
|
210
202
|
oneliner_count = 0
|
211
203
|
in_oneliner_def = nil
|
212
204
|
|
205
|
+
kw_count = 0
|
206
|
+
end_count = 0
|
207
|
+
|
208
|
+
@ignore_newline_not_beg = false
|
213
209
|
@lex.each do |lex|
|
210
|
+
kw_count += 1 if lex.is_kw?
|
211
|
+
end_count += 1 if lex.is_end?
|
212
|
+
|
213
|
+
if lex.type == :on_ignored_nl
|
214
|
+
@ignore_newline_not_beg = !lex.expr_beg?
|
215
|
+
end
|
216
|
+
|
214
217
|
if in_oneliner_def.nil?
|
215
218
|
in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN)
|
216
219
|
elsif lex.state.allbits?(Ripper::EXPR_ENDFN)
|
@@ -227,7 +230,10 @@ module DeadEnd
|
|
227
230
|
end
|
228
231
|
end
|
229
232
|
|
230
|
-
oneliner_count
|
233
|
+
kw_count -= oneliner_count
|
234
|
+
|
235
|
+
@is_kw = (kw_count - end_count) > 0
|
236
|
+
@is_end = (end_count - kw_count) > 0
|
231
237
|
end
|
232
238
|
end
|
233
239
|
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
|
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "banner"
|
4
3
|
require_relative "capture_code_context"
|
5
4
|
require_relative "display_code_with_line_numbers"
|
6
5
|
|
@@ -9,18 +8,13 @@ module DeadEnd
|
|
9
8
|
class DisplayInvalidBlocks
|
10
9
|
attr_reader :filename
|
11
10
|
|
12
|
-
def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: DEFAULT_VALUE
|
13
|
-
@terminal = terminal == DEFAULT_VALUE ? io.isatty : terminal
|
14
|
-
|
15
|
-
@filename = filename
|
11
|
+
def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: DEFAULT_VALUE)
|
16
12
|
@io = io
|
17
|
-
|
18
13
|
@blocks = Array(blocks)
|
19
|
-
|
20
|
-
@invalid_lines = @blocks.map(&:lines).flatten
|
14
|
+
@filename = filename
|
21
15
|
@code_lines = code_lines
|
22
16
|
|
23
|
-
@
|
17
|
+
@terminal = terminal == DEFAULT_VALUE ? io.isatty : terminal
|
24
18
|
end
|
25
19
|
|
26
20
|
def document_ok?
|
@@ -30,61 +24,58 @@ module DeadEnd
|
|
30
24
|
def call
|
31
25
|
if document_ok?
|
32
26
|
@io.puts "Syntax OK"
|
33
|
-
|
34
|
-
found_invalid_blocks
|
27
|
+
return self
|
35
28
|
end
|
36
|
-
self
|
37
|
-
end
|
38
29
|
|
39
|
-
|
40
|
-
|
41
|
-
EOM
|
42
|
-
end
|
43
|
-
|
44
|
-
private def found_invalid_blocks
|
45
|
-
@io.puts
|
46
|
-
if banner
|
47
|
-
@io.puts banner
|
30
|
+
if filename
|
31
|
+
@io.puts("--> #{filename}")
|
48
32
|
@io.puts
|
49
33
|
end
|
50
|
-
@
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
#{indent(code_block)}
|
55
|
-
EOM
|
56
|
-
end
|
57
|
-
|
58
|
-
def banner
|
59
|
-
Banner.new(invalid_obj: @invalid_obj).call
|
60
|
-
end
|
34
|
+
@blocks.each do |block|
|
35
|
+
display_block(block)
|
36
|
+
end
|
61
37
|
|
62
|
-
|
63
|
-
string.each_line.map { |l| with + l }.join
|
38
|
+
self
|
64
39
|
end
|
65
40
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
41
|
+
private def display_block(block)
|
42
|
+
# Build explanation
|
43
|
+
explain = ExplainSyntax.new(
|
44
|
+
code_lines: block.lines
|
45
|
+
).call
|
71
46
|
|
72
|
-
|
47
|
+
# Enhance code output
|
48
|
+
# Also handles several ambiguious cases
|
73
49
|
lines = CaptureCodeContext.new(
|
74
|
-
blocks:
|
50
|
+
blocks: block,
|
75
51
|
code_lines: @code_lines
|
76
52
|
).call
|
77
53
|
|
78
|
-
|
54
|
+
# Build code output
|
55
|
+
document = DisplayCodeWithLineNumbers.new(
|
79
56
|
lines: lines,
|
80
57
|
terminal: @terminal,
|
81
|
-
highlight_lines:
|
58
|
+
highlight_lines: block.lines
|
82
59
|
).call
|
60
|
+
|
61
|
+
# Output syntax error explanation
|
62
|
+
explain.errors.each do |e|
|
63
|
+
@io.puts e
|
64
|
+
end
|
65
|
+
@io.puts
|
66
|
+
|
67
|
+
# Output code
|
68
|
+
@io.puts(document)
|
83
69
|
end
|
84
70
|
|
85
|
-
def
|
71
|
+
private def code_with_context
|
72
|
+
lines = CaptureCodeContext.new(
|
73
|
+
blocks: @blocks,
|
74
|
+
code_lines: @code_lines
|
75
|
+
).call
|
76
|
+
|
86
77
|
DisplayCodeWithLineNumbers.new(
|
87
|
-
lines:
|
78
|
+
lines: lines,
|
88
79
|
terminal: @terminal,
|
89
80
|
highlight_lines: @invalid_lines
|
90
81
|
).call
|
@@ -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,168 @@
|
|
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
|
+
# ignore it.
|
65
|
+
when :on_words_beg, :on_symbos_beg, :on_qwords_beg,
|
66
|
+
:on_qsymbols_beg, :on_regexp_beg, :on_tstring_beg
|
67
|
+
# ^^^
|
68
|
+
# Handle shorthand syntaxes like `%Q{ i am a string }`
|
69
|
+
#
|
70
|
+
# The start token will be the full thing `%Q{` but we
|
71
|
+
# need to count it as if it's a `{`. Any token
|
72
|
+
# can be used
|
73
|
+
char = lex.token[-1]
|
74
|
+
@count_for_char[char] += 1 if @count_for_char.key?(char)
|
75
|
+
when :on_embexpr_beg
|
76
|
+
# ^^^
|
77
|
+
# Embedded string expressions like `"#{foo} <-embed"`
|
78
|
+
# are parsed with chars:
|
79
|
+
#
|
80
|
+
# `#{` as :on_embexpr_beg
|
81
|
+
# `}` as :on_embexpr_end
|
82
|
+
#
|
83
|
+
# We cannot ignore both :on_emb_expr_beg and :on_embexpr_end
|
84
|
+
# because sometimes the lexer thinks something is an embed
|
85
|
+
# string end, when it is not like `lol = }` (no clue why).
|
86
|
+
#
|
87
|
+
# When we see `#{` count it as a `{` or we will
|
88
|
+
# have a mis-match count.
|
89
|
+
#
|
90
|
+
case lex.token
|
91
|
+
when "\#{"
|
92
|
+
@count_for_char["{"] += 1
|
93
|
+
end
|
94
|
+
else
|
95
|
+
@end_count += 1 if lex.is_end?
|
96
|
+
@kw_count += 1 if lex.is_kw?
|
97
|
+
@count_for_char[lex.token] += 1 if @count_for_char.key?(lex.token)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def count_for_char(char)
|
102
|
+
@count_for_char[char]
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns an array of missing syntax characters
|
106
|
+
# or `"end"` or `"keyword"`
|
107
|
+
#
|
108
|
+
# left_right.missing
|
109
|
+
# # => ["}"]
|
110
|
+
def missing
|
111
|
+
out = missing_pairs
|
112
|
+
out << missing_pipe
|
113
|
+
out << missing_keyword_end
|
114
|
+
out.compact!
|
115
|
+
out
|
116
|
+
end
|
117
|
+
|
118
|
+
PAIRS = {
|
119
|
+
"{" => "}",
|
120
|
+
"[" => "]",
|
121
|
+
"(" => ")"
|
122
|
+
}.freeze
|
123
|
+
|
124
|
+
# Opening characters like `{` need closing characters # like `}`.
|
125
|
+
#
|
126
|
+
# When a mis-match count is detected, suggest the
|
127
|
+
# missing member.
|
128
|
+
#
|
129
|
+
# For example if there are 3 `}` and only two `{`
|
130
|
+
# return `"{"`
|
131
|
+
private def missing_pairs
|
132
|
+
PAIRS.map do |(left, right)|
|
133
|
+
case @count_for_char[left] <=> @count_for_char[right]
|
134
|
+
when 1
|
135
|
+
right
|
136
|
+
when 0
|
137
|
+
nil
|
138
|
+
when -1
|
139
|
+
left
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Keywords need ends and ends need keywords
|
145
|
+
#
|
146
|
+
# If we have more keywords, there's a missing `end`
|
147
|
+
# if we have more `end`-s, there's a missing keyword
|
148
|
+
private def missing_keyword_end
|
149
|
+
case @kw_count <=> @end_count
|
150
|
+
when 1
|
151
|
+
"end"
|
152
|
+
when 0
|
153
|
+
nil
|
154
|
+
when -1
|
155
|
+
"keyword"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Pipes come in pairs.
|
160
|
+
# If there's an odd number of pipes then we
|
161
|
+
# are missing one
|
162
|
+
private def missing_pipe
|
163
|
+
if @count_for_char["|"].odd?
|
164
|
+
"|"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|