dead_end 1.1.7 → 3.1.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +27 -1
  3. data/.github/workflows/check_changelog.yml +14 -7
  4. data/.standard.yml +1 -0
  5. data/CHANGELOG.md +60 -0
  6. data/CODE_OF_CONDUCT.md +2 -2
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +31 -2
  9. data/README.md +122 -35
  10. data/Rakefile +1 -1
  11. data/dead_end.gemspec +12 -12
  12. data/exe/dead_end +4 -67
  13. data/lib/dead_end/{internals.rb → api.rb} +90 -52
  14. data/lib/dead_end/around_block_scan.rb +16 -18
  15. data/lib/dead_end/auto.rb +3 -101
  16. data/lib/dead_end/block_expand.rb +6 -5
  17. data/lib/dead_end/capture_code_context.rb +167 -50
  18. data/lib/dead_end/clean_document.rb +304 -0
  19. data/lib/dead_end/cli.rb +129 -0
  20. data/lib/dead_end/code_block.rb +20 -4
  21. data/lib/dead_end/code_frontier.rb +74 -29
  22. data/lib/dead_end/code_line.rb +176 -87
  23. data/lib/dead_end/code_search.rb +40 -51
  24. data/lib/dead_end/core_ext.rb +35 -0
  25. data/lib/dead_end/display_code_with_line_numbers.rb +7 -8
  26. data/lib/dead_end/display_invalid_blocks.rb +42 -80
  27. data/lib/dead_end/explain_syntax.rb +103 -0
  28. data/lib/dead_end/insertion_sort.rb +46 -0
  29. data/lib/dead_end/left_right_lex_count.rb +168 -0
  30. data/lib/dead_end/lex_all.rb +25 -34
  31. data/lib/dead_end/lex_value.rb +70 -0
  32. data/lib/dead_end/parse_blocks_from_indent_line.rb +3 -4
  33. data/lib/dead_end/pathname_from_message.rb +47 -0
  34. data/lib/dead_end/ripper_errors.rb +36 -0
  35. data/lib/dead_end/version.rb +1 -1
  36. data/lib/dead_end.rb +2 -2
  37. metadata +14 -9
  38. data/.travis.yml +0 -6
  39. data/lib/dead_end/fyi.rb +0 -7
  40. data/lib/dead_end/heredoc_block_parse.rb +0 -30
  41. data/lib/dead_end/trailing_slash_join.rb +0 -53
  42. 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
- # Given a block, this method will capture surrounding
6
- # code to give the user more context for the location of
7
- # the problem.
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
- # Return is an array of CodeLines to be rendered.
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
- # Surrounding code is captured regardless of visible state
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
- # puts context.call.join
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: , code_lines:)
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
- return @lines_to_output
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
- # Problems heredocs are back in play
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
- start_index = block.visible_lines.first.index
73
- lines = @code_lines[start_index..block.lines.last.index]
74
- kw_end_lines = lines.select {|line| line.indent == block.current_indent && (line.is_end? || line.is_kw?) }
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
- # TODO handle case of heredocs showing up here
165
+ # Work backwards from the end to
166
+ # see if there are mis-matched
167
+ # keyword/end pairs
78
168
  #
79
- # Due to https://github.com/zombocom/dead_end/issues/32
80
- # There's a special case where a keyword right before the last
81
- # end of a valid block accidentally ends up identifying that the problem
82
- # was with the block instead of before it. To handle that
83
- # special case, we can re-parse back through the internals of blocks
84
- # and if they have mis-matched keywords and ends show the last one
85
- end_lines = kw_end_lines.select(&:is_end?)
86
- end_lines.each_with_index do |end_line, i|
87
- start_index = i.zero? ? 0 : end_lines[i-1].index
88
- end_index = end_line.index - 1
89
- lines = @code_lines[start_index..end_index]
90
-
91
- stop_next = false
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
@@ -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