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,44 +1,67 @@
1
- # frozen_string_literal: true
2
- #
3
- # This is the top level file, but is moved to `internals`
4
- # so the top level file can instead enable the "automatic" behavior
5
-
6
1
  require_relative "version"
7
2
 
8
- require 'tmpdir'
9
- require 'stringio'
10
- require 'pathname'
11
- require 'ripper'
12
- require 'timeout'
3
+ require "tmpdir"
4
+ require "stringio"
5
+ require "pathname"
6
+ require "ripper"
7
+ require "timeout"
13
8
 
14
9
  module DeadEnd
10
+ # Used to indicate a default value that cannot
11
+ # be confused with another input.
12
+ DEFAULT_VALUE = Object.new.freeze
13
+
15
14
  class Error < StandardError; end
16
- SEARCH_SOURCE_ON_ERROR_DEFAULT = true
17
- TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 5).to_i
15
+ TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
18
16
 
19
- def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT)
20
- raise e if !e.message.include?("end-of-input")
17
+ # DeadEnd.handle_error [Public]
18
+ #
19
+ # Takes a `SyntaxError`` exception, uses the
20
+ # error message to locate the file. Then the file
21
+ # will be analyzed to find the location of the syntax
22
+ # error and emit that location to stderr.
23
+ #
24
+ # Example:
25
+ #
26
+ # begin
27
+ # require 'bad_file'
28
+ # rescue => e
29
+ # DeadEnd.handle_error(e)
30
+ # end
31
+ #
32
+ # By default it will re-raise the exception unless
33
+ # `re_raise: false`. The message output location
34
+ # can be configured using the `io: $stderr` input.
35
+ #
36
+ # If a valid filename cannot be determined, the original
37
+ # exception will be re-raised (even with
38
+ # `re_raise: false`).
39
+ def self.handle_error(e, re_raise: true, io: $stderr)
40
+ unless e.is_a?(SyntaxError)
41
+ io.puts("DeadEnd: Must pass a SyntaxError, got: #{e.class}")
42
+ raise e
43
+ end
21
44
 
22
- filename = e.message.split(":").first
45
+ file = PathnameFromMessage.new(e.message, io: io).call.name
46
+ raise e unless file
23
47
 
24
- $stderr.sync = true
25
- $stderr.puts "Run `$ dead_end #{filename}` for more options\n"
48
+ io.sync = true
26
49
 
27
- if search_source_on_error
28
- self.call(
29
- source: Pathname(filename).read,
30
- filename: filename,
31
- terminal: true,
32
- )
33
- end
50
+ call(
51
+ io: io,
52
+ source: file.read,
53
+ filename: file
54
+ )
34
55
 
35
- $stderr.puts ""
36
- $stderr.puts ""
37
- raise e
56
+ raise e if re_raise
38
57
  end
39
58
 
40
- def self.call(source: , filename: , terminal: false, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
59
+ # DeadEnd.call [Private]
60
+ #
61
+ # Main private interface
62
+ def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
41
63
  search = nil
64
+ filename = nil if filename == DEFAULT_VALUE
42
65
  Timeout.timeout(timeout) do
43
66
  record_dir ||= ENV["DEBUG"] ? "tmp" : nil
44
67
  search = CodeSearch.new(source, record_dir: record_dir).call
@@ -46,25 +69,33 @@ module DeadEnd
46
69
 
47
70
  blocks = search.invalid_blocks
48
71
  DisplayInvalidBlocks.new(
72
+ io: io,
49
73
  blocks: blocks,
50
74
  filename: filename,
51
75
  terminal: terminal,
52
- code_lines: search.code_lines,
53
- invalid_obj: invalid_type(source),
54
- io: io
76
+ code_lines: search.code_lines
55
77
  ).call
56
78
  rescue Timeout::Error => e
57
79
  io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
58
80
  io.puts e.backtrace.first(3).join($/)
59
81
  end
60
82
 
61
- # Used for counting spaces
62
- module SpaceCount
63
- def self.indent(string)
64
- string.split(/\S/).first&.length || 0
65
- end
83
+ # DeadEnd.record_dir [Private]
84
+ #
85
+ # Used to generate a unique directory to record
86
+ # search steps for debugging
87
+ def self.record_dir(dir)
88
+ time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
89
+ dir = Pathname(dir)
90
+ symlink = dir.join("last").tap { |path| path.delete if path.exist? }
91
+ dir.join(time).tap { |path|
92
+ path.mkpath
93
+ FileUtils.symlink(path.basename, symlink)
94
+ }
66
95
  end
67
96
 
97
+ # DeadEnd.valid_without? [Private]
98
+ #
68
99
  # This will tell you if the `code_lines` would be valid
69
100
  # if you removed the `without_lines`. In short it's a
70
101
  # way to detect if we've found the lines with syntax errors
@@ -82,16 +113,19 @@ module DeadEnd
82
113
  # ) # => true
83
114
  #
84
115
  # DeadEnd.valid?(code_lines) # => false
85
- def self.valid_without?(without_lines: , code_lines:)
116
+ def self.valid_without?(without_lines:, code_lines:)
86
117
  lines = code_lines - Array(without_lines).flatten
87
118
 
88
119
  if lines.empty?
89
- return true
120
+ true
90
121
  else
91
- return valid?(lines)
122
+ valid?(lines)
92
123
  end
93
124
  end
94
125
 
126
+ # DeadEnd.invalid? [Private]
127
+ #
128
+ # Opposite of `DeadEnd.valid?`
95
129
  def self.invalid?(source)
96
130
  source = source.join if source.is_a?(Array)
97
131
  source = source.to_s
@@ -99,6 +133,8 @@ module DeadEnd
99
133
  Ripper.new(source).tap(&:parse).error?
100
134
  end
101
135
 
136
+ # DeadEnd.valid? [Private]
137
+ #
102
138
  # Returns truthy if a given input source is valid syntax
103
139
  #
104
140
  # DeadEnd.valid?(<<~EOM) # => true
@@ -136,23 +172,25 @@ module DeadEnd
136
172
  def self.valid?(source)
137
173
  !invalid?(source)
138
174
  end
175
+ end
139
176
 
177
+ # Integration
178
+ require_relative "cli"
140
179
 
141
- def self.invalid_type(source)
142
- WhoDisSyntaxError.new(source).call
143
- end
144
- end
180
+ # Core logic
181
+ require_relative "code_search"
182
+ require_relative "code_frontier"
183
+ require_relative "explain_syntax"
184
+ require_relative "clean_document"
145
185
 
186
+ # Helpers
187
+ require_relative "lex_all"
146
188
  require_relative "code_line"
147
189
  require_relative "code_block"
148
- require_relative "code_frontier"
149
- require_relative "display_invalid_blocks"
150
- require_relative "around_block_scan"
151
190
  require_relative "block_expand"
191
+ require_relative "ripper_errors"
192
+ require_relative "insertion_sort"
193
+ require_relative "around_block_scan"
194
+ require_relative "pathname_from_message"
195
+ require_relative "display_invalid_blocks"
152
196
  require_relative "parse_blocks_from_indent_line"
153
-
154
- require_relative "code_search"
155
- require_relative "who_dis_syntax_error"
156
- require_relative "heredoc_block_parse"
157
- require_relative "lex_all"
158
- require_relative "trailing_slash_join"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- #
2
+
3
3
  module DeadEnd
4
4
  # This class is useful for exploring contents before and after
5
5
  # a block
@@ -9,10 +9,10 @@ module DeadEnd
9
9
  #
10
10
  # Example:
11
11
  #
12
- # def dog
13
- # puts "bark"
14
- # puts "bark"
15
- # end
12
+ # def dog # 1
13
+ # puts "bark" # 2
14
+ # puts "bark" # 3
15
+ # end # 4
16
16
  #
17
17
  # scan = AroundBlockScan.new(
18
18
  # code_lines: code_lines
@@ -22,13 +22,13 @@ module DeadEnd
22
22
  # scan.scan_while { true }
23
23
  #
24
24
  # puts scan.before_index # => 0
25
- # puts scan.after_index # => 3
25
+ # puts scan.after_index # => 3
26
26
  #
27
27
  # Contents can also be filtered using AroundBlockScan#skip
28
28
  #
29
29
  # To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent
30
30
  class AroundBlockScan
31
- def initialize(code_lines: , block:)
31
+ def initialize(code_lines:, block:)
32
32
  @code_lines = code_lines
33
33
  @orig_before_index = block.lines.first.index
34
34
  @orig_after_index = block.lines.last.index
@@ -56,7 +56,7 @@ module DeadEnd
56
56
  end_count = 0
57
57
  @before_index = before_lines.reverse_each.take_while do |line|
58
58
  next false if stop_next
59
- next true if @skip_array.detect {|meth| line.send(meth) }
59
+ next true if @skip_array.detect { |meth| line.send(meth) }
60
60
 
61
61
  kw_count += 1 if line.is_kw?
62
62
  end_count += 1 if line.is_end?
@@ -65,14 +65,14 @@ module DeadEnd
65
65
  end
66
66
 
67
67
  block.call(line)
68
- end.reverse.first&.index
68
+ end.last&.index
69
69
 
70
70
  stop_next = false
71
71
  kw_count = 0
72
72
  end_count = 0
73
73
  @after_index = after_lines.take_while do |line|
74
74
  next false if stop_next
75
- next true if @skip_array.detect {|meth| line.send(meth) }
75
+ next true if @skip_array.detect { |meth| line.send(meth) }
76
76
 
77
77
  kw_count += 1 if line.is_kw?
78
78
  end_count += 1 if line.is_end?
@@ -89,7 +89,7 @@ module DeadEnd
89
89
  lines = []
90
90
  kw_count = 0
91
91
  end_count = 0
92
- before_lines.reverse.each do |line|
92
+ before_lines.reverse_each do |line|
93
93
  next if line.empty?
94
94
  break if line.indent < @orig_indent
95
95
  next if line.indent != @orig_indent
@@ -109,8 +109,6 @@ module DeadEnd
109
109
  kw_count = 0
110
110
  end_count = 0
111
111
  after_lines.each do |line|
112
- # puts "line: #{line.number} #{line.original_line}, indent: #{line.indent}, #{line.empty?} #{line.indent == @orig_indent}"
113
-
114
112
  next if line.empty?
115
113
  break if line.indent < @orig_indent
116
114
  next if line.indent != @orig_indent
@@ -124,14 +122,13 @@ module DeadEnd
124
122
 
125
123
  lines << line
126
124
  end
127
- lines.select! {|line| !line.is_comment? }
128
125
 
129
126
  lines
130
127
  end
131
128
 
132
129
  def on_falling_indent
133
130
  last_indent = @orig_indent
134
- before_lines.reverse.each do |line|
131
+ before_lines.reverse_each do |line|
135
132
  next if line.empty?
136
133
  if line.indent < last_indent
137
134
  yield line
@@ -150,7 +147,7 @@ module DeadEnd
150
147
  end
151
148
 
152
149
  def scan_neighbors
153
- self.scan_while {|line| line.not_empty? && line.indent >= @orig_indent }
150
+ scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
154
151
  end
155
152
 
156
153
  def next_up
@@ -167,13 +164,14 @@ module DeadEnd
167
164
  before_after_indent << (next_down&.indent || 0)
168
165
 
169
166
  indent = before_after_indent.min
170
- self.scan_while {|line| line.not_empty? && line.indent >= indent }
167
+ scan_while { |line| line.not_empty? && line.indent >= indent }
171
168
 
172
169
  self
173
170
  end
174
171
 
175
172
  def start_at_next_line
176
- before_index; after_index
173
+ before_index
174
+ after_index
177
175
  @before_index -= 1
178
176
  @after_index += 1
179
177
  self
data/lib/dead_end/auto.rb CHANGED
@@ -1,104 +1,6 @@
1
1
  # frozen_string_literal: true
2
- #
3
- require_relative "../dead_end/internals"
4
2
 
5
- # Monkey patch kernel to ensure that all `require` calls call the same
6
- # method
7
- module Kernel
8
- module_function
3
+ require_relative "../dead_end"
4
+ require_relative "core_ext"
9
5
 
10
- alias_method :dead_end_original_require, :require
11
- alias_method :dead_end_original_require_relative, :require_relative
12
- alias_method :dead_end_original_load, :load
13
-
14
- def load(file, wrap = false)
15
- dead_end_original_load(file)
16
- rescue SyntaxError => e
17
- DeadEnd.handle_error(e)
18
- end
19
-
20
- def require(file)
21
- dead_end_original_require(file)
22
- rescue SyntaxError => e
23
- DeadEnd.handle_error(e)
24
- end
25
-
26
- def require_relative(file)
27
- if Pathname.new(file).absolute?
28
- dead_end_original_require file
29
- else
30
- dead_end_original_require File.expand_path("../#{file}", Kernel.caller_locations(1, 1)[0].absolute_path)
31
- end
32
- rescue SyntaxError => e
33
- DeadEnd.handle_error(e)
34
- end
35
- end
36
-
37
- # I honestly have no idea why this Object delegation is needed
38
- # I keep staring at bootsnap and it doesn't have to do this
39
- # is there a bug in their implementation they haven't caught or
40
- # am I doing something different?
41
- class Object
42
- private
43
- def load(path, wrap = false)
44
- Kernel.load(path, wrap)
45
- rescue SyntaxError => e
46
- DeadEnd.handle_error(e)
47
- end
48
-
49
- def require(path)
50
- Kernel.require(path)
51
- rescue SyntaxError => e
52
- DeadEnd.handle_error(e)
53
- end
54
- end
55
-
56
- module DeadEnd
57
- IsProduction = -> {
58
- ENV["RAILS_ENV"] == "production" || ENV["RACK_ENV"] == "production"
59
- }
60
- end
61
-
62
- # Unlike a syntax error, a NoMethodError can occur hundreds or thousands of times and
63
- # chew up CPU and other resources. Since this is primarilly a "development" optimization
64
- # we can attempt to disable this behavior in a production context.
65
- if !DeadEnd::IsProduction.call
66
- class NoMethodError
67
- alias :dead_end_original_to_s :to_s
68
-
69
- def to_s
70
- return super if DeadEnd::IsProduction.call
71
-
72
- file, line, _ = backtrace[0].split(":")
73
- return super if !File.exist?(file)
74
-
75
- index = line.to_i - 1
76
- source = File.read(file)
77
- code_lines = DeadEnd::CodeLine.parse(source)
78
-
79
- block = DeadEnd::CodeBlock.new(lines: code_lines[index])
80
- lines = DeadEnd::CaptureCodeContext.new(
81
- blocks: block,
82
- code_lines: code_lines
83
- ).call
84
-
85
- message = super.dup
86
- message << $/
87
- message << $/
88
-
89
- message << DeadEnd::DisplayCodeWithLineNumbers.new(
90
- lines: lines,
91
- highlight_lines: block.lines,
92
- terminal: self.class.to_tty?
93
- ).call
94
-
95
- message << $/
96
- message
97
- rescue => e
98
- puts "DeadEnd Internal error: #{e.dead_end_original_to_s}"
99
- puts "DeadEnd Internal backtrace:"
100
- puts backtrace.map {|l| " " + l }.join($/)
101
- super
102
- end
103
- end
104
- end
6
+ warn "Calling `require 'dead_end/auto'` is deprecated, please `require 'dead_end'` instead."
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module DeadEnd
3
4
  # This class is responsible for taking a code block that exists
4
5
  # at a far indentaion and then iteratively increasing the block
@@ -30,7 +31,7 @@ module DeadEnd
30
31
  # end
31
32
  #
32
33
  class BlockExpand
33
- def initialize(code_lines: )
34
+ def initialize(code_lines:)
34
35
  @code_lines = code_lines
35
36
  end
36
37
 
@@ -43,7 +44,7 @@ module DeadEnd
43
44
  end
44
45
 
45
46
  def expand_indent(block)
46
- block = AroundBlockScan.new(code_lines: @code_lines, block: block)
47
+ AroundBlockScan.new(code_lines: @code_lines, block: block)
47
48
  .skip(:hidden?)
48
49
  .stop_after_kw
49
50
  .scan_adjacent_indent
@@ -59,15 +60,15 @@ module DeadEnd
59
60
  # Slurp up empties
60
61
  if grab_empty
61
62
  scan = AroundBlockScan.new(code_lines: @code_lines, block: scan.code_block)
62
- .scan_while {|line| line.empty? || line.hidden? }
63
+ .scan_while { |line| line.empty? || line.hidden? }
63
64
  end
64
65
 
65
66
  new_block = scan.code_block
66
67
 
67
68
  if block.lines == new_block.lines
68
- return nil
69
+ nil
69
70
  else
70
- return new_block
71
+ new_block
71
72
  end
72
73
  end
73
74
  end