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.
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