dead_end 1.1.7 → 3.1.1
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 +27 -1
- data/.github/workflows/check_changelog.yml +14 -7
- data/.standard.yml +1 -0
- data/CHANGELOG.md +60 -0
- data/CODE_OF_CONDUCT.md +2 -2
- data/Gemfile +2 -0
- data/Gemfile.lock +31 -2
- data/README.md +122 -35
- data/Rakefile +1 -1
- data/dead_end.gemspec +12 -12
- data/exe/dead_end +4 -67
- data/lib/dead_end/{internals.rb → api.rb} +90 -52
- data/lib/dead_end/around_block_scan.rb +16 -18
- data/lib/dead_end/auto.rb +3 -101
- 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 +304 -0
- data/lib/dead_end/cli.rb +129 -0
- data/lib/dead_end/code_block.rb +20 -4
- data/lib/dead_end/code_frontier.rb +74 -29
- data/lib/dead_end/code_line.rb +176 -87
- data/lib/dead_end/code_search.rb +40 -51
- data/lib/dead_end/core_ext.rb +35 -0
- data/lib/dead_end/display_code_with_line_numbers.rb +7 -8
- data/lib/dead_end/display_invalid_blocks.rb +42 -80
- 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 +25 -34
- data/lib/dead_end/lex_value.rb +70 -0
- data/lib/dead_end/parse_blocks_from_indent_line.rb +3 -4
- 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 +2 -2
- metadata +14 -9
- data/.travis.yml +0 -6
- data/lib/dead_end/fyi.rb +0 -7
- data/lib/dead_end/heredoc_block_parse.rb +0 -30
- data/lib/dead_end/trailing_slash_join.rb +0 -53
- data/lib/dead_end/who_dis_syntax_error.rb +0 -69
@@ -1,14 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DeadEnd
|
4
|
-
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
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.
|
8
13
|
#
|
9
|
-
#
|
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.
|
10
19
|
#
|
11
|
-
#
|
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:
|
12
25
|
#
|
13
26
|
# puts block.to_s # => "def bark"
|
14
27
|
#
|
@@ -17,7 +30,8 @@ module DeadEnd
|
|
17
30
|
# code_lines: code_lines
|
18
31
|
# )
|
19
32
|
#
|
20
|
-
#
|
33
|
+
# lines = context.call.map(&:original)
|
34
|
+
# puts lines.join
|
21
35
|
# # =>
|
22
36
|
# class Dog
|
23
37
|
# def bark
|
@@ -26,7 +40,7 @@ module DeadEnd
|
|
26
40
|
class CaptureCodeContext
|
27
41
|
attr_reader :code_lines
|
28
42
|
|
29
|
-
def initialize(blocks
|
43
|
+
def initialize(blocks:, code_lines:)
|
30
44
|
@blocks = Array(blocks)
|
31
45
|
@code_lines = code_lines
|
32
46
|
@visible_lines = @blocks.map(&:visible_lines).flatten
|
@@ -35,29 +49,73 @@ module DeadEnd
|
|
35
49
|
|
36
50
|
def call
|
37
51
|
@blocks.each do |block|
|
52
|
+
capture_first_kw_end_same_indent(block)
|
38
53
|
capture_last_end_same_indent(block)
|
39
54
|
capture_before_after_kws(block)
|
40
55
|
capture_falling_indent(block)
|
41
56
|
end
|
42
57
|
|
43
58
|
@lines_to_output.select!(&:not_empty?)
|
44
|
-
@lines_to_output.select!(&:not_comment?)
|
45
59
|
@lines_to_output.uniq!
|
46
60
|
@lines_to_output.sort!
|
47
61
|
|
48
|
-
|
62
|
+
@lines_to_output
|
49
63
|
end
|
50
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
|
+
#
|
51
80
|
def capture_falling_indent(block)
|
52
81
|
AroundBlockScan.new(
|
53
82
|
block: block,
|
54
|
-
code_lines: @code_lines
|
83
|
+
code_lines: @code_lines
|
55
84
|
).on_falling_indent do |line|
|
56
85
|
@lines_to_output << line
|
57
86
|
end
|
58
87
|
end
|
59
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
|
+
#
|
60
116
|
def capture_before_after_kws(block)
|
117
|
+
return unless block.visible_lines.count == 1
|
118
|
+
|
61
119
|
around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
62
120
|
.start_at_next_line
|
63
121
|
.capture_neighbor_context
|
@@ -67,50 +125,109 @@ module DeadEnd
|
|
67
125
|
@lines_to_output.concat(around_lines)
|
68
126
|
end
|
69
127
|
|
70
|
-
#
|
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/dead_end/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
|
71
149
|
def capture_last_end_same_indent(block)
|
72
|
-
|
73
|
-
|
74
|
-
|
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]
|
75
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
|
76
164
|
|
77
|
-
#
|
165
|
+
# Work backwards from the end to
|
166
|
+
# see if there are mis-matched
|
167
|
+
# keyword/end pairs
|
78
168
|
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
kw_count = 0
|
93
|
-
end_count = 0
|
94
|
-
lines = lines.reverse.take_while do |line|
|
95
|
-
next false if stop_next
|
96
|
-
|
97
|
-
end_count += 1 if line.is_end?
|
98
|
-
kw_count += 1 if line.is_kw?
|
99
|
-
|
100
|
-
stop_next = true if !kw_count.zero? && kw_count >= end_count
|
101
|
-
true
|
102
|
-
end.reverse
|
103
|
-
|
104
|
-
next unless kw_count > end_count
|
105
|
-
|
106
|
-
lines = lines.select {|line| line.is_kw? || line.is_end? }
|
107
|
-
|
108
|
-
next if lines.empty?
|
109
|
-
|
110
|
-
@lines_to_output << end_line
|
111
|
-
@lines_to_output << lines.first
|
112
|
-
@lines_to_output << lines.last
|
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
|
113
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
|
114
231
|
end
|
115
232
|
end
|
116
233
|
end
|
@@ -0,0 +1,304 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Parses and sanitizes source into a lexically aware document
|
5
|
+
#
|
6
|
+
# Internally the document is represented by an array with each
|
7
|
+
# index containing a CodeLine correlating to a line from the source code.
|
8
|
+
#
|
9
|
+
# There are three main phases in the algorithm:
|
10
|
+
#
|
11
|
+
# 1. Sanitize/format input source
|
12
|
+
# 2. Search for invalid blocks
|
13
|
+
# 3. Format invalid blocks into something meaninful
|
14
|
+
#
|
15
|
+
# This class handles the first part.
|
16
|
+
#
|
17
|
+
# The reason this class exists is to format input source
|
18
|
+
# for better/easier/cleaner exploration.
|
19
|
+
#
|
20
|
+
# The CodeSearch class operates at the line level so
|
21
|
+
# we must be careful to not introduce lines that look
|
22
|
+
# valid by themselves, but when removed will trigger syntax errors
|
23
|
+
# or strange behavior.
|
24
|
+
#
|
25
|
+
# ## Join Trailing slashes
|
26
|
+
#
|
27
|
+
# Code with a trailing slash is logically treated as a single line:
|
28
|
+
#
|
29
|
+
# 1 it "code can be split" \
|
30
|
+
# 2 "across multiple lines" do
|
31
|
+
#
|
32
|
+
# In this case removing line 2 would add a syntax error. We get around
|
33
|
+
# this by internally joining the two lines into a single "line" object
|
34
|
+
#
|
35
|
+
# ## Logically Consecutive lines
|
36
|
+
#
|
37
|
+
# Code that can be broken over multiple
|
38
|
+
# lines such as method calls are on different lines:
|
39
|
+
#
|
40
|
+
# 1 User.
|
41
|
+
# 2 where(name: "schneems").
|
42
|
+
# 3 first
|
43
|
+
#
|
44
|
+
# Removing line 2 can introduce a syntax error. To fix this, all lines
|
45
|
+
# are joined into one.
|
46
|
+
#
|
47
|
+
# ## Heredocs
|
48
|
+
#
|
49
|
+
# A heredoc is an way of defining a multi-line string. They can cause many
|
50
|
+
# problems. If left as a single line, Ripper would try to parse the contents
|
51
|
+
# as ruby code rather than as a string. Even without this problem, we still
|
52
|
+
# hit an issue with indentation
|
53
|
+
#
|
54
|
+
# 1 foo = <<~HEREDOC
|
55
|
+
# 2 "Be yourself; everyone else is already taken.""
|
56
|
+
# 3 ― Oscar Wilde
|
57
|
+
# 4 puts "I look like ruby code" # but i'm still a heredoc
|
58
|
+
# 5 HEREDOC
|
59
|
+
#
|
60
|
+
# If we didn't join these lines then our algorithm would think that line 4
|
61
|
+
# is separate from the rest, has a higher indentation, then look at it first
|
62
|
+
# and remove it.
|
63
|
+
#
|
64
|
+
# If the code evaluates line 5 by itself it will think line 5 is a constant,
|
65
|
+
# remove it, and introduce a syntax errror.
|
66
|
+
#
|
67
|
+
# All of these problems are fixed by joining the whole heredoc into a single
|
68
|
+
# line.
|
69
|
+
#
|
70
|
+
# ## Comments and whitespace
|
71
|
+
#
|
72
|
+
# Comments can throw off the way the lexer tells us that the line
|
73
|
+
# logically belongs with the next line. This is valid ruby but
|
74
|
+
# results in a different lex output than before:
|
75
|
+
#
|
76
|
+
# 1 User.
|
77
|
+
# 2 where(name: "schneems").
|
78
|
+
# 3 # Comment here
|
79
|
+
# 4 first
|
80
|
+
#
|
81
|
+
# To handle this we can replace comment lines with empty lines
|
82
|
+
# and then re-lex the source. This removal and re-lexing preserves
|
83
|
+
# line index and document size, but generates an easier to work with
|
84
|
+
# document.
|
85
|
+
#
|
86
|
+
class CleanDocument
|
87
|
+
def initialize(source:)
|
88
|
+
lines = clean_sweep(source: source)
|
89
|
+
@document = CodeLine.from_source(lines.join, lines: lines)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Call all of the document "cleaners"
|
93
|
+
# and return self
|
94
|
+
def call
|
95
|
+
join_trailing_slash!
|
96
|
+
join_consecutive!
|
97
|
+
join_heredoc!
|
98
|
+
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return an array of CodeLines in the
|
103
|
+
# document
|
104
|
+
def lines
|
105
|
+
@document
|
106
|
+
end
|
107
|
+
|
108
|
+
# Renders the document back to a string
|
109
|
+
def to_s
|
110
|
+
@document.join
|
111
|
+
end
|
112
|
+
|
113
|
+
# Remove comments and whitespace only lines
|
114
|
+
#
|
115
|
+
# replace with empty newlines
|
116
|
+
#
|
117
|
+
# source = <<~'EOM'
|
118
|
+
# # Comment 1
|
119
|
+
# puts "hello"
|
120
|
+
# # Comment 2
|
121
|
+
# puts "world"
|
122
|
+
# EOM
|
123
|
+
#
|
124
|
+
# lines = CleanDocument.new(source: source).lines
|
125
|
+
# expect(lines[0].to_s).to eq("\n")
|
126
|
+
# expect(lines[1].to_s).to eq("puts "hello")
|
127
|
+
# expect(lines[2].to_s).to eq("\n")
|
128
|
+
# expect(lines[3].to_s).to eq("puts "world")
|
129
|
+
#
|
130
|
+
# Important: This must be done before lexing.
|
131
|
+
#
|
132
|
+
# After this change is made, we lex the document because
|
133
|
+
# removing comments can change how the doc is parsed.
|
134
|
+
#
|
135
|
+
# For example:
|
136
|
+
#
|
137
|
+
# values = LexAll.new(source: <<~EOM))
|
138
|
+
# User.
|
139
|
+
# # comment
|
140
|
+
# where(name: 'schneems')
|
141
|
+
# EOM
|
142
|
+
# expect(
|
143
|
+
# values.count {|v| v.type == :on_ignored_nl}
|
144
|
+
# ).to eq(1)
|
145
|
+
#
|
146
|
+
# After the comment is removed:
|
147
|
+
#
|
148
|
+
# values = LexAll.new(source: <<~EOM))
|
149
|
+
# User.
|
150
|
+
#
|
151
|
+
# where(name: 'schneems')
|
152
|
+
# EOM
|
153
|
+
# expect(
|
154
|
+
# values.count {|v| v.type == :on_ignored_nl}
|
155
|
+
# ).to eq(2)
|
156
|
+
#
|
157
|
+
def clean_sweep(source:)
|
158
|
+
source.lines.map do |line|
|
159
|
+
if line.match?(/^\s*(#[^{].*)?$/) # https://rubular.com/r/LLE10D8HKMkJvs
|
160
|
+
$/
|
161
|
+
else
|
162
|
+
line
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Smushes all heredoc lines into one line
|
168
|
+
#
|
169
|
+
# source = <<~'EOM'
|
170
|
+
# foo = <<~HEREDOC
|
171
|
+
# lol
|
172
|
+
# hehehe
|
173
|
+
# HEREDOC
|
174
|
+
# EOM
|
175
|
+
#
|
176
|
+
# lines = CleanDocument.new(source: source).join_heredoc!.lines
|
177
|
+
# expect(lines[0].to_s).to eq(source)
|
178
|
+
# expect(lines[1].to_s).to eq("")
|
179
|
+
def join_heredoc!
|
180
|
+
start_index_stack = []
|
181
|
+
heredoc_beg_end_index = []
|
182
|
+
lines.each do |line|
|
183
|
+
line.lex.each do |lex_value|
|
184
|
+
case lex_value.type
|
185
|
+
when :on_heredoc_beg
|
186
|
+
start_index_stack << line.index
|
187
|
+
when :on_heredoc_end
|
188
|
+
start_index = start_index_stack.pop
|
189
|
+
end_index = line.index
|
190
|
+
heredoc_beg_end_index << [start_index, end_index]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
heredoc_groups = heredoc_beg_end_index.map { |start_index, end_index| @document[start_index..end_index] }
|
196
|
+
|
197
|
+
join_groups(heredoc_groups)
|
198
|
+
self
|
199
|
+
end
|
200
|
+
|
201
|
+
# Smushes logically "consecutive" lines
|
202
|
+
#
|
203
|
+
# source = <<~'EOM'
|
204
|
+
# User.
|
205
|
+
# where(name: 'schneems').
|
206
|
+
# first
|
207
|
+
# EOM
|
208
|
+
#
|
209
|
+
# lines = CleanDocument.new(source: source).join_consecutive!.lines
|
210
|
+
# expect(lines[0].to_s).to eq(source)
|
211
|
+
# expect(lines[1].to_s).to eq("")
|
212
|
+
#
|
213
|
+
# The one known case this doesn't handle is:
|
214
|
+
#
|
215
|
+
# Ripper.lex <<~EOM
|
216
|
+
# a &&
|
217
|
+
# b ||
|
218
|
+
# c
|
219
|
+
# EOM
|
220
|
+
#
|
221
|
+
# For some reason this introduces `on_ignore_newline` but with BEG type
|
222
|
+
#
|
223
|
+
def join_consecutive!
|
224
|
+
consecutive_groups = @document.select(&:ignore_newline_not_beg?).map do |code_line|
|
225
|
+
take_while_including(code_line.index..-1) do |line|
|
226
|
+
line.ignore_newline_not_beg?
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
join_groups(consecutive_groups)
|
231
|
+
self
|
232
|
+
end
|
233
|
+
|
234
|
+
# Join lines with a trailing slash
|
235
|
+
#
|
236
|
+
# source = <<~'EOM'
|
237
|
+
# it "code can be split" \
|
238
|
+
# "across multiple lines" do
|
239
|
+
# EOM
|
240
|
+
#
|
241
|
+
# lines = CleanDocument.new(source: source).join_consecutive!.lines
|
242
|
+
# expect(lines[0].to_s).to eq(source)
|
243
|
+
# expect(lines[1].to_s).to eq("")
|
244
|
+
def join_trailing_slash!
|
245
|
+
trailing_groups = @document.select(&:trailing_slash?).map do |code_line|
|
246
|
+
take_while_including(code_line.index..-1) { |x| x.trailing_slash? }
|
247
|
+
end
|
248
|
+
join_groups(trailing_groups)
|
249
|
+
self
|
250
|
+
end
|
251
|
+
|
252
|
+
# Helper method for joining "groups" of lines
|
253
|
+
#
|
254
|
+
# Input is expected to be type Array<Array<CodeLine>>
|
255
|
+
#
|
256
|
+
# The outer array holds the various "groups" while the
|
257
|
+
# inner array holds code lines.
|
258
|
+
#
|
259
|
+
# All code lines are "joined" into the first line in
|
260
|
+
# their group.
|
261
|
+
#
|
262
|
+
# To preserve document size, empty lines are placed
|
263
|
+
# in the place of the lines that were "joined"
|
264
|
+
def join_groups(groups)
|
265
|
+
groups.each do |lines|
|
266
|
+
line = lines.first
|
267
|
+
|
268
|
+
# Handle the case of multiple groups in a a row
|
269
|
+
# if one is already replaced, move on
|
270
|
+
next if @document[line.index].empty?
|
271
|
+
|
272
|
+
# Join group into the first line
|
273
|
+
@document[line.index] = CodeLine.new(
|
274
|
+
lex: lines.map(&:lex).flatten,
|
275
|
+
line: lines.join,
|
276
|
+
index: line.index
|
277
|
+
)
|
278
|
+
|
279
|
+
# Hide the rest of the lines
|
280
|
+
lines[1..-1].each do |line|
|
281
|
+
# The above lines already have newlines in them, if add more
|
282
|
+
# then there will be double newline, use an empty line instead
|
283
|
+
@document[line.index] = CodeLine.new(line: "", index: line.index, lex: [])
|
284
|
+
end
|
285
|
+
end
|
286
|
+
self
|
287
|
+
end
|
288
|
+
|
289
|
+
# Helper method for grabbing elements from document
|
290
|
+
#
|
291
|
+
# Like `take_while` except when it stops
|
292
|
+
# iterating, it also returns the line
|
293
|
+
# that caused it to stop
|
294
|
+
def take_while_including(range = 0..-1)
|
295
|
+
take_next_and_stop = false
|
296
|
+
@document[range].take_while do |line|
|
297
|
+
next if take_next_and_stop
|
298
|
+
|
299
|
+
take_next_and_stop = !(yield line)
|
300
|
+
true
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
data/lib/dead_end/cli.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "optparse"
|
5
|
+
|
6
|
+
module DeadEnd
|
7
|
+
# All the logic of the exe/dead_end CLI in one handy spot
|
8
|
+
#
|
9
|
+
# Cli.new(argv: ["--help"]).call
|
10
|
+
# Cli.new(argv: ["<path/to/file>.rb"]).call
|
11
|
+
# Cli.new(argv: ["<path/to/file>.rb", "--record=tmp"]).call
|
12
|
+
# Cli.new(argv: ["<path/to/file>.rb", "--terminal"]).call
|
13
|
+
#
|
14
|
+
class Cli
|
15
|
+
attr_accessor :options
|
16
|
+
|
17
|
+
# ARGV is Everything passed to the executable, does not include executable name
|
18
|
+
#
|
19
|
+
# All other intputs are dependency injection for testing
|
20
|
+
def initialize(argv:, exit_obj: Kernel, io: $stdout, env: ENV)
|
21
|
+
@options = {}
|
22
|
+
@parser = nil
|
23
|
+
options[:record_dir] = env["DEAD_END_RECORD_DIR"]
|
24
|
+
options[:record_dir] = "tmp" if env["DEBUG"]
|
25
|
+
options[:terminal] = DeadEnd::DEFAULT_VALUE
|
26
|
+
|
27
|
+
@io = io
|
28
|
+
@argv = argv
|
29
|
+
@exit_obj = exit_obj
|
30
|
+
end
|
31
|
+
|
32
|
+
def call
|
33
|
+
if @argv.empty?
|
34
|
+
# Display help if raw command
|
35
|
+
parser.parse! %w[--help]
|
36
|
+
return
|
37
|
+
else
|
38
|
+
# Mutates @argv
|
39
|
+
parse
|
40
|
+
return if options[:exit]
|
41
|
+
end
|
42
|
+
|
43
|
+
file_name = @argv.first
|
44
|
+
if file_name.nil?
|
45
|
+
@io.puts "No file given"
|
46
|
+
@exit_obj.exit(1)
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
file = Pathname(file_name)
|
51
|
+
if !file.exist?
|
52
|
+
@io.puts "file not found: #{file.expand_path} "
|
53
|
+
@exit_obj.exit(1)
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
@io.puts "Record dir: #{options[:record_dir]}" if options[:record_dir]
|
58
|
+
|
59
|
+
display = DeadEnd.call(
|
60
|
+
io: @io,
|
61
|
+
source: file.read,
|
62
|
+
filename: file.expand_path,
|
63
|
+
terminal: options.fetch(:terminal, DeadEnd::DEFAULT_VALUE),
|
64
|
+
record_dir: options[:record_dir]
|
65
|
+
)
|
66
|
+
|
67
|
+
if display.document_ok?
|
68
|
+
@exit_obj.exit(0)
|
69
|
+
else
|
70
|
+
@exit_obj.exit(1)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse
|
75
|
+
parser.parse!(@argv)
|
76
|
+
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def parser
|
81
|
+
@parser ||= OptionParser.new do |opts|
|
82
|
+
opts.banner = <<~EOM
|
83
|
+
Usage: dead_end <file> [options]
|
84
|
+
|
85
|
+
Parses a ruby source file and searches for syntax error(s) such as
|
86
|
+
unexpected `end', expecting end-of-input.
|
87
|
+
|
88
|
+
Example:
|
89
|
+
|
90
|
+
$ dead_end dog.rb
|
91
|
+
|
92
|
+
# ...
|
93
|
+
|
94
|
+
❯ 10 defdog
|
95
|
+
❯ 15 end
|
96
|
+
|
97
|
+
ENV options:
|
98
|
+
|
99
|
+
DEAD_END_RECORD_DIR=<dir>
|
100
|
+
|
101
|
+
Records the steps used to search for a syntax error
|
102
|
+
to the given directory
|
103
|
+
|
104
|
+
Options:
|
105
|
+
EOM
|
106
|
+
|
107
|
+
opts.version = DeadEnd::VERSION
|
108
|
+
|
109
|
+
opts.on("--help", "Help - displays this message") do |v|
|
110
|
+
@io.puts opts
|
111
|
+
options[:exit] = true
|
112
|
+
@exit_obj.exit
|
113
|
+
end
|
114
|
+
|
115
|
+
opts.on("--record <dir>", "Records the steps used to search for a syntax error to the given directory") do |v|
|
116
|
+
options[:record_dir] = v
|
117
|
+
end
|
118
|
+
|
119
|
+
opts.on("--terminal", "Enable terminal highlighting") do |v|
|
120
|
+
options[:terminal] = true
|
121
|
+
end
|
122
|
+
|
123
|
+
opts.on("--no-terminal", "Disable terminal highlighting") do |v|
|
124
|
+
options[:terminal] = false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|