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