dead_end 2.0.1 → 3.0.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/.github/workflows/check_changelog.yml +14 -7
- data/CHANGELOG.md +24 -0
- data/CODE_OF_CONDUCT.md +2 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -2
- data/README.md +118 -23
- data/exe/dead_end +3 -74
- data/lib/dead_end/auto.rb +1 -21
- data/lib/dead_end/clean_document.rb +21 -30
- data/lib/dead_end/cli.rb +129 -0
- data/lib/dead_end/code_block.rb +18 -2
- data/lib/dead_end/code_frontier.rb +48 -14
- data/lib/dead_end/code_line.rb +27 -21
- data/lib/dead_end/code_search.rb +7 -7
- data/lib/dead_end/display_invalid_blocks.rb +37 -45
- data/lib/dead_end/explain_syntax.rb +103 -0
- data/lib/dead_end/insertion_sort.rb +46 -0
- data/lib/dead_end/left_right_lex_count.rb +157 -0
- data/lib/dead_end/lex_all.rb +12 -8
- data/lib/dead_end/lex_value.rb +2 -0
- data/lib/dead_end/pathname_from_message.rb +47 -0
- data/lib/dead_end/ripper_errors.rb +30 -0
- data/lib/dead_end/version.rb +1 -1
- data/lib/dead_end.rb +161 -1
- metadata +8 -6
- data/lib/dead_end/banner.rb +0 -58
- data/lib/dead_end/fyi.rb +0 -8
- data/lib/dead_end/internals.rb +0 -154
- data/lib/dead_end/who_dis_syntax_error.rb +0 -83
data/lib/dead_end/lex_all.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DeadEnd
|
2
4
|
# Ripper.lex is not guaranteed to lex the entire source document
|
3
5
|
#
|
@@ -8,20 +10,22 @@ module DeadEnd
|
|
8
10
|
class LexAll
|
9
11
|
include Enumerable
|
10
12
|
|
11
|
-
def initialize(source:)
|
12
|
-
@lex = Ripper.
|
13
|
-
lineno = @lex.last.
|
14
|
-
source_lines
|
15
|
-
last_lineno = source_lines.
|
13
|
+
def initialize(source:, source_lines: nil)
|
14
|
+
@lex = Ripper::Lexer.new(source, "-", 1).parse.sort_by(&:pos)
|
15
|
+
lineno = @lex.last.pos.first + 1
|
16
|
+
source_lines ||= source.lines
|
17
|
+
last_lineno = source_lines.length
|
16
18
|
|
17
19
|
until lineno >= last_lineno
|
18
20
|
lines = source_lines[lineno..-1]
|
19
21
|
|
20
|
-
@lex.concat(
|
21
|
-
|
22
|
+
@lex.concat(
|
23
|
+
Ripper::Lexer.new(lines.join, "-", lineno + 1).parse.sort_by(&:pos)
|
24
|
+
)
|
25
|
+
lineno = @lex.last.pos.first + 1
|
22
26
|
end
|
23
27
|
|
24
|
-
@lex.map! { |
|
28
|
+
@lex.map! { |elem| LexValue.new(elem.pos.first, elem.event, elem.tok, elem.state) }
|
25
29
|
end
|
26
30
|
|
27
31
|
def to_a
|
data/lib/dead_end/lex_value.rb
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Converts a SyntaxError message to a path
|
5
|
+
#
|
6
|
+
# Handles the case where the filename has a colon in it
|
7
|
+
# such as on a windows file system: https://github.com/zombocom/dead_end/issues/111
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# message = "/tmp/scratch:2:in `require_relative': /private/tmp/bad.rb:1: syntax error, unexpected `end' (SyntaxError)"
|
12
|
+
# puts PathnameFromMessage.new(message).call.name
|
13
|
+
# # => "/tmp/scratch.rb"
|
14
|
+
#
|
15
|
+
class PathnameFromMessage
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
def initialize(message, io: $stderr)
|
19
|
+
@line = message.lines.first
|
20
|
+
@parts = @line.split(":")
|
21
|
+
@guess = []
|
22
|
+
@name = nil
|
23
|
+
@io = io
|
24
|
+
end
|
25
|
+
|
26
|
+
def call
|
27
|
+
until stop?
|
28
|
+
@guess << @parts.shift
|
29
|
+
@name = Pathname(@guess.join(":"))
|
30
|
+
end
|
31
|
+
|
32
|
+
if @parts.empty?
|
33
|
+
@io.puts "DeadEnd: could not find filename from #{@line.inspect}"
|
34
|
+
@name = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop?
|
41
|
+
return true if @parts.empty?
|
42
|
+
return false if @guess.empty?
|
43
|
+
|
44
|
+
@name&.exist?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeadEnd
|
4
|
+
# Capture parse errors from ripper
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# puts RipperErrors.new(" def foo").call.errors
|
9
|
+
# # => ["syntax error, unexpected end-of-input, expecting ';' or '\\n'"]
|
10
|
+
class RipperErrors < Ripper
|
11
|
+
attr_reader :errors
|
12
|
+
|
13
|
+
# Comes from ripper, called
|
14
|
+
# on every parse error, msg
|
15
|
+
# is a string
|
16
|
+
def on_parse_error(msg)
|
17
|
+
@errors ||= []
|
18
|
+
@errors << msg
|
19
|
+
end
|
20
|
+
|
21
|
+
def call
|
22
|
+
@run_once ||= begin
|
23
|
+
@errors = []
|
24
|
+
parse
|
25
|
+
true
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/dead_end/version.rb
CHANGED
data/lib/dead_end.rb
CHANGED
@@ -1,4 +1,164 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "dead_end/
|
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"
|
4
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"
|
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:
|
4
|
+
version: 3.0.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
|
+
date: 2021-11-12 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
|
@@ -38,23 +38,25 @@ files:
|
|
38
38
|
- lib/dead_end.rb
|
39
39
|
- lib/dead_end/around_block_scan.rb
|
40
40
|
- lib/dead_end/auto.rb
|
41
|
-
- lib/dead_end/banner.rb
|
42
41
|
- lib/dead_end/block_expand.rb
|
43
42
|
- lib/dead_end/capture_code_context.rb
|
44
43
|
- lib/dead_end/clean_document.rb
|
44
|
+
- lib/dead_end/cli.rb
|
45
45
|
- lib/dead_end/code_block.rb
|
46
46
|
- lib/dead_end/code_frontier.rb
|
47
47
|
- lib/dead_end/code_line.rb
|
48
48
|
- lib/dead_end/code_search.rb
|
49
49
|
- lib/dead_end/display_code_with_line_numbers.rb
|
50
50
|
- lib/dead_end/display_invalid_blocks.rb
|
51
|
-
- lib/dead_end/
|
52
|
-
- lib/dead_end/
|
51
|
+
- lib/dead_end/explain_syntax.rb
|
52
|
+
- lib/dead_end/insertion_sort.rb
|
53
|
+
- lib/dead_end/left_right_lex_count.rb
|
53
54
|
- lib/dead_end/lex_all.rb
|
54
55
|
- lib/dead_end/lex_value.rb
|
55
56
|
- lib/dead_end/parse_blocks_from_indent_line.rb
|
57
|
+
- lib/dead_end/pathname_from_message.rb
|
58
|
+
- lib/dead_end/ripper_errors.rb
|
56
59
|
- lib/dead_end/version.rb
|
57
|
-
- lib/dead_end/who_dis_syntax_error.rb
|
58
60
|
homepage: https://github.com/zombocom/dead_end.git
|
59
61
|
licenses:
|
60
62
|
- MIT
|
data/lib/dead_end/banner.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module DeadEnd
|
4
|
-
class Banner
|
5
|
-
attr_reader :invalid_obj
|
6
|
-
|
7
|
-
def initialize(invalid_obj:)
|
8
|
-
@invalid_obj = invalid_obj
|
9
|
-
end
|
10
|
-
|
11
|
-
def call
|
12
|
-
case invalid_obj.error_symbol
|
13
|
-
when :missing_end
|
14
|
-
<<~EOM
|
15
|
-
DeadEnd: Missing `end` detected
|
16
|
-
|
17
|
-
This code has a missing `end`. Ensure that all
|
18
|
-
syntax keywords (`def`, `do`, etc.) have a matching `end`.
|
19
|
-
EOM
|
20
|
-
when :unmatched_syntax
|
21
|
-
case unmatched_symbol
|
22
|
-
when :end
|
23
|
-
<<~EOM
|
24
|
-
DeadEnd: Unmatched `end` detected
|
25
|
-
|
26
|
-
This code has an unmatched `end`. Ensure that all `end` lines
|
27
|
-
in your code have a matching syntax keyword (`def`, `do`, etc.)
|
28
|
-
and that you don't have any extra `end` lines.
|
29
|
-
EOM
|
30
|
-
when :|
|
31
|
-
<<~EOM
|
32
|
-
DeadEnd: Unmatched `|` character detected
|
33
|
-
|
34
|
-
Example:
|
35
|
-
|
36
|
-
`do |x` should be `do |x|`
|
37
|
-
EOM
|
38
|
-
when *WhoDisSyntaxError::CHARACTERS.keys
|
39
|
-
<<~EOM
|
40
|
-
DeadEnd: Unmatched `#{unmatched_symbol}` character detected
|
41
|
-
|
42
|
-
It appears a `#{missing_character}` might be missing.
|
43
|
-
EOM
|
44
|
-
else
|
45
|
-
"DeadEnd: Unmatched `#{unmatched_symbol}` detected"
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
private def unmatched_symbol
|
51
|
-
invalid_obj.unmatched_symbol
|
52
|
-
end
|
53
|
-
|
54
|
-
private def missing_character
|
55
|
-
WhoDisSyntaxError::CHARACTERS[unmatched_symbol]
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
data/lib/dead_end/fyi.rb
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
require_relative "../dead_end/internals"
|
2
|
-
|
3
|
-
require_relative "auto"
|
4
|
-
|
5
|
-
DeadEnd.send(:remove_const, :SEARCH_SOURCE_ON_ERROR_DEFAULT)
|
6
|
-
DeadEnd::SEARCH_SOURCE_ON_ERROR_DEFAULT = false
|
7
|
-
|
8
|
-
warn "DEPRECATED: calling `require 'dead_end/fyi'` is deprecated, `require 'dead_end'` instead"
|
data/lib/dead_end/internals.rb
DELETED
@@ -1,154 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# This is the top level file, but is moved to `internals`
|
4
|
-
# so the top level require can instead enable the "automatic" behavior
|
5
|
-
|
6
|
-
require_relative "version"
|
7
|
-
|
8
|
-
require "tmpdir"
|
9
|
-
require "stringio"
|
10
|
-
require "pathname"
|
11
|
-
require "ripper"
|
12
|
-
require "timeout"
|
13
|
-
|
14
|
-
module DeadEnd
|
15
|
-
class Error < StandardError; end
|
16
|
-
SEARCH_SOURCE_ON_ERROR_DEFAULT = true
|
17
|
-
TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
|
18
|
-
|
19
|
-
def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT)
|
20
|
-
raise e unless e.message.include?("end-of-input")
|
21
|
-
|
22
|
-
filename = e.message.split(":").first
|
23
|
-
|
24
|
-
$stderr.sync = true
|
25
|
-
warn "Run `$ dead_end #{filename}` for more options\n"
|
26
|
-
|
27
|
-
if search_source_on_error
|
28
|
-
call(
|
29
|
-
source: Pathname(filename).read,
|
30
|
-
filename: filename,
|
31
|
-
terminal: true
|
32
|
-
)
|
33
|
-
end
|
34
|
-
|
35
|
-
raise e
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.call(source:, filename:, terminal: false, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
|
39
|
-
search = nil
|
40
|
-
Timeout.timeout(timeout) do
|
41
|
-
record_dir ||= ENV["DEBUG"] ? "tmp" : nil
|
42
|
-
search = CodeSearch.new(source, record_dir: record_dir).call
|
43
|
-
end
|
44
|
-
|
45
|
-
blocks = search.invalid_blocks
|
46
|
-
DisplayInvalidBlocks.new(
|
47
|
-
blocks: blocks,
|
48
|
-
filename: filename,
|
49
|
-
terminal: terminal,
|
50
|
-
code_lines: search.code_lines,
|
51
|
-
invalid_obj: invalid_type(source),
|
52
|
-
io: io
|
53
|
-
).call
|
54
|
-
rescue Timeout::Error => e
|
55
|
-
io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
|
56
|
-
io.puts e.backtrace.first(3).join($/)
|
57
|
-
end
|
58
|
-
|
59
|
-
# Used for counting spaces
|
60
|
-
module SpaceCount
|
61
|
-
def self.indent(string)
|
62
|
-
string.split(/\S/).first&.length || 0
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
# This will tell you if the `code_lines` would be valid
|
67
|
-
# if you removed the `without_lines`. In short it's a
|
68
|
-
# way to detect if we've found the lines with syntax errors
|
69
|
-
# in our document yet.
|
70
|
-
#
|
71
|
-
# code_lines = [
|
72
|
-
# CodeLine.new(line: "def foo\n", index: 0)
|
73
|
-
# CodeLine.new(line: " def bar\n", index: 1)
|
74
|
-
# CodeLine.new(line: "end\n", index: 2)
|
75
|
-
# ]
|
76
|
-
#
|
77
|
-
# DeadEnd.valid_without?(
|
78
|
-
# without_lines: code_lines[1],
|
79
|
-
# code_lines: code_lines
|
80
|
-
# ) # => true
|
81
|
-
#
|
82
|
-
# DeadEnd.valid?(code_lines) # => false
|
83
|
-
def self.valid_without?(without_lines:, code_lines:)
|
84
|
-
lines = code_lines - Array(without_lines).flatten
|
85
|
-
|
86
|
-
if lines.empty?
|
87
|
-
true
|
88
|
-
else
|
89
|
-
valid?(lines)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def self.invalid?(source)
|
94
|
-
source = source.join if source.is_a?(Array)
|
95
|
-
source = source.to_s
|
96
|
-
|
97
|
-
Ripper.new(source).tap(&:parse).error?
|
98
|
-
end
|
99
|
-
|
100
|
-
# Returns truthy if a given input source is valid syntax
|
101
|
-
#
|
102
|
-
# DeadEnd.valid?(<<~EOM) # => true
|
103
|
-
# def foo
|
104
|
-
# end
|
105
|
-
# EOM
|
106
|
-
#
|
107
|
-
# DeadEnd.valid?(<<~EOM) # => false
|
108
|
-
# def foo
|
109
|
-
# def bar # Syntax error here
|
110
|
-
# end
|
111
|
-
# EOM
|
112
|
-
#
|
113
|
-
# You can also pass in an array of lines and they'll be
|
114
|
-
# joined before evaluating
|
115
|
-
#
|
116
|
-
# DeadEnd.valid?(
|
117
|
-
# [
|
118
|
-
# "def foo\n",
|
119
|
-
# "end\n"
|
120
|
-
# ]
|
121
|
-
# ) # => true
|
122
|
-
#
|
123
|
-
# DeadEnd.valid?(
|
124
|
-
# [
|
125
|
-
# "def foo\n",
|
126
|
-
# " def bar\n", # Syntax error here
|
127
|
-
# "end\n"
|
128
|
-
# ]
|
129
|
-
# ) # => false
|
130
|
-
#
|
131
|
-
# As an FYI the CodeLine class instances respond to `to_s`
|
132
|
-
# so passing a CodeLine in as an object or as an array
|
133
|
-
# will convert it to it's code representation.
|
134
|
-
def self.valid?(source)
|
135
|
-
!invalid?(source)
|
136
|
-
end
|
137
|
-
|
138
|
-
def self.invalid_type(source)
|
139
|
-
WhoDisSyntaxError.new(source).call
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
require_relative "code_line"
|
144
|
-
require_relative "code_block"
|
145
|
-
require_relative "code_search"
|
146
|
-
require_relative "code_frontier"
|
147
|
-
require_relative "clean_document"
|
148
|
-
|
149
|
-
require_relative "lex_all"
|
150
|
-
require_relative "block_expand"
|
151
|
-
require_relative "around_block_scan"
|
152
|
-
require_relative "who_dis_syntax_error"
|
153
|
-
require_relative "display_invalid_blocks"
|
154
|
-
require_relative "parse_blocks_from_indent_line"
|
@@ -1,83 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module DeadEnd
|
4
|
-
# Determines what type of syntax error that is in the source
|
5
|
-
#
|
6
|
-
# Example:
|
7
|
-
#
|
8
|
-
# puts WhoDisSyntaxError.new("def foo;").call.error_symbol
|
9
|
-
# # => :missing_end
|
10
|
-
class WhoDisSyntaxError < Ripper
|
11
|
-
CHARACTERS = {"{": :"}", "}": :"{", "[": :"]", "]": :"[", "(": :")", ")": :"("}
|
12
|
-
class Null
|
13
|
-
def error_symbol
|
14
|
-
:missing_end
|
15
|
-
end
|
16
|
-
|
17
|
-
def unmatched_symbol
|
18
|
-
:end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
attr_reader :error, :run_once
|
22
|
-
|
23
|
-
# Return options:
|
24
|
-
# - :missing_end
|
25
|
-
# - :unmatched_syntax
|
26
|
-
# - :unknown
|
27
|
-
def error_symbol
|
28
|
-
call
|
29
|
-
@error_symbol
|
30
|
-
end
|
31
|
-
|
32
|
-
# Return options:
|
33
|
-
# - :end
|
34
|
-
# - :|
|
35
|
-
# - :}
|
36
|
-
# - :unknown
|
37
|
-
def unmatched_symbol
|
38
|
-
call
|
39
|
-
@unmatched_symbol
|
40
|
-
end
|
41
|
-
|
42
|
-
def call
|
43
|
-
@run_once ||= begin
|
44
|
-
parse
|
45
|
-
true
|
46
|
-
end
|
47
|
-
self
|
48
|
-
end
|
49
|
-
|
50
|
-
def on_parse_error(msg)
|
51
|
-
return if @error_symbol && @unmatched_symbol
|
52
|
-
|
53
|
-
@error = msg
|
54
|
-
@unmatched_symbol = :unknown
|
55
|
-
|
56
|
-
case @error
|
57
|
-
when /unexpected end-of-input/
|
58
|
-
@error_symbol = :missing_end
|
59
|
-
when /expecting end-of-input/
|
60
|
-
@unmatched_symbol = :end
|
61
|
-
@error_symbol = :unmatched_syntax
|
62
|
-
when /unexpected .* expecting ['`]?(?<unmatched_symbol>[^']*)/
|
63
|
-
if $1
|
64
|
-
character = $1.to_sym
|
65
|
-
@unmatched_symbol = CHARACTERS[character] || character
|
66
|
-
@unmatched_symbol = :end if @unmatched_symbol == :keyword_end
|
67
|
-
end
|
68
|
-
@error_symbol = :unmatched_syntax
|
69
|
-
when /unexpected '(?<unmatched_symbol>.*)'/
|
70
|
-
@unmatched_symbol = $1.to_sym
|
71
|
-
@unmatched_symbol = :end if @unmatched_symbol == :keyword_end
|
72
|
-
@error_symbol = :unmatched_syntax
|
73
|
-
when /unexpected `end'/, # Ruby 2.7 and 3.0
|
74
|
-
/unexpected end/, # Ruby 2.6
|
75
|
-
/unexpected keyword_end/i # Ruby 2.5
|
76
|
-
|
77
|
-
@error_symbol = :unmatched_syntax
|
78
|
-
else
|
79
|
-
@error_symbol = :unknown
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|