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.
- 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,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
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
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
|
-
|
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
|
-
|
20
|
-
|
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
|
-
|
45
|
+
file = PathnameFromMessage.new(e.message, io: io).call.name
|
46
|
+
raise e unless file
|
23
47
|
|
24
|
-
|
25
|
-
$stderr.puts "Run `$ dead_end #{filename}` for more options\n"
|
48
|
+
io.sync = true
|
26
49
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
)
|
33
|
-
end
|
50
|
+
call(
|
51
|
+
io: io,
|
52
|
+
source: file.read,
|
53
|
+
filename: file
|
54
|
+
)
|
34
55
|
|
35
|
-
|
36
|
-
$stderr.puts ""
|
37
|
-
raise e
|
56
|
+
raise e if re_raise
|
38
57
|
end
|
39
58
|
|
40
|
-
|
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
|
-
#
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
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
|
-
|
120
|
+
true
|
90
121
|
else
|
91
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
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
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
6
|
-
|
7
|
-
module Kernel
|
8
|
-
module_function
|
3
|
+
require_relative "../dead_end"
|
4
|
+
require_relative "core_ext"
|
9
5
|
|
10
|
-
|
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
|
-
|
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
|
-
|
69
|
+
nil
|
69
70
|
else
|
70
|
-
|
71
|
+
new_block
|
71
72
|
end
|
72
73
|
end
|
73
74
|
end
|