dead_end 1.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.
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require "optparse"
5
+ require_relative "../lib/dead_end.rb"
6
+
7
+ options = {}
8
+ options[:terminal] = true
9
+ options[:record_dir] = ENV["DEAD_END_RECORD_DIR"]
10
+
11
+ parser = OptionParser.new do |opts|
12
+ opts.banner = <<~EOM
13
+ Usage: dead_end <file> [options]
14
+
15
+ Parses a ruby source file and searches for syntax error(s) such as
16
+ unexpected `end', expecting end-of-input.
17
+
18
+ Example:
19
+
20
+ $ dead_end dog.rb
21
+
22
+ # ...
23
+
24
+ ❯ 10 defdog
25
+ ❯ 15 end
26
+ ❯ 16
27
+
28
+ Env options:
29
+
30
+ DEAD_END_RECORD_DIR=<dir>
31
+
32
+ When enabled, records the steps used to search for a syntax error to the
33
+ given directory
34
+
35
+ Options:
36
+ EOM
37
+
38
+ opts.on("--help", "Help - displays this message") do |v|
39
+ puts opts
40
+ exit
41
+ end
42
+
43
+ opts.on("--record <dir>", "When enabled, records the steps used to search for a syntax error to the given directory") do |v|
44
+ options[:record_dir] = v
45
+ end
46
+
47
+ opts.on("--no-terminal", "Disable terminal highlighting") do |v|
48
+ options[:terminal] = false
49
+ end
50
+ end
51
+ parser.parse!
52
+
53
+ file = ARGV[0]
54
+
55
+ if file.nil? || file.empty?
56
+ # Display help if raw command
57
+ parser.parse! %w[--help]
58
+ end
59
+
60
+ file = Pathname(file)
61
+ options[:record_dir] = "tmp" if ENV["DEBUG"]
62
+
63
+ $stderr.puts "Record dir: #{options[:record_dir]}" if options[:record_dir]
64
+
65
+ DeadEnd.call(
66
+ source: file.read,
67
+ filename: file.expand_path,
68
+ terminal: options[:terminal],
69
+ record_dir: options[:record_dir]
70
+ )
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dead_end/internals"
4
+ require_relative "dead_end/auto"
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ module DeadEnd
4
+ # This class is useful for exploring contents before and after
5
+ # a block
6
+ #
7
+ # It searches above and below the passed in block to match for
8
+ # whatever criteria you give it:
9
+ #
10
+ # Example:
11
+ #
12
+ # def dog
13
+ # puts "bark"
14
+ # puts "bark"
15
+ # end
16
+ #
17
+ # scan = AroundBlockScan.new(
18
+ # code_lines: code_lines
19
+ # block: CodeBlock.new(lines: code_lines[1])
20
+ # )
21
+ #
22
+ # scan.scan_while { true }
23
+ #
24
+ # puts scan.before_index # => 0
25
+ # puts scan.after_index # => 3
26
+ #
27
+ # Contents can also be filtered using AroundBlockScan#skip
28
+ #
29
+ # To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent
30
+ class AroundBlockScan
31
+ def initialize(code_lines: , block:)
32
+ @code_lines = code_lines
33
+ @orig_before_index = block.lines.first.index
34
+ @orig_after_index = block.lines.last.index
35
+ @orig_indent = block.current_indent
36
+ @skip_array = []
37
+ @after_array = []
38
+ @before_array = []
39
+ @stop_after_kw = false
40
+ end
41
+
42
+ def skip(name)
43
+ @skip_array << name
44
+ self
45
+ end
46
+
47
+ def stop_after_kw
48
+ @stop_after_kw = true
49
+ self
50
+ end
51
+
52
+ def scan_while(&block)
53
+ stop_next = false
54
+
55
+ kw_count = 0
56
+ end_count = 0
57
+ @before_index = before_lines.reverse_each.take_while do |line|
58
+ next false if stop_next
59
+ next true if @skip_array.detect {|meth| line.send(meth) }
60
+
61
+ kw_count += 1 if line.is_kw?
62
+ end_count += 1 if line.is_end?
63
+ if @stop_after_kw && kw_count > end_count
64
+ stop_next = true
65
+ end
66
+
67
+ block.call(line)
68
+ end.reverse.first&.index
69
+
70
+ stop_next = false
71
+ kw_count = 0
72
+ end_count = 0
73
+ @after_index = after_lines.take_while do |line|
74
+ next false if stop_next
75
+ next true if @skip_array.detect {|meth| line.send(meth) }
76
+
77
+ kw_count += 1 if line.is_kw?
78
+ end_count += 1 if line.is_end?
79
+ if @stop_after_kw && end_count > kw_count
80
+ stop_next = true
81
+ end
82
+
83
+ block.call(line)
84
+ end.last&.index
85
+ self
86
+ end
87
+
88
+ def capture_neighbor_context
89
+ lines = []
90
+ kw_count = 0
91
+ end_count = 0
92
+ before_lines.reverse.each do |line|
93
+ next if line.empty?
94
+ break if line.indent < @orig_indent
95
+ next if line.indent != @orig_indent
96
+
97
+ kw_count += 1 if line.is_kw?
98
+ end_count += 1 if line.is_end?
99
+ if kw_count != 0 && kw_count == end_count
100
+ lines << line
101
+ break
102
+ end
103
+
104
+ lines << line
105
+ end
106
+
107
+ lines.reverse!
108
+
109
+ kw_count = 0
110
+ end_count = 0
111
+ after_lines.each do |line|
112
+ # puts "line: #{line.number} #{line.original_line}, indent: #{line.indent}, #{line.empty?} #{line.indent == @orig_indent}"
113
+
114
+ next if line.empty?
115
+ break if line.indent < @orig_indent
116
+ next if line.indent != @orig_indent
117
+
118
+ kw_count += 1 if line.is_kw?
119
+ end_count += 1 if line.is_end?
120
+ if kw_count != 0 && kw_count == end_count
121
+ lines << line
122
+ break
123
+ end
124
+
125
+ lines << line
126
+ end
127
+ lines.select! {|line| !line.is_comment? }
128
+
129
+ lines
130
+ end
131
+
132
+ def on_falling_indent
133
+ last_indent = @orig_indent
134
+ before_lines.reverse.each do |line|
135
+ next if line.empty?
136
+ if line.indent < last_indent
137
+ yield line
138
+ last_indent = line.indent
139
+ end
140
+ end
141
+
142
+ last_indent = @orig_indent
143
+ after_lines.each do |line|
144
+ next if line.empty?
145
+ if line.indent < last_indent
146
+ yield line
147
+ last_indent = line.indent
148
+ end
149
+ end
150
+ end
151
+
152
+ def scan_neighbors
153
+ self.scan_while {|line| line.not_empty? && line.indent >= @orig_indent }
154
+ end
155
+
156
+ def scan_adjacent_indent
157
+ before_indent = @code_lines[@orig_before_index.pred]&.indent || 0
158
+ after_indent = @code_lines[@orig_after_index.next]&.indent || 0
159
+
160
+ indent = [before_indent, after_indent].min
161
+ self.scan_while {|line| line.not_empty? && line.indent >= indent }
162
+
163
+ self
164
+ end
165
+
166
+ def start_at_next_line
167
+ before_index; after_index
168
+ @before_index -= 1
169
+ @after_index += 1
170
+ self
171
+ end
172
+
173
+ def code_block
174
+ CodeBlock.new(lines: @code_lines[before_index..after_index])
175
+ end
176
+
177
+ def before_index
178
+ @before_index ||= @orig_before_index
179
+ end
180
+
181
+ def after_index
182
+ @after_index ||= @orig_after_index
183
+ end
184
+
185
+ private def before_lines
186
+ @code_lines[0...@orig_before_index]
187
+ end
188
+
189
+ private def after_lines
190
+ @code_lines[@orig_after_index.next..-1]
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,51 @@
1
+ require_relative "../dead_end/internals"
2
+
3
+ # Monkey patch kernel to ensure that all `require` calls call the same
4
+ # method
5
+ module Kernel
6
+ alias_method :original_require, :require
7
+ alias_method :original_require_relative, :require_relative
8
+ alias_method :original_load, :load
9
+
10
+ def load(file, wrap = false)
11
+ original_load(file)
12
+ rescue SyntaxError => e
13
+ DeadEnd.handle_error(e)
14
+ end
15
+
16
+ def require(file)
17
+ original_require(file)
18
+ rescue SyntaxError => e
19
+ DeadEnd.handle_error(e)
20
+ end
21
+
22
+ def require_relative(file)
23
+ if Pathname.new(file).absolute?
24
+ original_require file
25
+ else
26
+ original_require File.expand_path("../#{file}", caller_locations(1, 1)[0].absolute_path)
27
+ end
28
+ rescue SyntaxError => e
29
+ DeadEnd.handle_error(e)
30
+ end
31
+ end
32
+
33
+ # I honestly have no idea why this Object delegation is needed
34
+ # I keep staring at bootsnap and it doesn't have to do this
35
+ # is there a bug in their implementation they haven't caught or
36
+ # am I doing something different?
37
+ class Object
38
+ private
39
+ def load(path, wrap = false)
40
+ Kernel.load(path, wrap)
41
+ rescue SyntaxError => e
42
+ DeadEnd.handle_error(e)
43
+ end
44
+
45
+ def require(path)
46
+ Kernel.require(path)
47
+ rescue SyntaxError => e
48
+ DeadEnd.handle_error(e)
49
+ end
50
+ end
51
+
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+ module DeadEnd
3
+ # This class is responsible for taking a code block that exists
4
+ # at a far indentaion and then iteratively increasing the block
5
+ # so that it captures everything within the same indentation block.
6
+ #
7
+ # def dog
8
+ # puts "bow"
9
+ # puts "wow"
10
+ # end
11
+ #
12
+ # block = BlockExpand.new(code_lines: code_lines)
13
+ # .call(CodeBlock.new(lines: code_lines[1]))
14
+ #
15
+ # puts block.to_s
16
+ # # => puts "bow"
17
+ # puts "wow"
18
+ #
19
+ #
20
+ # Once a code block has captured everything at a given indentation level
21
+ # then it will expand to capture surrounding indentation.
22
+ #
23
+ # block = BlockExpand.new(code_lines: code_lines)
24
+ # .call(block)
25
+ #
26
+ # block.to_s
27
+ # # => def dog
28
+ # puts "bow"
29
+ # puts "wow"
30
+ # end
31
+ #
32
+ class BlockExpand
33
+ def initialize(code_lines: )
34
+ @code_lines = code_lines
35
+ end
36
+
37
+ def call(block)
38
+ if (next_block = expand_neighbors(block, grab_empty: true))
39
+ return next_block
40
+ end
41
+
42
+ expand_indent(block)
43
+ end
44
+
45
+ def expand_indent(block)
46
+ block = AroundBlockScan.new(code_lines: @code_lines, block: block)
47
+ .skip(:hidden?)
48
+ .stop_after_kw
49
+ .scan_adjacent_indent
50
+ .code_block
51
+ end
52
+
53
+ def expand_neighbors(block, grab_empty: true)
54
+ scan = AroundBlockScan.new(code_lines: @code_lines, block: block)
55
+ .skip(:hidden?)
56
+ .stop_after_kw
57
+ .scan_neighbors
58
+
59
+ # Slurp up empties
60
+ if grab_empty
61
+ scan = AroundBlockScan.new(code_lines: @code_lines, block: scan.code_block)
62
+ .scan_while {|line| line.empty? || line.hidden? }
63
+ end
64
+
65
+ new_block = scan.code_block
66
+
67
+ if block.lines == new_block.lines
68
+ return nil
69
+ else
70
+ return new_block
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadEnd
4
+
5
+ # Given a block, this method will capture surrounding
6
+ # code to give the user more context for the location of
7
+ # the problem.
8
+ #
9
+ # Return is an array of CodeLines to be rendered.
10
+ #
11
+ # Surrounding code is captured regardless of visible state
12
+ #
13
+ # puts block.to_s # => "def bark"
14
+ #
15
+ # context = CaptureCodeContext.new(
16
+ # blocks: block,
17
+ # code_lines: code_lines
18
+ # )
19
+ #
20
+ # puts context.call.join
21
+ # # =>
22
+ # class Dog
23
+ # def bark
24
+ # end
25
+ #
26
+ class CaptureCodeContext
27
+ attr_reader :code_lines
28
+
29
+ def initialize(blocks: , code_lines:)
30
+ @blocks = Array(blocks)
31
+ @code_lines = code_lines
32
+ @visible_lines = @blocks.map(&:visible_lines).flatten
33
+ @lines_to_output = @visible_lines.dup
34
+ end
35
+
36
+ def call
37
+ @blocks.each do |block|
38
+ around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
39
+ .start_at_next_line
40
+ .capture_neighbor_context
41
+
42
+ around_lines -= block.lines
43
+
44
+ @lines_to_output.concat(around_lines)
45
+
46
+ AroundBlockScan.new(
47
+ block: block,
48
+ code_lines: @code_lines,
49
+ ).on_falling_indent do |line|
50
+ @lines_to_output << line
51
+ end
52
+ end
53
+
54
+ @lines_to_output.select!(&:not_empty?)
55
+ @lines_to_output.select!(&:not_comment?)
56
+ @lines_to_output.uniq!
57
+ @lines_to_output.sort!
58
+
59
+ return @lines_to_output
60
+ end
61
+ end
62
+ end