dead_end 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|