dead_end 1.0.0
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 +7 -0
- data/.circleci/config.yml +41 -0
- data/.github/workflows/check_changelog.yml +13 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +39 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +122 -0
- data/Rakefile +8 -0
- data/assets/syntax_search.gif +0 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dead_end.gemspec +28 -0
- data/exe/dead_end +70 -0
- data/lib/dead_end.rb +4 -0
- data/lib/dead_end/around_block_scan.rb +193 -0
- data/lib/dead_end/auto.rb +51 -0
- data/lib/dead_end/block_expand.rb +74 -0
- data/lib/dead_end/capture_code_context.rb +62 -0
- data/lib/dead_end/code_block.rb +78 -0
- data/lib/dead_end/code_frontier.rb +151 -0
- data/lib/dead_end/code_line.rb +139 -0
- data/lib/dead_end/code_search.rb +157 -0
- data/lib/dead_end/display_code_with_line_numbers.rb +71 -0
- data/lib/dead_end/display_invalid_blocks.rb +122 -0
- data/lib/dead_end/fyi.rb +7 -0
- data/lib/dead_end/heredoc_block_parse.rb +30 -0
- data/lib/dead_end/internals.rb +156 -0
- data/lib/dead_end/lex_all.rb +58 -0
- data/lib/dead_end/parse_blocks_from_indent_line.rb +56 -0
- data/lib/dead_end/trailing_slash_join.rb +53 -0
- data/lib/dead_end/version.rb +5 -0
- data/lib/dead_end/who_dis_syntax_error.rb +66 -0
- metadata +83 -0
data/exe/dead_end
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require "optparse"
|
5
|
+
require_relative "../lib/dead_end.rb"
|
6
|
+
|
7
|
+
options = {}
|
8
|
+
options[:terminal] = true
|
9
|
+
options[:record_dir] = ENV["DEAD_END_RECORD_DIR"]
|
10
|
+
|
11
|
+
parser = OptionParser.new do |opts|
|
12
|
+
opts.banner = <<~EOM
|
13
|
+
Usage: dead_end <file> [options]
|
14
|
+
|
15
|
+
Parses a ruby source file and searches for syntax error(s) such as
|
16
|
+
unexpected `end', expecting end-of-input.
|
17
|
+
|
18
|
+
Example:
|
19
|
+
|
20
|
+
$ dead_end dog.rb
|
21
|
+
|
22
|
+
# ...
|
23
|
+
|
24
|
+
❯ 10 defdog
|
25
|
+
❯ 15 end
|
26
|
+
❯ 16
|
27
|
+
|
28
|
+
Env options:
|
29
|
+
|
30
|
+
DEAD_END_RECORD_DIR=<dir>
|
31
|
+
|
32
|
+
When enabled, records the steps used to search for a syntax error to the
|
33
|
+
given directory
|
34
|
+
|
35
|
+
Options:
|
36
|
+
EOM
|
37
|
+
|
38
|
+
opts.on("--help", "Help - displays this message") do |v|
|
39
|
+
puts opts
|
40
|
+
exit
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("--record <dir>", "When enabled, records the steps used to search for a syntax error to the given directory") do |v|
|
44
|
+
options[:record_dir] = v
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on("--no-terminal", "Disable terminal highlighting") do |v|
|
48
|
+
options[:terminal] = false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
parser.parse!
|
52
|
+
|
53
|
+
file = ARGV[0]
|
54
|
+
|
55
|
+
if file.nil? || file.empty?
|
56
|
+
# Display help if raw command
|
57
|
+
parser.parse! %w[--help]
|
58
|
+
end
|
59
|
+
|
60
|
+
file = Pathname(file)
|
61
|
+
options[:record_dir] = "tmp" if ENV["DEBUG"]
|
62
|
+
|
63
|
+
$stderr.puts "Record dir: #{options[:record_dir]}" if options[:record_dir]
|
64
|
+
|
65
|
+
DeadEnd.call(
|
66
|
+
source: file.read,
|
67
|
+
filename: file.expand_path,
|
68
|
+
terminal: options[:terminal],
|
69
|
+
record_dir: options[:record_dir]
|
70
|
+
)
|
data/lib/dead_end.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
module DeadEnd
|
4
|
+
# This class is useful for exploring contents before and after
|
5
|
+
# a block
|
6
|
+
#
|
7
|
+
# It searches above and below the passed in block to match for
|
8
|
+
# whatever criteria you give it:
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# def dog
|
13
|
+
# puts "bark"
|
14
|
+
# puts "bark"
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# scan = AroundBlockScan.new(
|
18
|
+
# code_lines: code_lines
|
19
|
+
# block: CodeBlock.new(lines: code_lines[1])
|
20
|
+
# )
|
21
|
+
#
|
22
|
+
# scan.scan_while { true }
|
23
|
+
#
|
24
|
+
# puts scan.before_index # => 0
|
25
|
+
# puts scan.after_index # => 3
|
26
|
+
#
|
27
|
+
# Contents can also be filtered using AroundBlockScan#skip
|
28
|
+
#
|
29
|
+
# To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent
|
30
|
+
class AroundBlockScan
|
31
|
+
def initialize(code_lines: , block:)
|
32
|
+
@code_lines = code_lines
|
33
|
+
@orig_before_index = block.lines.first.index
|
34
|
+
@orig_after_index = block.lines.last.index
|
35
|
+
@orig_indent = block.current_indent
|
36
|
+
@skip_array = []
|
37
|
+
@after_array = []
|
38
|
+
@before_array = []
|
39
|
+
@stop_after_kw = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def skip(name)
|
43
|
+
@skip_array << name
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def stop_after_kw
|
48
|
+
@stop_after_kw = true
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def scan_while(&block)
|
53
|
+
stop_next = false
|
54
|
+
|
55
|
+
kw_count = 0
|
56
|
+
end_count = 0
|
57
|
+
@before_index = before_lines.reverse_each.take_while do |line|
|
58
|
+
next false if stop_next
|
59
|
+
next true if @skip_array.detect {|meth| line.send(meth) }
|
60
|
+
|
61
|
+
kw_count += 1 if line.is_kw?
|
62
|
+
end_count += 1 if line.is_end?
|
63
|
+
if @stop_after_kw && kw_count > end_count
|
64
|
+
stop_next = true
|
65
|
+
end
|
66
|
+
|
67
|
+
block.call(line)
|
68
|
+
end.reverse.first&.index
|
69
|
+
|
70
|
+
stop_next = false
|
71
|
+
kw_count = 0
|
72
|
+
end_count = 0
|
73
|
+
@after_index = after_lines.take_while do |line|
|
74
|
+
next false if stop_next
|
75
|
+
next true if @skip_array.detect {|meth| line.send(meth) }
|
76
|
+
|
77
|
+
kw_count += 1 if line.is_kw?
|
78
|
+
end_count += 1 if line.is_end?
|
79
|
+
if @stop_after_kw && end_count > kw_count
|
80
|
+
stop_next = true
|
81
|
+
end
|
82
|
+
|
83
|
+
block.call(line)
|
84
|
+
end.last&.index
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def capture_neighbor_context
|
89
|
+
lines = []
|
90
|
+
kw_count = 0
|
91
|
+
end_count = 0
|
92
|
+
before_lines.reverse.each do |line|
|
93
|
+
next if line.empty?
|
94
|
+
break if line.indent < @orig_indent
|
95
|
+
next if line.indent != @orig_indent
|
96
|
+
|
97
|
+
kw_count += 1 if line.is_kw?
|
98
|
+
end_count += 1 if line.is_end?
|
99
|
+
if kw_count != 0 && kw_count == end_count
|
100
|
+
lines << line
|
101
|
+
break
|
102
|
+
end
|
103
|
+
|
104
|
+
lines << line
|
105
|
+
end
|
106
|
+
|
107
|
+
lines.reverse!
|
108
|
+
|
109
|
+
kw_count = 0
|
110
|
+
end_count = 0
|
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
|
+
next if line.empty?
|
115
|
+
break if line.indent < @orig_indent
|
116
|
+
next if line.indent != @orig_indent
|
117
|
+
|
118
|
+
kw_count += 1 if line.is_kw?
|
119
|
+
end_count += 1 if line.is_end?
|
120
|
+
if kw_count != 0 && kw_count == end_count
|
121
|
+
lines << line
|
122
|
+
break
|
123
|
+
end
|
124
|
+
|
125
|
+
lines << line
|
126
|
+
end
|
127
|
+
lines.select! {|line| !line.is_comment? }
|
128
|
+
|
129
|
+
lines
|
130
|
+
end
|
131
|
+
|
132
|
+
def on_falling_indent
|
133
|
+
last_indent = @orig_indent
|
134
|
+
before_lines.reverse.each do |line|
|
135
|
+
next if line.empty?
|
136
|
+
if line.indent < last_indent
|
137
|
+
yield line
|
138
|
+
last_indent = line.indent
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
last_indent = @orig_indent
|
143
|
+
after_lines.each do |line|
|
144
|
+
next if line.empty?
|
145
|
+
if line.indent < last_indent
|
146
|
+
yield line
|
147
|
+
last_indent = line.indent
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def scan_neighbors
|
153
|
+
self.scan_while {|line| line.not_empty? && line.indent >= @orig_indent }
|
154
|
+
end
|
155
|
+
|
156
|
+
def scan_adjacent_indent
|
157
|
+
before_indent = @code_lines[@orig_before_index.pred]&.indent || 0
|
158
|
+
after_indent = @code_lines[@orig_after_index.next]&.indent || 0
|
159
|
+
|
160
|
+
indent = [before_indent, after_indent].min
|
161
|
+
self.scan_while {|line| line.not_empty? && line.indent >= indent }
|
162
|
+
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
def start_at_next_line
|
167
|
+
before_index; after_index
|
168
|
+
@before_index -= 1
|
169
|
+
@after_index += 1
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
def code_block
|
174
|
+
CodeBlock.new(lines: @code_lines[before_index..after_index])
|
175
|
+
end
|
176
|
+
|
177
|
+
def before_index
|
178
|
+
@before_index ||= @orig_before_index
|
179
|
+
end
|
180
|
+
|
181
|
+
def after_index
|
182
|
+
@after_index ||= @orig_after_index
|
183
|
+
end
|
184
|
+
|
185
|
+
private def before_lines
|
186
|
+
@code_lines[0...@orig_before_index]
|
187
|
+
end
|
188
|
+
|
189
|
+
private def after_lines
|
190
|
+
@code_lines[@orig_after_index.next..-1]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative "../dead_end/internals"
|
2
|
+
|
3
|
+
# Monkey patch kernel to ensure that all `require` calls call the same
|
4
|
+
# method
|
5
|
+
module Kernel
|
6
|
+
alias_method :original_require, :require
|
7
|
+
alias_method :original_require_relative, :require_relative
|
8
|
+
alias_method :original_load, :load
|
9
|
+
|
10
|
+
def load(file, wrap = false)
|
11
|
+
original_load(file)
|
12
|
+
rescue SyntaxError => e
|
13
|
+
DeadEnd.handle_error(e)
|
14
|
+
end
|
15
|
+
|
16
|
+
def require(file)
|
17
|
+
original_require(file)
|
18
|
+
rescue SyntaxError => e
|
19
|
+
DeadEnd.handle_error(e)
|
20
|
+
end
|
21
|
+
|
22
|
+
def require_relative(file)
|
23
|
+
if Pathname.new(file).absolute?
|
24
|
+
original_require file
|
25
|
+
else
|
26
|
+
original_require File.expand_path("../#{file}", caller_locations(1, 1)[0].absolute_path)
|
27
|
+
end
|
28
|
+
rescue SyntaxError => e
|
29
|
+
DeadEnd.handle_error(e)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# I honestly have no idea why this Object delegation is needed
|
34
|
+
# I keep staring at bootsnap and it doesn't have to do this
|
35
|
+
# is there a bug in their implementation they haven't caught or
|
36
|
+
# am I doing something different?
|
37
|
+
class Object
|
38
|
+
private
|
39
|
+
def load(path, wrap = false)
|
40
|
+
Kernel.load(path, wrap)
|
41
|
+
rescue SyntaxError => e
|
42
|
+
DeadEnd.handle_error(e)
|
43
|
+
end
|
44
|
+
|
45
|
+
def require(path)
|
46
|
+
Kernel.require(path)
|
47
|
+
rescue SyntaxError => e
|
48
|
+
DeadEnd.handle_error(e)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module DeadEnd
|
3
|
+
# This class is responsible for taking a code block that exists
|
4
|
+
# at a far indentaion and then iteratively increasing the block
|
5
|
+
# so that it captures everything within the same indentation block.
|
6
|
+
#
|
7
|
+
# def dog
|
8
|
+
# puts "bow"
|
9
|
+
# puts "wow"
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# block = BlockExpand.new(code_lines: code_lines)
|
13
|
+
# .call(CodeBlock.new(lines: code_lines[1]))
|
14
|
+
#
|
15
|
+
# puts block.to_s
|
16
|
+
# # => puts "bow"
|
17
|
+
# puts "wow"
|
18
|
+
#
|
19
|
+
#
|
20
|
+
# Once a code block has captured everything at a given indentation level
|
21
|
+
# then it will expand to capture surrounding indentation.
|
22
|
+
#
|
23
|
+
# block = BlockExpand.new(code_lines: code_lines)
|
24
|
+
# .call(block)
|
25
|
+
#
|
26
|
+
# block.to_s
|
27
|
+
# # => def dog
|
28
|
+
# puts "bow"
|
29
|
+
# puts "wow"
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
class BlockExpand
|
33
|
+
def initialize(code_lines: )
|
34
|
+
@code_lines = code_lines
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(block)
|
38
|
+
if (next_block = expand_neighbors(block, grab_empty: true))
|
39
|
+
return next_block
|
40
|
+
end
|
41
|
+
|
42
|
+
expand_indent(block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def expand_indent(block)
|
46
|
+
block = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
47
|
+
.skip(:hidden?)
|
48
|
+
.stop_after_kw
|
49
|
+
.scan_adjacent_indent
|
50
|
+
.code_block
|
51
|
+
end
|
52
|
+
|
53
|
+
def expand_neighbors(block, grab_empty: true)
|
54
|
+
scan = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
55
|
+
.skip(:hidden?)
|
56
|
+
.stop_after_kw
|
57
|
+
.scan_neighbors
|
58
|
+
|
59
|
+
# Slurp up empties
|
60
|
+
if grab_empty
|
61
|
+
scan = AroundBlockScan.new(code_lines: @code_lines, block: scan.code_block)
|
62
|
+
.scan_while {|line| line.empty? || line.hidden? }
|
63
|
+
end
|
64
|
+
|
65
|
+
new_block = scan.code_block
|
66
|
+
|
67
|
+
if block.lines == new_block.lines
|
68
|
+
return nil
|
69
|
+
else
|
70
|
+
return new_block
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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.
|
8
|
+
#
|
9
|
+
# Return is an array of CodeLines to be rendered.
|
10
|
+
#
|
11
|
+
# Surrounding code is captured regardless of visible state
|
12
|
+
#
|
13
|
+
# puts block.to_s # => "def bark"
|
14
|
+
#
|
15
|
+
# context = CaptureCodeContext.new(
|
16
|
+
# blocks: block,
|
17
|
+
# code_lines: code_lines
|
18
|
+
# )
|
19
|
+
#
|
20
|
+
# puts context.call.join
|
21
|
+
# # =>
|
22
|
+
# class Dog
|
23
|
+
# def bark
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
class CaptureCodeContext
|
27
|
+
attr_reader :code_lines
|
28
|
+
|
29
|
+
def initialize(blocks: , code_lines:)
|
30
|
+
@blocks = Array(blocks)
|
31
|
+
@code_lines = code_lines
|
32
|
+
@visible_lines = @blocks.map(&:visible_lines).flatten
|
33
|
+
@lines_to_output = @visible_lines.dup
|
34
|
+
end
|
35
|
+
|
36
|
+
def call
|
37
|
+
@blocks.each do |block|
|
38
|
+
around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
39
|
+
.start_at_next_line
|
40
|
+
.capture_neighbor_context
|
41
|
+
|
42
|
+
around_lines -= block.lines
|
43
|
+
|
44
|
+
@lines_to_output.concat(around_lines)
|
45
|
+
|
46
|
+
AroundBlockScan.new(
|
47
|
+
block: block,
|
48
|
+
code_lines: @code_lines,
|
49
|
+
).on_falling_indent do |line|
|
50
|
+
@lines_to_output << line
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
@lines_to_output.select!(&:not_empty?)
|
55
|
+
@lines_to_output.select!(&:not_comment?)
|
56
|
+
@lines_to_output.uniq!
|
57
|
+
@lines_to_output.sort!
|
58
|
+
|
59
|
+
return @lines_to_output
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|