dead_end 3.1.1 → 4.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 +4 -4
- data/.circleci/config.yml +19 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -2
- data/README.md +1 -1
- data/dead_end.gemspec +6 -2
- data/exe/dead_end +1 -1
- data/lib/dead_end/api.rb +9 -3
- data/lib/dead_end/around_block_scan.rb +34 -10
- data/lib/dead_end/block_expand.rb +12 -13
- data/lib/dead_end/code_block.rb +12 -9
- data/lib/dead_end/code_frontier.rb +23 -28
- data/lib/dead_end/code_search.rb +14 -21
- data/lib/dead_end/core_ext.rb +80 -27
- data/lib/dead_end/parse_blocks_from_indent_line.rb +11 -6
- data/lib/dead_end/pathname_from_message.rb +19 -7
- data/lib/dead_end/priority_engulf_queue.rb +63 -0
- data/lib/dead_end/priority_queue.rb +105 -0
- data/lib/dead_end/unvisited_lines.rb +36 -0
- data/lib/dead_end/version.rb +6 -2
- data/lib/dead_end.rb +0 -1
- metadata +6 -4
- data/lib/dead_end/insertion_sort.rb +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 655326e21a4ad4ae77a34020b69ce404f2405b2695b2e2e977c685352fcc0e79
|
4
|
+
data.tar.gz: 4a85b498c7d039a1ab016dae09775cb7a0a456c02a234ce83c5ac5776af97f0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 409e22fe3a76ca5cc64a1886f58aaf8bcccc9b1f8564e580f8f1d70a63ba8a32216b6c2e9ac1e0c670b26e06a4a4b977d98562e4b9d2ea28a8bc30c3b096342f
|
7
|
+
data.tar.gz: 379bda579f643647303c39d409c4a675711e5c9679da123961b81b30b86d5b4133e8ea8636cf24eab757b4b262a2f8e93e977360b9a7ba68a5dfaa029e02fba2
|
data/.circleci/config.yml
CHANGED
@@ -47,12 +47,25 @@ jobs:
|
|
47
47
|
|
48
48
|
"ruby-3-1":
|
49
49
|
docker:
|
50
|
-
- image: 'cimg/
|
50
|
+
- image: 'cimg/ruby:3.1'
|
51
51
|
steps:
|
52
52
|
- checkout
|
53
|
-
- ruby/install
|
54
|
-
|
55
|
-
|
53
|
+
- ruby/install-deps
|
54
|
+
- <<: *unit
|
55
|
+
|
56
|
+
"ruby-3-2":
|
57
|
+
docker:
|
58
|
+
- image: 'ruby:3.2.0-preview1'
|
59
|
+
steps:
|
60
|
+
- checkout
|
61
|
+
- ruby/install-deps
|
62
|
+
- <<: *unit
|
63
|
+
"ruby-3-2-yjit":
|
64
|
+
docker:
|
65
|
+
- image: 'ruby:3.2.0-preview1'
|
66
|
+
steps:
|
67
|
+
- run: echo "export RUBY_YJIT_ENABLE=1" >> $BASH_ENV
|
68
|
+
- checkout
|
56
69
|
- ruby/install-deps
|
57
70
|
- <<: *unit
|
58
71
|
|
@@ -73,4 +86,6 @@ workflows:
|
|
73
86
|
- "ruby-2-7"
|
74
87
|
- "ruby-3-0"
|
75
88
|
- "ruby-3-1"
|
89
|
+
- "ruby-3-2"
|
90
|
+
- "ruby-3-2-yjit"
|
76
91
|
- "lint"
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
## HEAD (unreleased)
|
2
2
|
|
3
|
+
## 4.0.0
|
4
|
+
|
5
|
+
- Code that does not have an associated file (eval and streamed) no longer produce a warning saying that the file could not be found. To produce a warning with these code types run with DEBUG=1 environment variable. (https://github.com/zombocom/dead_end/pull/143)
|
6
|
+
- [Breaking] Lazy load DeadEnd internals only if there is a Syntax error. Use `require "dead_end"; require "dead_end/api"` to load eagerly all internals. Otherwise `require "dead_end"` will set up an autoload for the first time the DeadEnd module is used in code. This should only happen on a syntax error. (https://github.com/zombocom/dead_end/pull/142)
|
7
|
+
- Monkeypatch `SyntaxError#detailed_message` in Ruby 3.2+ instead of `require`, `load`, and `require_relative` (https://github.com/zombocom/dead_end/pull/139)
|
8
|
+
|
9
|
+
## 3.1.2
|
10
|
+
|
11
|
+
- Fixed internal class AroundBlockScan, minor changes in outputs (https://github.com/zombocom/dead_end/pull/131)
|
12
|
+
|
3
13
|
## 3.1.1
|
4
14
|
|
5
15
|
- Fix case where Ripper lexing identified incorrect code as a keyword (https://github.com/zombocom/dead_end/pull/122)
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
dead_end (
|
4
|
+
dead_end (4.0.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
9
|
ast (2.4.2)
|
10
|
+
benchmark-ips (2.9.2)
|
10
11
|
diff-lcs (1.4.4)
|
11
12
|
parallel (1.21.0)
|
12
13
|
parser (3.0.2.0)
|
@@ -54,6 +55,7 @@ PLATFORMS
|
|
54
55
|
ruby
|
55
56
|
|
56
57
|
DEPENDENCIES
|
58
|
+
benchmark-ips
|
57
59
|
dead_end!
|
58
60
|
rake (~> 12.0)
|
59
61
|
rspec (~> 3.0)
|
@@ -62,4 +64,4 @@ DEPENDENCIES
|
|
62
64
|
standard
|
63
65
|
|
64
66
|
BUNDLED WITH
|
65
|
-
2.3.
|
67
|
+
2.3.14
|
data/README.md
CHANGED
@@ -168,7 +168,7 @@ Here's an example:
|
|
168
168
|
|
169
169
|
## Use internals
|
170
170
|
|
171
|
-
To use the `dead_end` gem without monkeypatching you can `require '
|
171
|
+
To use the `dead_end` gem without monkeypatching you can `require 'dead_end/api'`. This will allow you to load `dead_end` and use its internals without mutating `require`.
|
172
172
|
|
173
173
|
Stable internal interface(s):
|
174
174
|
|
data/dead_end.gemspec
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
begin
|
4
|
+
require_relative "lib/dead_end/version"
|
5
|
+
rescue LoadError # Fallback to load version file in ruby core repository
|
6
|
+
require_relative "version"
|
7
|
+
end
|
4
8
|
|
5
9
|
Gem::Specification.new do |spec|
|
6
10
|
spec.name = "dead_end"
|
7
|
-
spec.version =
|
11
|
+
spec.version = UnloadedDeadEnd::VERSION
|
8
12
|
spec.authors = ["schneems"]
|
9
13
|
spec.email = ["richard.schneeman+foo@gmail.com"]
|
10
14
|
|
data/exe/dead_end
CHANGED
data/lib/dead_end/api.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "version"
|
2
4
|
|
3
5
|
require "tmpdir"
|
@@ -7,6 +9,8 @@ require "ripper"
|
|
7
9
|
require "timeout"
|
8
10
|
|
9
11
|
module DeadEnd
|
12
|
+
VERSION = UnloadedDeadEnd::VERSION
|
13
|
+
|
10
14
|
# Used to indicate a default value that cannot
|
11
15
|
# be confused with another input.
|
12
16
|
DEFAULT_VALUE = Object.new.freeze
|
@@ -16,7 +20,7 @@ module DeadEnd
|
|
16
20
|
|
17
21
|
# DeadEnd.handle_error [Public]
|
18
22
|
#
|
19
|
-
# Takes a `SyntaxError
|
23
|
+
# Takes a `SyntaxError` exception, uses the
|
20
24
|
# error message to locate the file. Then the file
|
21
25
|
# will be analyzed to find the location of the syntax
|
22
26
|
# error and emit that location to stderr.
|
@@ -59,7 +63,7 @@ module DeadEnd
|
|
59
63
|
# DeadEnd.call [Private]
|
60
64
|
#
|
61
65
|
# Main private interface
|
62
|
-
def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir:
|
66
|
+
def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: DEFAULT_VALUE, timeout: TIMEOUT_DEFAULT, io: $stderr)
|
63
67
|
search = nil
|
64
68
|
filename = nil if filename == DEFAULT_VALUE
|
65
69
|
Timeout.timeout(timeout) do
|
@@ -189,8 +193,10 @@ require_relative "code_line"
|
|
189
193
|
require_relative "code_block"
|
190
194
|
require_relative "block_expand"
|
191
195
|
require_relative "ripper_errors"
|
192
|
-
require_relative "
|
196
|
+
require_relative "priority_queue"
|
197
|
+
require_relative "unvisited_lines"
|
193
198
|
require_relative "around_block_scan"
|
199
|
+
require_relative "priority_engulf_queue"
|
194
200
|
require_relative "pathname_from_message"
|
195
201
|
require_relative "display_invalid_blocks"
|
196
202
|
require_relative "parse_blocks_from_indent_line"
|
@@ -37,10 +37,20 @@ module DeadEnd
|
|
37
37
|
@after_array = []
|
38
38
|
@before_array = []
|
39
39
|
@stop_after_kw = false
|
40
|
+
|
41
|
+
@skip_hidden = false
|
42
|
+
@skip_empty = false
|
40
43
|
end
|
41
44
|
|
42
45
|
def skip(name)
|
43
|
-
|
46
|
+
case name
|
47
|
+
when :hidden?
|
48
|
+
@skip_hidden = true
|
49
|
+
when :empty?
|
50
|
+
@skip_empty = true
|
51
|
+
else
|
52
|
+
raise "Unsupported skip #{name}"
|
53
|
+
end
|
44
54
|
self
|
45
55
|
end
|
46
56
|
|
@@ -49,14 +59,15 @@ module DeadEnd
|
|
49
59
|
self
|
50
60
|
end
|
51
61
|
|
52
|
-
def scan_while
|
62
|
+
def scan_while
|
53
63
|
stop_next = false
|
54
64
|
|
55
65
|
kw_count = 0
|
56
66
|
end_count = 0
|
57
|
-
|
67
|
+
index = before_lines.reverse_each.take_while do |line|
|
58
68
|
next false if stop_next
|
59
|
-
next true if @
|
69
|
+
next true if @skip_hidden && line.hidden?
|
70
|
+
next true if @skip_empty && line.empty?
|
60
71
|
|
61
72
|
kw_count += 1 if line.is_kw?
|
62
73
|
end_count += 1 if line.is_end?
|
@@ -64,15 +75,20 @@ module DeadEnd
|
|
64
75
|
stop_next = true
|
65
76
|
end
|
66
77
|
|
67
|
-
|
78
|
+
yield line
|
68
79
|
end.last&.index
|
69
80
|
|
81
|
+
if index && index < before_index
|
82
|
+
@before_index = index
|
83
|
+
end
|
84
|
+
|
70
85
|
stop_next = false
|
71
86
|
kw_count = 0
|
72
87
|
end_count = 0
|
73
|
-
|
88
|
+
index = after_lines.take_while do |line|
|
74
89
|
next false if stop_next
|
75
|
-
next true if @
|
90
|
+
next true if @skip_hidden && line.hidden?
|
91
|
+
next true if @skip_empty && line.empty?
|
76
92
|
|
77
93
|
kw_count += 1 if line.is_kw?
|
78
94
|
end_count += 1 if line.is_end?
|
@@ -80,8 +96,12 @@ module DeadEnd
|
|
80
96
|
stop_next = true
|
81
97
|
end
|
82
98
|
|
83
|
-
|
99
|
+
yield line
|
84
100
|
end.last&.index
|
101
|
+
|
102
|
+
if index && index > after_index
|
103
|
+
@after_index = index
|
104
|
+
end
|
85
105
|
self
|
86
106
|
end
|
87
107
|
|
@@ -178,7 +198,11 @@ module DeadEnd
|
|
178
198
|
end
|
179
199
|
|
180
200
|
def code_block
|
181
|
-
CodeBlock.new(lines:
|
201
|
+
CodeBlock.new(lines: lines)
|
202
|
+
end
|
203
|
+
|
204
|
+
def lines
|
205
|
+
@code_lines[before_index..after_index]
|
182
206
|
end
|
183
207
|
|
184
208
|
def before_index
|
@@ -190,7 +214,7 @@ module DeadEnd
|
|
190
214
|
end
|
191
215
|
|
192
216
|
private def before_lines
|
193
|
-
@code_lines[0
|
217
|
+
@code_lines[0...before_index] || []
|
194
218
|
end
|
195
219
|
|
196
220
|
private def after_lines
|
@@ -36,7 +36,7 @@ module DeadEnd
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def call(block)
|
39
|
-
if (next_block = expand_neighbors(block
|
39
|
+
if (next_block = expand_neighbors(block))
|
40
40
|
return next_block
|
41
41
|
end
|
42
42
|
|
@@ -51,25 +51,24 @@ module DeadEnd
|
|
51
51
|
.code_block
|
52
52
|
end
|
53
53
|
|
54
|
-
def expand_neighbors(block
|
55
|
-
|
54
|
+
def expand_neighbors(block)
|
55
|
+
expanded_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
56
56
|
.skip(:hidden?)
|
57
57
|
.stop_after_kw
|
58
58
|
.scan_neighbors
|
59
|
+
.scan_while { |line| line.empty? } # Slurp up empties
|
60
|
+
.lines
|
59
61
|
|
60
|
-
|
61
|
-
if grab_empty
|
62
|
-
scan = AroundBlockScan.new(code_lines: @code_lines, block: scan.code_block)
|
63
|
-
.scan_while { |line| line.empty? || line.hidden? }
|
64
|
-
end
|
65
|
-
|
66
|
-
new_block = scan.code_block
|
67
|
-
|
68
|
-
if block.lines == new_block.lines
|
62
|
+
if block.lines == expanded_lines
|
69
63
|
nil
|
70
64
|
else
|
71
|
-
|
65
|
+
CodeBlock.new(lines: expanded_lines)
|
72
66
|
end
|
73
67
|
end
|
68
|
+
|
69
|
+
# Managable rspec errors
|
70
|
+
def inspect
|
71
|
+
"#<DeadEnd::CodeBlock:0x0000123843lol >"
|
72
|
+
end
|
74
73
|
end
|
75
74
|
end
|
data/lib/dead_end/code_block.rb
CHANGED
@@ -18,11 +18,22 @@ module DeadEnd
|
|
18
18
|
#
|
19
19
|
class CodeBlock
|
20
20
|
UNSET = Object.new.freeze
|
21
|
-
attr_reader :lines
|
21
|
+
attr_reader :lines, :starts_at, :ends_at
|
22
22
|
|
23
23
|
def initialize(lines: [])
|
24
24
|
@lines = Array(lines)
|
25
25
|
@valid = UNSET
|
26
|
+
@deleted = false
|
27
|
+
@starts_at = @lines.first.number
|
28
|
+
@ends_at = @lines.last.number
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete
|
32
|
+
@deleted = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def deleted?
|
36
|
+
@deleted
|
26
37
|
end
|
27
38
|
|
28
39
|
def visible_lines
|
@@ -41,14 +52,6 @@ module DeadEnd
|
|
41
52
|
@lines.all?(&:hidden?)
|
42
53
|
end
|
43
54
|
|
44
|
-
def starts_at
|
45
|
-
@starts_at ||= @lines.first&.line_number
|
46
|
-
end
|
47
|
-
|
48
|
-
def ends_at
|
49
|
-
@ends_at ||= @lines.last&.line_number
|
50
|
-
end
|
51
|
-
|
52
55
|
# This is used for frontier ordering, we are searching from
|
53
56
|
# the largest indentation to the smallest. This allows us to
|
54
57
|
# populate an array with multiple code blocks then call `sort!`
|
@@ -50,18 +50,16 @@ module DeadEnd
|
|
50
50
|
# CodeFrontier#detect_invalid_blocks
|
51
51
|
#
|
52
52
|
class CodeFrontier
|
53
|
-
def initialize(code_lines:)
|
53
|
+
def initialize(code_lines:, unvisited: UnvisitedLines.new(code_lines: code_lines))
|
54
54
|
@code_lines = code_lines
|
55
|
-
@
|
56
|
-
@
|
57
|
-
@visited_lines = {}
|
55
|
+
@unvisited = unvisited
|
56
|
+
@queue = PriorityEngulfQueue.new
|
58
57
|
|
59
|
-
@has_run = false
|
60
58
|
@check_next = true
|
61
59
|
end
|
62
60
|
|
63
61
|
def count
|
64
|
-
@
|
62
|
+
@queue.length
|
65
63
|
end
|
66
64
|
|
67
65
|
# Performance optimization
|
@@ -88,7 +86,7 @@ module DeadEnd
|
|
88
86
|
# removed. By default it checks all blocks in present in
|
89
87
|
# the frontier array, but can be used for arbitrary arrays
|
90
88
|
# of codeblocks as well
|
91
|
-
def holds_all_syntax_errors?(block_array = @
|
89
|
+
def holds_all_syntax_errors?(block_array = @queue, can_cache: true)
|
92
90
|
return false if can_cache && can_skip_check?
|
93
91
|
|
94
92
|
without_lines = block_array.to_a.flat_map do |block|
|
@@ -103,23 +101,23 @@ module DeadEnd
|
|
103
101
|
|
104
102
|
# Returns a code block with the largest indentation possible
|
105
103
|
def pop
|
106
|
-
@
|
104
|
+
@queue.pop
|
107
105
|
end
|
108
106
|
|
109
107
|
def next_indent_line
|
110
|
-
@
|
108
|
+
@unvisited.peek
|
111
109
|
end
|
112
110
|
|
113
111
|
def expand?
|
114
|
-
return false if @
|
115
|
-
return true if @
|
112
|
+
return false if @queue.empty?
|
113
|
+
return true if @unvisited.empty?
|
116
114
|
|
117
|
-
frontier_indent = @
|
115
|
+
frontier_indent = @queue.peek.current_indent
|
118
116
|
unvisited_indent = next_indent_line.indent
|
119
117
|
|
120
118
|
if ENV["DEBUG"]
|
121
119
|
puts "```"
|
122
|
-
puts @
|
120
|
+
puts @queue.peek.to_s
|
123
121
|
puts "```"
|
124
122
|
puts " @frontier indent: #{frontier_indent}"
|
125
123
|
puts " @unvisited indent: #{unvisited_indent}"
|
@@ -129,33 +127,30 @@ module DeadEnd
|
|
129
127
|
frontier_indent >= unvisited_indent
|
130
128
|
end
|
131
129
|
|
130
|
+
# Keeps track of what lines have been added to blocks and which are not yet
|
131
|
+
# visited.
|
132
132
|
def register_indent_block(block)
|
133
|
-
block
|
134
|
-
next if @visited_lines[line]
|
135
|
-
@visited_lines[line] = true
|
136
|
-
|
137
|
-
index = @unvisited_lines.bsearch_index { |l| line.indent_index <=> l.indent_index }
|
138
|
-
@unvisited_lines.delete_at(index)
|
139
|
-
end
|
133
|
+
@unvisited.visit_block(block)
|
140
134
|
self
|
141
135
|
end
|
142
136
|
|
137
|
+
# When one element fully encapsulates another we remove the smaller
|
138
|
+
# block from the frontier. This prevents double expansions and all-around
|
139
|
+
# weird behavior. However this guarantee is quite expensive to maintain
|
140
|
+
def register_engulf_block(block)
|
141
|
+
end
|
142
|
+
|
143
143
|
# Add a block to the frontier
|
144
144
|
#
|
145
145
|
# This method ensures the frontier always remains sorted (in indentation order)
|
146
146
|
# and that each code block's lines are removed from the indentation hash so we
|
147
147
|
# don't re-evaluate the same line multiple times.
|
148
148
|
def <<(block)
|
149
|
-
|
149
|
+
@unvisited.visit_block(block)
|
150
150
|
|
151
|
-
|
152
|
-
@frontier.to_a.reject! { |b|
|
153
|
-
b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
|
154
|
-
}
|
151
|
+
@queue.push(block)
|
155
152
|
|
156
153
|
@check_next = true if block.invalid?
|
157
|
-
@frontier << block
|
158
|
-
# @frontier.sort!
|
159
154
|
|
160
155
|
self
|
161
156
|
end
|
@@ -175,7 +170,7 @@ module DeadEnd
|
|
175
170
|
# Given that we know our syntax error exists somewhere in our frontier, we want to find
|
176
171
|
# the smallest possible set of blocks that contain all the syntax errors
|
177
172
|
def detect_invalid_blocks
|
178
|
-
self.class.combination(@
|
173
|
+
self.class.combination(@queue.to_a.select(&:invalid?)).detect do |block_array|
|
179
174
|
holds_all_syntax_errors?(block_array, can_cache: false)
|
180
175
|
end || []
|
181
176
|
end
|
data/lib/dead_end/code_search.rb
CHANGED
@@ -41,7 +41,13 @@ module DeadEnd
|
|
41
41
|
|
42
42
|
attr_reader :invalid_blocks, :record_dir, :code_lines
|
43
43
|
|
44
|
-
def initialize(source, record_dir:
|
44
|
+
def initialize(source, record_dir: DEFAULT_VALUE)
|
45
|
+
record_dir = if record_dir == DEFAULT_VALUE
|
46
|
+
ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil
|
47
|
+
else
|
48
|
+
record_dir
|
49
|
+
end
|
50
|
+
|
45
51
|
if record_dir
|
46
52
|
@record_dir = DeadEnd.record_dir(record_dir)
|
47
53
|
@write_count = 0
|
@@ -63,7 +69,7 @@ module DeadEnd
|
|
63
69
|
def record(block:, name: "record")
|
64
70
|
return unless @record_dir
|
65
71
|
@name_tick[name] += 1
|
66
|
-
filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt"
|
72
|
+
filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}-(#{block.starts_at}__#{block.ends_at}).txt"
|
67
73
|
if ENV["DEBUG"]
|
68
74
|
puts "\n\n==== #{filename} ===="
|
69
75
|
puts "\n```#{block.starts_at}..#{block.ends_at}"
|
@@ -78,7 +84,7 @@ module DeadEnd
|
|
78
84
|
highlight_lines: block.lines
|
79
85
|
).call
|
80
86
|
|
81
|
-
f.write(document)
|
87
|
+
f.write(" Block lines: #{block.starts_at..block.ends_at} (#{name}) \n\n#{document}")
|
82
88
|
end
|
83
89
|
end
|
84
90
|
|
@@ -89,25 +95,13 @@ module DeadEnd
|
|
89
95
|
frontier << block
|
90
96
|
end
|
91
97
|
|
92
|
-
# Removes the block without putting it back in the frontier
|
93
|
-
def sweep(block:, name:)
|
94
|
-
record(block: block, name: name)
|
95
|
-
|
96
|
-
block.lines.each(&:mark_invisible)
|
97
|
-
frontier.register_indent_block(block)
|
98
|
-
end
|
99
|
-
|
100
98
|
# Parses the most indented lines into blocks that are marked
|
101
99
|
# and added to the frontier
|
102
|
-
def
|
100
|
+
def create_blocks_from_untracked_lines
|
103
101
|
max_indent = frontier.next_indent_line&.indent
|
104
102
|
|
105
103
|
while (line = frontier.next_indent_line) && (line.indent == max_indent)
|
106
|
-
|
107
104
|
@parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|
|
108
|
-
record(block: block, name: "add")
|
109
|
-
|
110
|
-
block.mark_invisible if block.valid?
|
111
105
|
push(block, name: "add")
|
112
106
|
end
|
113
107
|
end
|
@@ -115,13 +109,12 @@ module DeadEnd
|
|
115
109
|
|
116
110
|
# Given an already existing block in the frontier, expand it to see
|
117
111
|
# if it contains our invalid syntax
|
118
|
-
def
|
112
|
+
def expand_existing
|
119
113
|
block = frontier.pop
|
120
114
|
return unless block
|
121
115
|
|
122
|
-
record(block: block, name: "
|
116
|
+
record(block: block, name: "before-expand")
|
123
117
|
|
124
|
-
# block = block.expand_until_next_boundry
|
125
118
|
block = @block_expand.call(block)
|
126
119
|
push(block, name: "expand")
|
127
120
|
end
|
@@ -132,9 +125,9 @@ module DeadEnd
|
|
132
125
|
@tick += 1
|
133
126
|
|
134
127
|
if frontier.expand?
|
135
|
-
|
128
|
+
expand_existing
|
136
129
|
else
|
137
|
-
|
130
|
+
create_blocks_from_untracked_lines
|
138
131
|
end
|
139
132
|
end
|
140
133
|
|
data/lib/dead_end/core_ext.rb
CHANGED
@@ -1,35 +1,88 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
rescue SyntaxError => e
|
15
|
-
DeadEnd.handle_error(e)
|
16
|
-
end
|
3
|
+
# Allow lazy loading, only load code if/when there's a syntax error
|
4
|
+
autoload :DeadEnd, "dead_end/api"
|
5
|
+
|
6
|
+
# Ruby 3.2+ has a cleaner way to hook into Ruby that doesn't use `require`
|
7
|
+
if SyntaxError.new.respond_to?(:detailed_message)
|
8
|
+
module DeadEndUnloaded
|
9
|
+
class MiniStringIO
|
10
|
+
def initialize(isatty: $stderr.isatty)
|
11
|
+
@string = +""
|
12
|
+
@isatty = isatty
|
13
|
+
end
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
attr_reader :isatty
|
16
|
+
def puts(value = $/, **)
|
17
|
+
@string << value
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :string
|
21
|
+
end
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
SyntaxError.prepend Module.new {
|
25
|
+
def detailed_message(highlight: nil, **)
|
26
|
+
message = super
|
27
|
+
file = DeadEnd::PathnameFromMessage.new(message).call.name
|
28
|
+
io = DeadEndUnloaded::MiniStringIO.new
|
29
|
+
|
30
|
+
if file
|
31
|
+
DeadEnd.call(
|
32
|
+
io: io,
|
33
|
+
source: file.read,
|
34
|
+
filename: file
|
35
|
+
)
|
36
|
+
annotation = io.string
|
37
|
+
|
38
|
+
annotation + message
|
39
|
+
else
|
40
|
+
message
|
41
|
+
end
|
42
|
+
rescue => e
|
43
|
+
if ENV["DEBUG"]
|
44
|
+
$stderr.warn(e.message)
|
45
|
+
$stderr.warn(e.backtrace)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Ignore internal errors
|
49
|
+
message
|
50
|
+
end
|
51
|
+
}
|
52
|
+
else
|
53
|
+
autoload :Pathname, "pathname"
|
54
|
+
|
55
|
+
# Monkey patch kernel to ensure that all `require` calls call the same
|
56
|
+
# method
|
57
|
+
module Kernel
|
58
|
+
module_function
|
59
|
+
|
60
|
+
alias_method :dead_end_original_require, :require
|
61
|
+
alias_method :dead_end_original_require_relative, :require_relative
|
62
|
+
alias_method :dead_end_original_load, :load
|
63
|
+
|
64
|
+
def load(file, wrap = false)
|
65
|
+
dead_end_original_load(file)
|
66
|
+
rescue SyntaxError => e
|
67
|
+
DeadEnd.handle_error(e)
|
68
|
+
end
|
69
|
+
|
70
|
+
def require(file)
|
71
|
+
dead_end_original_require(file)
|
72
|
+
rescue SyntaxError => e
|
73
|
+
DeadEnd.handle_error(e)
|
74
|
+
end
|
75
|
+
|
76
|
+
def require_relative(file)
|
77
|
+
if Pathname.new(file).absolute?
|
78
|
+
dead_end_original_require file
|
79
|
+
else
|
80
|
+
relative_from = caller_locations(1..1).first
|
81
|
+
relative_from_path = relative_from.absolute_path || relative_from.path
|
82
|
+
dead_end_original_require File.expand_path("../#{file}", relative_from_path)
|
83
|
+
end
|
84
|
+
rescue SyntaxError => e
|
85
|
+
DeadEnd.handle_error(e)
|
31
86
|
end
|
32
|
-
rescue SyntaxError => e
|
33
|
-
DeadEnd.handle_error(e)
|
34
87
|
end
|
35
88
|
end
|
@@ -42,13 +42,18 @@ module DeadEnd
|
|
42
42
|
|
43
43
|
neighbors = scan.code_block.lines
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
block = CodeBlock.new(lines: neighbors)
|
46
|
+
if neighbors.length <= 2 || block.valid?
|
47
|
+
yield block
|
48
|
+
else
|
49
|
+
until neighbors.empty?
|
50
|
+
lines = [neighbors.pop]
|
51
|
+
while (block = CodeBlock.new(lines: lines)) && block.invalid? && neighbors.any?
|
52
|
+
lines.prepend neighbors.pop
|
53
|
+
end
|
50
54
|
|
51
|
-
|
55
|
+
yield block if block
|
56
|
+
end
|
52
57
|
end
|
53
58
|
end
|
54
59
|
end
|
@@ -13,6 +13,8 @@ module DeadEnd
|
|
13
13
|
# # => "/tmp/scratch.rb"
|
14
14
|
#
|
15
15
|
class PathnameFromMessage
|
16
|
+
EVAL_RE = /^\(eval\):\d+/
|
17
|
+
STREAMING_RE = /^-:\d+/
|
16
18
|
attr_reader :name
|
17
19
|
|
18
20
|
def initialize(message, io: $stderr)
|
@@ -24,14 +26,20 @@ module DeadEnd
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def call
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
if skip_missing_file_name?
|
30
|
+
if ENV["DEBUG"]
|
31
|
+
@io.puts "DeadEnd: Could not find filename from #{@line.inspect}"
|
32
|
+
end
|
33
|
+
else
|
34
|
+
until stop?
|
35
|
+
@guess << @parts.shift
|
36
|
+
@name = Pathname(@guess.join(":"))
|
37
|
+
end
|
31
38
|
|
32
|
-
|
33
|
-
|
34
|
-
|
39
|
+
if @parts.empty?
|
40
|
+
@io.puts "DeadEnd: Could not find filename from #{@line.inspect}"
|
41
|
+
@name = nil
|
42
|
+
end
|
35
43
|
end
|
36
44
|
|
37
45
|
self
|
@@ -43,5 +51,9 @@ module DeadEnd
|
|
43
51
|
|
44
52
|
@name&.exist?
|
45
53
|
end
|
54
|
+
|
55
|
+
def skip_missing_file_name?
|
56
|
+
@line.match?(EVAL_RE) || @line.match?(STREAMING_RE)
|
57
|
+
end
|
46
58
|
end
|
47
59
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Keeps track of what elements are in the queue in
|
5
|
+
# priority and also ensures that when one element
|
6
|
+
# engulfs/covers/eats another that the larger element
|
7
|
+
# evicts the smaller element
|
8
|
+
class PriorityEngulfQueue
|
9
|
+
def initialize
|
10
|
+
@queue = PriorityQueue.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_a
|
14
|
+
@queue.to_a
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
@queue.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def length
|
22
|
+
@queue.length
|
23
|
+
end
|
24
|
+
|
25
|
+
def peek
|
26
|
+
@queue.peek
|
27
|
+
end
|
28
|
+
|
29
|
+
def pop
|
30
|
+
@queue.pop
|
31
|
+
end
|
32
|
+
|
33
|
+
def push(block)
|
34
|
+
prune_engulf(block)
|
35
|
+
@queue << block
|
36
|
+
flush_deleted
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
private def flush_deleted
|
42
|
+
while @queue&.peek&.deleted?
|
43
|
+
@queue.pop
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private def prune_engulf(block)
|
48
|
+
# If we're about to pop off the same block, we can skip deleting
|
49
|
+
# things from the frontier this iteration since we'll get it
|
50
|
+
# on the next iteration
|
51
|
+
return if @queue.peek && (block <=> @queue.peek) == 1
|
52
|
+
|
53
|
+
if block.starts_at != block.ends_at # A block of size 1 cannot engulf another
|
54
|
+
@queue.to_a.each { |b|
|
55
|
+
if b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
|
56
|
+
b.delete
|
57
|
+
true
|
58
|
+
end
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Holds elements in a priority heap on insert
|
5
|
+
#
|
6
|
+
# Instead of constantly calling `sort!`, put
|
7
|
+
# the element where it belongs the first time
|
8
|
+
# around
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# queue = PriorityQueue.new
|
13
|
+
# queue << 33
|
14
|
+
# queue << 44
|
15
|
+
# queue << 1
|
16
|
+
#
|
17
|
+
# puts queue.peek # => 44
|
18
|
+
#
|
19
|
+
class PriorityQueue
|
20
|
+
attr_reader :elements
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@elements = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def <<(element)
|
27
|
+
@elements << element
|
28
|
+
bubble_up(last_index, element)
|
29
|
+
end
|
30
|
+
|
31
|
+
def pop
|
32
|
+
exchange(0, last_index)
|
33
|
+
max = @elements.pop
|
34
|
+
bubble_down(0)
|
35
|
+
max
|
36
|
+
end
|
37
|
+
|
38
|
+
def length
|
39
|
+
@elements.length
|
40
|
+
end
|
41
|
+
|
42
|
+
def empty?
|
43
|
+
@elements.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
def peek
|
47
|
+
@elements.first
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_a
|
51
|
+
@elements
|
52
|
+
end
|
53
|
+
|
54
|
+
# Used for testing, extremely not performant
|
55
|
+
def sorted
|
56
|
+
out = []
|
57
|
+
elements = @elements.dup
|
58
|
+
while (element = pop)
|
59
|
+
out << element
|
60
|
+
end
|
61
|
+
@elements = elements
|
62
|
+
out.reverse
|
63
|
+
end
|
64
|
+
|
65
|
+
private def last_index
|
66
|
+
@elements.size - 1
|
67
|
+
end
|
68
|
+
|
69
|
+
private def bubble_up(index, element)
|
70
|
+
return if index <= 0
|
71
|
+
|
72
|
+
parent_index = (index - 1) / 2
|
73
|
+
parent = @elements[parent_index]
|
74
|
+
|
75
|
+
return if (parent <=> element) >= 0
|
76
|
+
|
77
|
+
exchange(index, parent_index)
|
78
|
+
bubble_up(parent_index, element)
|
79
|
+
end
|
80
|
+
|
81
|
+
private def bubble_down(index)
|
82
|
+
child_index = (index * 2) + 1
|
83
|
+
|
84
|
+
return if child_index > last_index
|
85
|
+
|
86
|
+
not_the_last_element = child_index < last_index
|
87
|
+
left_element = @elements[child_index]
|
88
|
+
right_element = @elements[child_index + 1]
|
89
|
+
|
90
|
+
child_index += 1 if not_the_last_element && (right_element <=> left_element) == 1
|
91
|
+
|
92
|
+
return if (@elements[index] <=> @elements[child_index]) >= 0
|
93
|
+
|
94
|
+
exchange(index, child_index)
|
95
|
+
bubble_down(child_index)
|
96
|
+
end
|
97
|
+
|
98
|
+
def exchange(source, target)
|
99
|
+
a = @elements[source]
|
100
|
+
b = @elements[target]
|
101
|
+
@elements[source] = b
|
102
|
+
@elements[target] = a
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Tracks which lines various code blocks have expanded to
|
5
|
+
# and which are still unexplored
|
6
|
+
class UnvisitedLines
|
7
|
+
def initialize(code_lines:)
|
8
|
+
@unvisited = code_lines.sort_by(&:indent_index)
|
9
|
+
@visited_lines = {}
|
10
|
+
@visited_lines.compare_by_identity
|
11
|
+
end
|
12
|
+
|
13
|
+
def empty?
|
14
|
+
@unvisited.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def peek
|
18
|
+
@unvisited.last
|
19
|
+
end
|
20
|
+
|
21
|
+
def pop
|
22
|
+
@unvisited.pop
|
23
|
+
end
|
24
|
+
|
25
|
+
def visit_block(block)
|
26
|
+
block.lines.each do |line|
|
27
|
+
next if @visited_lines[line]
|
28
|
+
@visited_lines[line] = true
|
29
|
+
end
|
30
|
+
|
31
|
+
while @visited_lines[@unvisited.last]
|
32
|
+
@unvisited.pop
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/dead_end/version.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
# Calling `DeadEnd::VERSION` forces an eager load due to
|
4
|
+
# an `autoload` on the `DeadEnd` constant.
|
5
|
+
#
|
6
|
+
# This is used for gemspec access in tests
|
7
|
+
module UnloadedDeadEnd
|
8
|
+
VERSION = "4.0.0"
|
5
9
|
end
|
data/lib/dead_end.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dead_end
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- schneems
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-06-02 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: When you get an "unexpected end" in your syntax this gem helps you find
|
14
14
|
it
|
@@ -51,13 +51,15 @@ files:
|
|
51
51
|
- lib/dead_end/display_code_with_line_numbers.rb
|
52
52
|
- lib/dead_end/display_invalid_blocks.rb
|
53
53
|
- lib/dead_end/explain_syntax.rb
|
54
|
-
- lib/dead_end/insertion_sort.rb
|
55
54
|
- lib/dead_end/left_right_lex_count.rb
|
56
55
|
- lib/dead_end/lex_all.rb
|
57
56
|
- lib/dead_end/lex_value.rb
|
58
57
|
- lib/dead_end/parse_blocks_from_indent_line.rb
|
59
58
|
- lib/dead_end/pathname_from_message.rb
|
59
|
+
- lib/dead_end/priority_engulf_queue.rb
|
60
|
+
- lib/dead_end/priority_queue.rb
|
60
61
|
- lib/dead_end/ripper_errors.rb
|
62
|
+
- lib/dead_end/unvisited_lines.rb
|
61
63
|
- lib/dead_end/version.rb
|
62
64
|
homepage: https://github.com/zombocom/dead_end.git
|
63
65
|
licenses:
|
@@ -80,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
82
|
- !ruby/object:Gem::Version
|
81
83
|
version: '0'
|
82
84
|
requirements: []
|
83
|
-
rubygems_version: 3.
|
85
|
+
rubygems_version: 3.3.7
|
84
86
|
signing_key:
|
85
87
|
specification_version: 4
|
86
88
|
summary: Find syntax errors in your source in a snap
|
@@ -1,46 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module DeadEnd
|
4
|
-
# Sort elements on insert
|
5
|
-
#
|
6
|
-
# Instead of constantly calling `sort!`, put
|
7
|
-
# the element where it belongs the first time
|
8
|
-
# around
|
9
|
-
#
|
10
|
-
# Example:
|
11
|
-
#
|
12
|
-
# sorted = InsertionSort.new
|
13
|
-
# sorted << 33
|
14
|
-
# sorted << 44
|
15
|
-
# sorted << 1
|
16
|
-
# puts sorted.to_a
|
17
|
-
# # => [1, 44, 33]
|
18
|
-
#
|
19
|
-
class InsertionSort
|
20
|
-
def initialize
|
21
|
-
@array = []
|
22
|
-
end
|
23
|
-
|
24
|
-
def <<(value)
|
25
|
-
insert_in = @array.length
|
26
|
-
@array.each.with_index do |existing, index|
|
27
|
-
case value <=> existing
|
28
|
-
when -1
|
29
|
-
insert_in = index
|
30
|
-
break
|
31
|
-
when 0
|
32
|
-
insert_in = index
|
33
|
-
break
|
34
|
-
when 1
|
35
|
-
# Keep going
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
@array.insert(insert_in, value)
|
40
|
-
end
|
41
|
-
|
42
|
-
def to_a
|
43
|
-
@array
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|