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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64abeaee0636d3ded8df0f0dc124674a70f954287b37302dff7151f04b746557
4
- data.tar.gz: 02e6eea73a9e9e832b898f9294c1ce3f7f38f9b8996554a8f6527abf7027d115
3
+ metadata.gz: 655326e21a4ad4ae77a34020b69ce404f2405b2695b2e2e977c685352fcc0e79
4
+ data.tar.gz: 4a85b498c7d039a1ab016dae09775cb7a0a456c02a234ce83c5ac5776af97f0f
5
5
  SHA512:
6
- metadata.gz: e7381458f44ef92f5053ac3171ac8bc59e89a805d793dde1c48ce1eda855dcea624ef6dc7a1faa3da9c20abc53983675fc66a7b08e3e02893801b4b4ff0edf65
7
- data.tar.gz: 8b92dee56fac32777f31b76e25a0b778b2a4ffa1ff6914f24ef835bbf5ab79898f4a3a5607a9bebc641f86b05f2a88224504f0f8aaab83c3262b1d067ea32af0
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/base:stable'
50
+ - image: 'cimg/ruby:3.1'
51
51
  steps:
52
52
  - checkout
53
- - ruby/install:
54
- version: '3.1.0-preview1'
55
- - run: ruby -v
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
@@ -10,3 +10,5 @@ gem "rspec", "~> 3.0"
10
10
  gem "stackprof"
11
11
  gem "standard"
12
12
  gem "ruby-prof"
13
+
14
+ gem "benchmark-ips"
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dead_end (3.1.1)
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.4
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 'dead_en/api'`. This will allow you to load `dead_end` and use its internals without mutating `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
- require_relative "lib/dead_end/version"
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 = DeadEnd::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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative "../lib/dead_end"
3
+ require_relative "../lib/dead_end/api"
4
4
 
5
5
  DeadEnd::Cli.new(
6
6
  argv: ARGV
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`` exception, uses the
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: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
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 "insertion_sort"
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
- @skip_array << name
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(&block)
62
+ def scan_while
53
63
  stop_next = false
54
64
 
55
65
  kw_count = 0
56
66
  end_count = 0
57
- @before_index = before_lines.reverse_each.take_while do |line|
67
+ index = before_lines.reverse_each.take_while do |line|
58
68
  next false if stop_next
59
- next true if @skip_array.detect { |meth| line.send(meth) }
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
- block.call(line)
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
- @after_index = after_lines.take_while do |line|
88
+ index = after_lines.take_while do |line|
74
89
  next false if stop_next
75
- next true if @skip_array.detect { |meth| line.send(meth) }
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
- block.call(line)
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: @code_lines[before_index..after_index])
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...@orig_before_index] || []
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, grab_empty: true))
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, grab_empty: true)
55
- scan = AroundBlockScan.new(code_lines: @code_lines, block: block)
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
- # Slurp up empties
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
- new_block
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
@@ -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
- @frontier = InsertionSort.new
56
- @unvisited_lines = @code_lines.sort_by(&:indent_index)
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
- @frontier.to_a.length
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 = @frontier, can_cache: true)
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
- @frontier.to_a.pop
104
+ @queue.pop
107
105
  end
108
106
 
109
107
  def next_indent_line
110
- @unvisited_lines.last
108
+ @unvisited.peek
111
109
  end
112
110
 
113
111
  def expand?
114
- return false if @frontier.to_a.empty?
115
- return true if @unvisited_lines.to_a.empty?
112
+ return false if @queue.empty?
113
+ return true if @unvisited.empty?
116
114
 
117
- frontier_indent = @frontier.to_a.last.current_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 @frontier.to_a.last.to_s
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.lines.each do |line|
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
- register_indent_block(block)
149
+ @unvisited.visit_block(block)
150
150
 
151
- # Make sure we don't double expand, if a code block fully engulfs another code block, keep the bigger one
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(@frontier.to_a.select(&:invalid?)).detect do |block_array|
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
@@ -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: ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
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 visit_new_blocks
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 expand_invalid_block
112
+ def expand_existing
119
113
  block = frontier.pop
120
114
  return unless block
121
115
 
122
- record(block: block, name: "pop")
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
- expand_invalid_block
128
+ expand_existing
136
129
  else
137
- visit_new_blocks
130
+ create_blocks_from_untracked_lines
138
131
  end
139
132
  end
140
133
 
@@ -1,35 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Monkey patch kernel to ensure that all `require` calls call the same
4
- # method
5
- module Kernel
6
- module_function
7
-
8
- alias_method :dead_end_original_require, :require
9
- alias_method :dead_end_original_require_relative, :require_relative
10
- alias_method :dead_end_original_load, :load
11
-
12
- def load(file, wrap = false)
13
- dead_end_original_load(file)
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
- def require(file)
19
- dead_end_original_require(file)
20
- rescue SyntaxError => e
21
- DeadEnd.handle_error(e)
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
- def require_relative(file)
25
- if Pathname.new(file).absolute?
26
- dead_end_original_require file
27
- else
28
- relative_from = caller_locations(1..1).first
29
- relative_from_path = relative_from.absolute_path || relative_from.path
30
- dead_end_original_require File.expand_path("../#{file}", relative_from_path)
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
- until neighbors.empty?
46
- lines = [neighbors.pop]
47
- while (block = CodeBlock.new(lines: lines)) && block.invalid? && neighbors.any?
48
- lines.prepend neighbors.pop
49
- end
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
- yield block if block
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
- until stop?
28
- @guess << @parts.shift
29
- @name = Pathname(@guess.join(":"))
30
- end
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
- if @parts.empty?
33
- @io.puts "DeadEnd: Could not find filename from #{@line.inspect}"
34
- @name = nil
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
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DeadEnd
4
- VERSION = "3.1.1"
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
@@ -1,4 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "dead_end/api"
4
3
  require_relative "dead_end/core_ext"
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: 3.1.1
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-01-04 00:00:00.000000000 Z
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.2.32
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