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.
@@ -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
 
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
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
17
+
18
+ def require(file)
19
+ dead_end_original_require(file)
20
+ rescue SyntaxError => e
21
+ DeadEnd.handle_error(e)
22
+ end
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)
31
+ end
32
+ rescue SyntaxError => e
33
+ DeadEnd.handle_error(e)
34
+ end
35
+ end
@@ -25,7 +25,10 @@ module DeadEnd
25
25
  lineno = @lex.last.pos.first + 1
26
26
  end
27
27
 
28
- @lex.map! { |elem| LexValue.new(elem.pos.first, elem.event, elem.tok, elem.state) }
28
+ last_lex = nil
29
+ @lex.map! { |elem|
30
+ last_lex = LexValue.new(elem.pos.first, elem.event, elem.tok, elem.state, last_lex)
31
+ }
29
32
  end
30
33
 
31
34
  def to_a
@@ -15,19 +15,21 @@ module DeadEnd
15
15
  class LexValue
16
16
  attr_reader :line, :type, :token, :state
17
17
 
18
- def initialize(line, type, token, state)
18
+ def initialize(line, type, token, state, last_lex = nil)
19
19
  @line = line
20
20
  @type = type
21
21
  @token = token
22
22
  @state = state
23
23
 
24
- set_kw_end
24
+ set_kw_end(last_lex)
25
25
  end
26
26
 
27
- private def set_kw_end
27
+ private def set_kw_end(last_lex)
28
28
  @is_end = false
29
29
  @is_kw = false
30
30
  return if type != :on_kw
31
+ #
32
+ return if last_lex && last_lex.fname? # https://github.com/ruby/ruby/commit/776759e300e4659bb7468e2b97c8c2d4359a2953
31
33
 
32
34
  case token
33
35
  when "if", "unless", "while", "until"
@@ -41,6 +43,10 @@ module DeadEnd
41
43
  end
42
44
  end
43
45
 
46
+ def fname?
47
+ state.allbits?(Ripper::EXPR_FNAME)
48
+ end
49
+
44
50
  def ignore_newline?
45
51
  type == :on_ignored_nl
46
52
  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
@@ -30,7 +30,7 @@ module DeadEnd
30
30
  end
31
31
 
32
32
  if @parts.empty?
33
- @io.puts "DeadEnd: could not find filename from #{@line.inspect}"
33
+ @io.puts "DeadEnd: Could not find filename from #{@line.inspect}"
34
34
  @name = nil
35
35
  end
36
36
 
@@ -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,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadEnd
4
- VERSION = "3.0.3"
4
+ VERSION = "3.1.2"
5
5
  end
data/lib/dead_end.rb CHANGED
@@ -1,164 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "dead_end/version"
4
-
5
- require "tmpdir"
6
- require "stringio"
7
- require "pathname"
8
- require "ripper"
9
- require "timeout"
10
-
11
- module DeadEnd
12
- # Used to indicate a default value that cannot
13
- # be confused with another input
14
- DEFAULT_VALUE = Object.new.freeze
15
-
16
- class Error < StandardError; end
17
- TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
18
-
19
- def self.handle_error(e)
20
- file = PathnameFromMessage.new(e.message).call.name
21
- raise e unless file
22
-
23
- $stderr.sync = true
24
-
25
- call(
26
- source: file.read,
27
- filename: file
28
- )
29
-
30
- raise e
31
- end
32
-
33
- def self.record_dir(dir)
34
- time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
35
- dir = Pathname(dir)
36
- symlink = dir.join("last").tap { |path| path.delete if path.exist? }
37
- dir.join(time).tap { |path|
38
- path.mkpath
39
- FileUtils.symlink(path.basename, symlink)
40
- }
41
- end
42
-
43
- def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
44
- search = nil
45
- filename = nil if filename == DEFAULT_VALUE
46
- Timeout.timeout(timeout) do
47
- record_dir ||= ENV["DEBUG"] ? "tmp" : nil
48
- search = CodeSearch.new(source, record_dir: record_dir).call
49
- end
50
-
51
- blocks = search.invalid_blocks
52
- DisplayInvalidBlocks.new(
53
- io: io,
54
- blocks: blocks,
55
- filename: filename,
56
- terminal: terminal,
57
- code_lines: search.code_lines
58
- ).call
59
- rescue Timeout::Error => e
60
- io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
61
- io.puts e.backtrace.first(3).join($/)
62
- end
63
-
64
- # Used for counting spaces
65
- module SpaceCount
66
- def self.indent(string)
67
- string.split(/\S/).first&.length || 0
68
- end
69
- end
70
-
71
- # This will tell you if the `code_lines` would be valid
72
- # if you removed the `without_lines`. In short it's a
73
- # way to detect if we've found the lines with syntax errors
74
- # in our document yet.
75
- #
76
- # code_lines = [
77
- # CodeLine.new(line: "def foo\n", index: 0)
78
- # CodeLine.new(line: " def bar\n", index: 1)
79
- # CodeLine.new(line: "end\n", index: 2)
80
- # ]
81
- #
82
- # DeadEnd.valid_without?(
83
- # without_lines: code_lines[1],
84
- # code_lines: code_lines
85
- # ) # => true
86
- #
87
- # DeadEnd.valid?(code_lines) # => false
88
- def self.valid_without?(without_lines:, code_lines:)
89
- lines = code_lines - Array(without_lines).flatten
90
-
91
- if lines.empty?
92
- true
93
- else
94
- valid?(lines)
95
- end
96
- end
97
-
98
- def self.invalid?(source)
99
- source = source.join if source.is_a?(Array)
100
- source = source.to_s
101
-
102
- Ripper.new(source).tap(&:parse).error?
103
- end
104
-
105
- # Returns truthy if a given input source is valid syntax
106
- #
107
- # DeadEnd.valid?(<<~EOM) # => true
108
- # def foo
109
- # end
110
- # EOM
111
- #
112
- # DeadEnd.valid?(<<~EOM) # => false
113
- # def foo
114
- # def bar # Syntax error here
115
- # end
116
- # EOM
117
- #
118
- # You can also pass in an array of lines and they'll be
119
- # joined before evaluating
120
- #
121
- # DeadEnd.valid?(
122
- # [
123
- # "def foo\n",
124
- # "end\n"
125
- # ]
126
- # ) # => true
127
- #
128
- # DeadEnd.valid?(
129
- # [
130
- # "def foo\n",
131
- # " def bar\n", # Syntax error here
132
- # "end\n"
133
- # ]
134
- # ) # => false
135
- #
136
- # As an FYI the CodeLine class instances respond to `to_s`
137
- # so passing a CodeLine in as an object or as an array
138
- # will convert it to it's code representation.
139
- def self.valid?(source)
140
- !invalid?(source)
141
- end
142
- end
143
-
144
- # Integration
145
- require_relative "dead_end/cli"
146
- require_relative "dead_end/auto"
147
-
148
- # Core logic
149
- require_relative "dead_end/code_search"
150
- require_relative "dead_end/code_frontier"
151
- require_relative "dead_end/explain_syntax"
152
- require_relative "dead_end/clean_document"
153
-
154
- # Helpers
155
- require_relative "dead_end/lex_all"
156
- require_relative "dead_end/code_line"
157
- require_relative "dead_end/code_block"
158
- require_relative "dead_end/block_expand"
159
- require_relative "dead_end/ripper_errors"
160
- require_relative "dead_end/insertion_sort"
161
- require_relative "dead_end/around_block_scan"
162
- require_relative "dead_end/pathname_from_message"
163
- require_relative "dead_end/display_invalid_blocks"
164
- require_relative "dead_end/parse_blocks_from_indent_line"
3
+ require_relative "dead_end/api"
4
+ 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.0.3
4
+ version: 3.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - schneems
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-17 00:00:00.000000000 Z
11
+ date: 2022-05-18 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
@@ -36,6 +36,7 @@ files:
36
36
  - dead_end.gemspec
37
37
  - exe/dead_end
38
38
  - lib/dead_end.rb
39
+ - lib/dead_end/api.rb
39
40
  - lib/dead_end/around_block_scan.rb
40
41
  - lib/dead_end/auto.rb
41
42
  - lib/dead_end/block_expand.rb
@@ -46,16 +47,19 @@ files:
46
47
  - lib/dead_end/code_frontier.rb
47
48
  - lib/dead_end/code_line.rb
48
49
  - lib/dead_end/code_search.rb
50
+ - lib/dead_end/core_ext.rb
49
51
  - lib/dead_end/display_code_with_line_numbers.rb
50
52
  - lib/dead_end/display_invalid_blocks.rb
51
53
  - lib/dead_end/explain_syntax.rb
52
- - lib/dead_end/insertion_sort.rb
53
54
  - lib/dead_end/left_right_lex_count.rb
54
55
  - lib/dead_end/lex_all.rb
55
56
  - lib/dead_end/lex_value.rb
56
57
  - lib/dead_end/parse_blocks_from_indent_line.rb
57
58
  - lib/dead_end/pathname_from_message.rb
59
+ - lib/dead_end/priority_engulf_queue.rb
60
+ - lib/dead_end/priority_queue.rb
58
61
  - lib/dead_end/ripper_errors.rb
62
+ - lib/dead_end/unvisited_lines.rb
59
63
  - lib/dead_end/version.rb
60
64
  homepage: https://github.com/zombocom/dead_end.git
61
65
  licenses:
@@ -78,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
82
  - !ruby/object:Gem::Version
79
83
  version: '0'
80
84
  requirements: []
81
- rubygems_version: 3.2.22
85
+ rubygems_version: 3.3.14
82
86
  signing_key:
83
87
  specification_version: 4
84
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