dead_end 3.0.3 → 3.1.2
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 +25 -1
- data/CHANGELOG.md +15 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -2
- data/README.md +10 -0
- data/dead_end.gemspec +5 -1
- data/lib/dead_end/api.rb +198 -0
- data/lib/dead_end/around_block_scan.rb +34 -10
- data/lib/dead_end/auto.rb +2 -31
- 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 +35 -0
- data/lib/dead_end/lex_all.rb +4 -1
- data/lib/dead_end/lex_value.rb +9 -3
- data/lib/dead_end/parse_blocks_from_indent_line.rb +11 -6
- data/lib/dead_end/pathname_from_message.rb +1 -1
- 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 +1 -1
- data/lib/dead_end.rb +2 -162
- metadata +8 -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: f35aa207ae1ff3904d39f5c0ddca5b8a0f68ae157f5c0a999cd3f2f1530e461b
|
4
|
+
data.tar.gz: b08307eb152fc12b84493409bfd8a9b8f23905180ad5ed9168a58d152913bc6e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02b4ec64732eea9dbb55594c130d2fade4838a855bf3366408a5846683c3d2866e21844c7d3a2797adc865e177e7edf3bffaad0b87f555627f75fcac3d2aa219
|
7
|
+
data.tar.gz: '029add7304c46c97a8a04b564f5798a2e5d43afe24472ae51268226e2dd5e71c56b9c48bf889232d4c3f2759b6edb7ea4c4f332688d6995ddaac0f7ec8ae5803'
|
data/.circleci/config.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
version: 2.1
|
2
2
|
orbs:
|
3
|
-
ruby: circleci/ruby@1.
|
3
|
+
ruby: circleci/ruby@1.2.0
|
4
4
|
references:
|
5
5
|
unit: &unit
|
6
6
|
run:
|
@@ -45,6 +45,28 @@ jobs:
|
|
45
45
|
- ruby/install-deps
|
46
46
|
- <<: *unit
|
47
47
|
|
48
|
+
"ruby-3-1":
|
49
|
+
docker:
|
50
|
+
- image: 'cimg/base:stable'
|
51
|
+
steps:
|
52
|
+
- checkout
|
53
|
+
- ruby/install:
|
54
|
+
version: '3.1.2'
|
55
|
+
- run: ruby -v
|
56
|
+
- ruby/install-deps
|
57
|
+
- <<: *unit
|
58
|
+
|
59
|
+
"ruby-3-2":
|
60
|
+
docker:
|
61
|
+
- image: 'cimg/base:stable'
|
62
|
+
steps:
|
63
|
+
- checkout
|
64
|
+
- ruby/install:
|
65
|
+
version: '3.2.0-preview1'
|
66
|
+
- run: ruby -v
|
67
|
+
- ruby/install-deps
|
68
|
+
- <<: *unit
|
69
|
+
|
48
70
|
"lint":
|
49
71
|
docker:
|
50
72
|
- image: circleci/ruby:3.0
|
@@ -61,4 +83,6 @@ workflows:
|
|
61
83
|
- "ruby-2-6"
|
62
84
|
- "ruby-2-7"
|
63
85
|
- "ruby-3-0"
|
86
|
+
- "ruby-3-1"
|
87
|
+
- "ruby-3-2"
|
64
88
|
- "lint"
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
## HEAD (unreleased)
|
2
2
|
|
3
|
+
## 3.1.2
|
4
|
+
|
5
|
+
- Fixed internal class AroundBlockScan, minor changes in outputs (https://github.com/zombocom/dead_end/pull/131)
|
6
|
+
|
7
|
+
## 3.1.1
|
8
|
+
|
9
|
+
- Fix case where Ripper lexing identified incorrect code as a keyword (https://github.com/zombocom/dead_end/pull/122)
|
10
|
+
|
11
|
+
## 3.1.0
|
12
|
+
|
13
|
+
- Add support for Ruby 3.1 by updating `require_relative` logic (https://github.com/zombocom/dead_end/pull/120)
|
14
|
+
- Requiring `dead_end/auto` is now deprecated please require `dead_end` instead (https://github.com/zombocom/dead_end/pull/119)
|
15
|
+
- Requiring `dead_end/api` now loads code without monkeypatching core extensions (https://github.com/zombocom/dead_end/pull/119)
|
16
|
+
- The interface `DeadEnd.handle_error` is declared public and stable (https://github.com/zombocom/dead_end/pull/119)
|
17
|
+
|
3
18
|
## 3.0.3
|
4
19
|
|
5
20
|
- Expand explanations coming from additional Ripper errors (https://github.com/zombocom/dead_end/pull/117)
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
dead_end (3.
|
4
|
+
dead_end (3.1.2)
|
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.
|
67
|
+
2.3.14
|
data/README.md
CHANGED
@@ -166,6 +166,16 @@ Here's an example:
|
|
166
166
|
|
167
167
|

|
168
168
|
|
169
|
+
## Use internals
|
170
|
+
|
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
|
+
|
173
|
+
Stable internal interface(s):
|
174
|
+
|
175
|
+
- `DeadEnd.handle_error(e)`
|
176
|
+
|
177
|
+
Any other entrypoints are subject to change without warning. If you want to use an internal interface from `dead_end` not on this list, open an issue to explain your use case.
|
178
|
+
|
169
179
|
## Development
|
170
180
|
|
171
181
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/dead_end.gemspec
CHANGED
@@ -1,6 +1,10 @@
|
|
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"
|
data/lib/dead_end/api.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
require_relative "version"
|
2
|
+
|
3
|
+
require "tmpdir"
|
4
|
+
require "stringio"
|
5
|
+
require "pathname"
|
6
|
+
require "ripper"
|
7
|
+
require "timeout"
|
8
|
+
|
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
|
+
|
14
|
+
class Error < StandardError; end
|
15
|
+
TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
|
16
|
+
|
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
|
44
|
+
|
45
|
+
file = PathnameFromMessage.new(e.message, io: io).call.name
|
46
|
+
raise e unless file
|
47
|
+
|
48
|
+
io.sync = true
|
49
|
+
|
50
|
+
call(
|
51
|
+
io: io,
|
52
|
+
source: file.read,
|
53
|
+
filename: file
|
54
|
+
)
|
55
|
+
|
56
|
+
raise e if re_raise
|
57
|
+
end
|
58
|
+
|
59
|
+
# DeadEnd.call [Private]
|
60
|
+
#
|
61
|
+
# Main private interface
|
62
|
+
def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: DEFAULT_VALUE, timeout: TIMEOUT_DEFAULT, io: $stderr)
|
63
|
+
search = nil
|
64
|
+
filename = nil if filename == DEFAULT_VALUE
|
65
|
+
Timeout.timeout(timeout) do
|
66
|
+
record_dir ||= ENV["DEBUG"] ? "tmp" : nil
|
67
|
+
search = CodeSearch.new(source, record_dir: record_dir).call
|
68
|
+
end
|
69
|
+
|
70
|
+
blocks = search.invalid_blocks
|
71
|
+
DisplayInvalidBlocks.new(
|
72
|
+
io: io,
|
73
|
+
blocks: blocks,
|
74
|
+
filename: filename,
|
75
|
+
terminal: terminal,
|
76
|
+
code_lines: search.code_lines
|
77
|
+
).call
|
78
|
+
rescue Timeout::Error => e
|
79
|
+
io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
|
80
|
+
io.puts e.backtrace.first(3).join($/)
|
81
|
+
end
|
82
|
+
|
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
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# DeadEnd.valid_without? [Private]
|
98
|
+
#
|
99
|
+
# This will tell you if the `code_lines` would be valid
|
100
|
+
# if you removed the `without_lines`. In short it's a
|
101
|
+
# way to detect if we've found the lines with syntax errors
|
102
|
+
# in our document yet.
|
103
|
+
#
|
104
|
+
# code_lines = [
|
105
|
+
# CodeLine.new(line: "def foo\n", index: 0)
|
106
|
+
# CodeLine.new(line: " def bar\n", index: 1)
|
107
|
+
# CodeLine.new(line: "end\n", index: 2)
|
108
|
+
# ]
|
109
|
+
#
|
110
|
+
# DeadEnd.valid_without?(
|
111
|
+
# without_lines: code_lines[1],
|
112
|
+
# code_lines: code_lines
|
113
|
+
# ) # => true
|
114
|
+
#
|
115
|
+
# DeadEnd.valid?(code_lines) # => false
|
116
|
+
def self.valid_without?(without_lines:, code_lines:)
|
117
|
+
lines = code_lines - Array(without_lines).flatten
|
118
|
+
|
119
|
+
if lines.empty?
|
120
|
+
true
|
121
|
+
else
|
122
|
+
valid?(lines)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# DeadEnd.invalid? [Private]
|
127
|
+
#
|
128
|
+
# Opposite of `DeadEnd.valid?`
|
129
|
+
def self.invalid?(source)
|
130
|
+
source = source.join if source.is_a?(Array)
|
131
|
+
source = source.to_s
|
132
|
+
|
133
|
+
Ripper.new(source).tap(&:parse).error?
|
134
|
+
end
|
135
|
+
|
136
|
+
# DeadEnd.valid? [Private]
|
137
|
+
#
|
138
|
+
# Returns truthy if a given input source is valid syntax
|
139
|
+
#
|
140
|
+
# DeadEnd.valid?(<<~EOM) # => true
|
141
|
+
# def foo
|
142
|
+
# end
|
143
|
+
# EOM
|
144
|
+
#
|
145
|
+
# DeadEnd.valid?(<<~EOM) # => false
|
146
|
+
# def foo
|
147
|
+
# def bar # Syntax error here
|
148
|
+
# end
|
149
|
+
# EOM
|
150
|
+
#
|
151
|
+
# You can also pass in an array of lines and they'll be
|
152
|
+
# joined before evaluating
|
153
|
+
#
|
154
|
+
# DeadEnd.valid?(
|
155
|
+
# [
|
156
|
+
# "def foo\n",
|
157
|
+
# "end\n"
|
158
|
+
# ]
|
159
|
+
# ) # => true
|
160
|
+
#
|
161
|
+
# DeadEnd.valid?(
|
162
|
+
# [
|
163
|
+
# "def foo\n",
|
164
|
+
# " def bar\n", # Syntax error here
|
165
|
+
# "end\n"
|
166
|
+
# ]
|
167
|
+
# ) # => false
|
168
|
+
#
|
169
|
+
# As an FYI the CodeLine class instances respond to `to_s`
|
170
|
+
# so passing a CodeLine in as an object or as an array
|
171
|
+
# will convert it to it's code representation.
|
172
|
+
def self.valid?(source)
|
173
|
+
!invalid?(source)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Integration
|
178
|
+
require_relative "cli"
|
179
|
+
|
180
|
+
# Core logic
|
181
|
+
require_relative "code_search"
|
182
|
+
require_relative "code_frontier"
|
183
|
+
require_relative "explain_syntax"
|
184
|
+
require_relative "clean_document"
|
185
|
+
|
186
|
+
# Helpers
|
187
|
+
require_relative "lex_all"
|
188
|
+
require_relative "code_line"
|
189
|
+
require_relative "code_block"
|
190
|
+
require_relative "block_expand"
|
191
|
+
require_relative "ripper_errors"
|
192
|
+
require_relative "priority_queue"
|
193
|
+
require_relative "unvisited_lines"
|
194
|
+
require_relative "around_block_scan"
|
195
|
+
require_relative "priority_engulf_queue"
|
196
|
+
require_relative "pathname_from_message"
|
197
|
+
require_relative "display_invalid_blocks"
|
198
|
+
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
|
data/lib/dead_end/auto.rb
CHANGED
@@ -1,35 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "../dead_end"
|
4
|
+
require_relative "core_ext"
|
4
5
|
|
5
|
-
|
6
|
-
# method
|
7
|
-
module Kernel
|
8
|
-
module_function
|
9
|
-
|
10
|
-
alias_method :dead_end_original_require, :require
|
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
|
6
|
+
warn "Calling `require 'dead_end/auto'` is deprecated, please `require 'dead_end'` instead."
|
@@ -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
|