dead_end 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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