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