dead_end 3.0.1 → 3.1.1
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/.circleci/config.yml +13 -1
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +2 -2
- data/Gemfile.lock +2 -2
- data/README.md +19 -2
- data/lib/dead_end/api.rb +196 -0
- data/lib/dead_end/auto.rb +2 -31
- data/lib/dead_end/clean_document.rb +3 -3
- data/lib/dead_end/code_frontier.rb +9 -1
- data/lib/dead_end/code_line.rb +27 -21
- data/lib/dead_end/core_ext.rb +35 -0
- data/lib/dead_end/left_right_lex_count.rb +11 -0
- data/lib/dead_end/lex_all.rb +15 -8
- data/lib/dead_end/lex_value.rb +11 -3
- data/lib/dead_end/pathname_from_message.rb +47 -0
- data/lib/dead_end/ripper_errors.rb +6 -0
- data/lib/dead_end/version.rb +1 -1
- data/lib/dead_end.rb +2 -157
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64abeaee0636d3ded8df0f0dc124674a70f954287b37302dff7151f04b746557
|
4
|
+
data.tar.gz: 02e6eea73a9e9e832b898f9294c1ce3f7f38f9b8996554a8f6527abf7027d115
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7381458f44ef92f5053ac3171ac8bc59e89a805d793dde1c48ce1eda855dcea624ef6dc7a1faa3da9c20abc53983675fc66a7b08e3e02893801b4b4ff0edf65
|
7
|
+
data.tar.gz: 8b92dee56fac32777f31b76e25a0b778b2a4ffa1ff6914f24ef835bbf5ab79898f4a3a5607a9bebc641f86b05f2a88224504f0f8aaab83c3262b1d067ea32af0
|
data/.circleci/config.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
version: 2.1
|
2
2
|
orbs:
|
3
|
-
ruby: circleci/ruby@1.
|
3
|
+
ruby: circleci/ruby@1.2.0
|
4
4
|
references:
|
5
5
|
unit: &unit
|
6
6
|
run:
|
@@ -45,6 +45,17 @@ jobs:
|
|
45
45
|
- ruby/install-deps
|
46
46
|
- <<: *unit
|
47
47
|
|
48
|
+
"ruby-3-1":
|
49
|
+
docker:
|
50
|
+
- image: 'cimg/base:stable'
|
51
|
+
steps:
|
52
|
+
- checkout
|
53
|
+
- ruby/install:
|
54
|
+
version: '3.1.0-preview1'
|
55
|
+
- run: ruby -v
|
56
|
+
- ruby/install-deps
|
57
|
+
- <<: *unit
|
58
|
+
|
48
59
|
"lint":
|
49
60
|
docker:
|
50
61
|
- image: circleci/ruby:3.0
|
@@ -61,4 +72,5 @@ workflows:
|
|
61
72
|
- "ruby-2-6"
|
62
73
|
- "ruby-2-7"
|
63
74
|
- "ruby-3-0"
|
75
|
+
- "ruby-3-1"
|
64
76
|
- "lint"
|
data/CHANGELOG.md
CHANGED
@@ -1,11 +1,33 @@
|
|
1
1
|
## HEAD (unreleased)
|
2
2
|
|
3
|
+
## 3.1.1
|
4
|
+
|
5
|
+
- Fix case where Ripper lexing identified incorrect code as a keyword (https://github.com/zombocom/dead_end/pull/122)
|
6
|
+
|
7
|
+
## 3.1.0
|
8
|
+
|
9
|
+
- Add support for Ruby 3.1 by updating `require_relative` logic (https://github.com/zombocom/dead_end/pull/120)
|
10
|
+
- Requiring `dead_end/auto` is now deprecated please require `dead_end` instead (https://github.com/zombocom/dead_end/pull/119)
|
11
|
+
- Requiring `dead_end/api` now loads code without monkeypatching core extensions (https://github.com/zombocom/dead_end/pull/119)
|
12
|
+
- The interface `DeadEnd.handle_error` is declared public and stable (https://github.com/zombocom/dead_end/pull/119)
|
13
|
+
|
14
|
+
## 3.0.3
|
15
|
+
|
16
|
+
- Expand explanations coming from additional Ripper errors (https://github.com/zombocom/dead_end/pull/117)
|
17
|
+
- Fix explanation involving shorthand syntax for literals like `%w[]` and `%Q{}` (https://github.com/zombocom/dead_end/pull/116)
|
18
|
+
|
19
|
+
## 3.0.2
|
20
|
+
|
21
|
+
- Fix windows filename detection (https://github.com/zombocom/dead_end/pull/114)
|
22
|
+
- Update links on readme and code of conduct (https://github.com/zombocom/dead_end/pull/107)
|
23
|
+
|
3
24
|
## 3.0.1
|
4
25
|
|
5
26
|
- Fix CLI parsing when flags come before filename (https://github.com/zombocom/dead_end/pull/102)
|
6
27
|
|
7
28
|
## 3.0.0
|
8
29
|
|
30
|
+
- [Breaking] CLI now outputs to STDOUT instead of STDERR (https://github.com/zombocom/dead_end/pull/98)
|
9
31
|
- [Breaking] Remove previously deprecated `require "dead_end/fyi"` interface (https://github.com/zombocom/dead_end/pull/94)
|
10
32
|
- Fix double output bug (https://github.com/zombocom/dead_end/pull/99)
|
11
33
|
- Fix bug causing poor results (fix #95, fix #88) (https://github.com/zombocom/dead_end/pull/96)
|
data/CODE_OF_CONDUCT.md
CHANGED
@@ -68,7 +68,7 @@ members of the project's leadership.
|
|
68
68
|
## Attribution
|
69
69
|
|
70
70
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
-
available at [https://contributor-covenant.org/version/1/4][version]
|
71
|
+
available at [https://contributor-covenant.org/version/1/4/code-of-conduct/][version]
|
72
72
|
|
73
73
|
[homepage]: https://contributor-covenant.org
|
74
|
-
[version]: https://contributor-covenant.org/version/1/4/
|
74
|
+
[version]: https://contributor-covenant.org/version/1/4/code-of-conduct/
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -45,6 +45,13 @@ To get the CLI and manually search for syntax errors (but not automatically anno
|
|
45
45
|
|
46
46
|
This gives you the CLI command `$ dead_end` for more info run `$ dead_end --help`.
|
47
47
|
|
48
|
+
## Editor integration
|
49
|
+
|
50
|
+
An extension is available for VSCode:
|
51
|
+
|
52
|
+
- Extension: https://marketplace.visualstudio.com/items?itemName=Zombocom.dead-end-vscode
|
53
|
+
- GitHub: https://github.com/zombocom/dead_end-vscode
|
54
|
+
|
48
55
|
## What syntax errors does it handle?
|
49
56
|
|
50
57
|
Dead end will fire against all syntax errors and can isolate any syntax error. In addition, dead_end attempts to produce human readable descriptions of what needs to be done to resolve the issue. For example:
|
@@ -159,6 +166,16 @@ Here's an example:
|
|
159
166
|
|
160
167
|

|
161
168
|
|
169
|
+
## Use internals
|
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`.
|
172
|
+
|
173
|
+
Stable internal interface(s):
|
174
|
+
|
175
|
+
- `DeadEnd.handle_error(e)`
|
176
|
+
|
177
|
+
Any other entrypoints are subject to change without warning. If you want to use an internal interface from `dead_end` not on this list, open an issue to explain your use case.
|
178
|
+
|
162
179
|
## Development
|
163
180
|
|
164
181
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -195,7 +212,7 @@ $ qcachegrind tmp/last/profile.callgrind.out.<numbers>
|
|
195
212
|
|
196
213
|
## Contributing
|
197
214
|
|
198
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/zombocom/dead_end. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/zombocom/dead_end/blob/
|
215
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/zombocom/dead_end. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/zombocom/dead_end/blob/main/CODE_OF_CONDUCT.md).
|
199
216
|
|
200
217
|
|
201
218
|
## License
|
@@ -204,4 +221,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
204
221
|
|
205
222
|
## Code of Conduct
|
206
223
|
|
207
|
-
Everyone interacting in the DeadEnd project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/zombocom/dead_end/blob/
|
224
|
+
Everyone interacting in the DeadEnd project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/zombocom/dead_end/blob/main/CODE_OF_CONDUCT.md).
|
data/lib/dead_end/api.rb
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
require_relative "version"
|
2
|
+
|
3
|
+
require "tmpdir"
|
4
|
+
require "stringio"
|
5
|
+
require "pathname"
|
6
|
+
require "ripper"
|
7
|
+
require "timeout"
|
8
|
+
|
9
|
+
module DeadEnd
|
10
|
+
# Used to indicate a default value that cannot
|
11
|
+
# be confused with another input.
|
12
|
+
DEFAULT_VALUE = Object.new.freeze
|
13
|
+
|
14
|
+
class Error < StandardError; end
|
15
|
+
TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
|
16
|
+
|
17
|
+
# DeadEnd.handle_error [Public]
|
18
|
+
#
|
19
|
+
# Takes a `SyntaxError`` exception, uses the
|
20
|
+
# error message to locate the file. Then the file
|
21
|
+
# will be analyzed to find the location of the syntax
|
22
|
+
# error and emit that location to stderr.
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# begin
|
27
|
+
# require 'bad_file'
|
28
|
+
# rescue => e
|
29
|
+
# DeadEnd.handle_error(e)
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# By default it will re-raise the exception unless
|
33
|
+
# `re_raise: false`. The message output location
|
34
|
+
# can be configured using the `io: $stderr` input.
|
35
|
+
#
|
36
|
+
# If a valid filename cannot be determined, the original
|
37
|
+
# exception will be re-raised (even with
|
38
|
+
# `re_raise: false`).
|
39
|
+
def self.handle_error(e, re_raise: true, io: $stderr)
|
40
|
+
unless e.is_a?(SyntaxError)
|
41
|
+
io.puts("DeadEnd: Must pass a SyntaxError, got: #{e.class}")
|
42
|
+
raise e
|
43
|
+
end
|
44
|
+
|
45
|
+
file = PathnameFromMessage.new(e.message, io: io).call.name
|
46
|
+
raise e unless file
|
47
|
+
|
48
|
+
io.sync = true
|
49
|
+
|
50
|
+
call(
|
51
|
+
io: io,
|
52
|
+
source: file.read,
|
53
|
+
filename: file
|
54
|
+
)
|
55
|
+
|
56
|
+
raise e if re_raise
|
57
|
+
end
|
58
|
+
|
59
|
+
# DeadEnd.call [Private]
|
60
|
+
#
|
61
|
+
# Main private interface
|
62
|
+
def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
|
63
|
+
search = nil
|
64
|
+
filename = nil if filename == DEFAULT_VALUE
|
65
|
+
Timeout.timeout(timeout) do
|
66
|
+
record_dir ||= ENV["DEBUG"] ? "tmp" : nil
|
67
|
+
search = CodeSearch.new(source, record_dir: record_dir).call
|
68
|
+
end
|
69
|
+
|
70
|
+
blocks = search.invalid_blocks
|
71
|
+
DisplayInvalidBlocks.new(
|
72
|
+
io: io,
|
73
|
+
blocks: blocks,
|
74
|
+
filename: filename,
|
75
|
+
terminal: terminal,
|
76
|
+
code_lines: search.code_lines
|
77
|
+
).call
|
78
|
+
rescue Timeout::Error => e
|
79
|
+
io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
|
80
|
+
io.puts e.backtrace.first(3).join($/)
|
81
|
+
end
|
82
|
+
|
83
|
+
# DeadEnd.record_dir [Private]
|
84
|
+
#
|
85
|
+
# Used to generate a unique directory to record
|
86
|
+
# search steps for debugging
|
87
|
+
def self.record_dir(dir)
|
88
|
+
time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
|
89
|
+
dir = Pathname(dir)
|
90
|
+
symlink = dir.join("last").tap { |path| path.delete if path.exist? }
|
91
|
+
dir.join(time).tap { |path|
|
92
|
+
path.mkpath
|
93
|
+
FileUtils.symlink(path.basename, symlink)
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# DeadEnd.valid_without? [Private]
|
98
|
+
#
|
99
|
+
# This will tell you if the `code_lines` would be valid
|
100
|
+
# if you removed the `without_lines`. In short it's a
|
101
|
+
# way to detect if we've found the lines with syntax errors
|
102
|
+
# in our document yet.
|
103
|
+
#
|
104
|
+
# code_lines = [
|
105
|
+
# CodeLine.new(line: "def foo\n", index: 0)
|
106
|
+
# CodeLine.new(line: " def bar\n", index: 1)
|
107
|
+
# CodeLine.new(line: "end\n", index: 2)
|
108
|
+
# ]
|
109
|
+
#
|
110
|
+
# DeadEnd.valid_without?(
|
111
|
+
# without_lines: code_lines[1],
|
112
|
+
# code_lines: code_lines
|
113
|
+
# ) # => true
|
114
|
+
#
|
115
|
+
# DeadEnd.valid?(code_lines) # => false
|
116
|
+
def self.valid_without?(without_lines:, code_lines:)
|
117
|
+
lines = code_lines - Array(without_lines).flatten
|
118
|
+
|
119
|
+
if lines.empty?
|
120
|
+
true
|
121
|
+
else
|
122
|
+
valid?(lines)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# DeadEnd.invalid? [Private]
|
127
|
+
#
|
128
|
+
# Opposite of `DeadEnd.valid?`
|
129
|
+
def self.invalid?(source)
|
130
|
+
source = source.join if source.is_a?(Array)
|
131
|
+
source = source.to_s
|
132
|
+
|
133
|
+
Ripper.new(source).tap(&:parse).error?
|
134
|
+
end
|
135
|
+
|
136
|
+
# DeadEnd.valid? [Private]
|
137
|
+
#
|
138
|
+
# Returns truthy if a given input source is valid syntax
|
139
|
+
#
|
140
|
+
# DeadEnd.valid?(<<~EOM) # => true
|
141
|
+
# def foo
|
142
|
+
# end
|
143
|
+
# EOM
|
144
|
+
#
|
145
|
+
# DeadEnd.valid?(<<~EOM) # => false
|
146
|
+
# def foo
|
147
|
+
# def bar # Syntax error here
|
148
|
+
# end
|
149
|
+
# EOM
|
150
|
+
#
|
151
|
+
# You can also pass in an array of lines and they'll be
|
152
|
+
# joined before evaluating
|
153
|
+
#
|
154
|
+
# DeadEnd.valid?(
|
155
|
+
# [
|
156
|
+
# "def foo\n",
|
157
|
+
# "end\n"
|
158
|
+
# ]
|
159
|
+
# ) # => true
|
160
|
+
#
|
161
|
+
# DeadEnd.valid?(
|
162
|
+
# [
|
163
|
+
# "def foo\n",
|
164
|
+
# " def bar\n", # Syntax error here
|
165
|
+
# "end\n"
|
166
|
+
# ]
|
167
|
+
# ) # => false
|
168
|
+
#
|
169
|
+
# As an FYI the CodeLine class instances respond to `to_s`
|
170
|
+
# so passing a CodeLine in as an object or as an array
|
171
|
+
# will convert it to it's code representation.
|
172
|
+
def self.valid?(source)
|
173
|
+
!invalid?(source)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Integration
|
178
|
+
require_relative "cli"
|
179
|
+
|
180
|
+
# Core logic
|
181
|
+
require_relative "code_search"
|
182
|
+
require_relative "code_frontier"
|
183
|
+
require_relative "explain_syntax"
|
184
|
+
require_relative "clean_document"
|
185
|
+
|
186
|
+
# Helpers
|
187
|
+
require_relative "lex_all"
|
188
|
+
require_relative "code_line"
|
189
|
+
require_relative "code_block"
|
190
|
+
require_relative "block_expand"
|
191
|
+
require_relative "ripper_errors"
|
192
|
+
require_relative "insertion_sort"
|
193
|
+
require_relative "around_block_scan"
|
194
|
+
require_relative "pathname_from_message"
|
195
|
+
require_relative "display_invalid_blocks"
|
196
|
+
require_relative "parse_blocks_from_indent_line"
|
data/lib/dead_end/auto.rb
CHANGED
@@ -1,35 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "../dead_end"
|
4
|
+
require_relative "core_ext"
|
4
5
|
|
5
|
-
|
6
|
-
# method
|
7
|
-
module Kernel
|
8
|
-
module_function
|
9
|
-
|
10
|
-
alias_method :dead_end_original_require, :require
|
11
|
-
alias_method :dead_end_original_require_relative, :require_relative
|
12
|
-
alias_method :dead_end_original_load, :load
|
13
|
-
|
14
|
-
def load(file, wrap = false)
|
15
|
-
dead_end_original_load(file)
|
16
|
-
rescue SyntaxError => e
|
17
|
-
DeadEnd.handle_error(e)
|
18
|
-
end
|
19
|
-
|
20
|
-
def require(file)
|
21
|
-
dead_end_original_require(file)
|
22
|
-
rescue SyntaxError => e
|
23
|
-
DeadEnd.handle_error(e)
|
24
|
-
end
|
25
|
-
|
26
|
-
def require_relative(file)
|
27
|
-
if Pathname.new(file).absolute?
|
28
|
-
dead_end_original_require file
|
29
|
-
else
|
30
|
-
dead_end_original_require File.expand_path("../#{file}", Kernel.caller_locations(1, 1)[0].absolute_path)
|
31
|
-
end
|
32
|
-
rescue SyntaxError => e
|
33
|
-
DeadEnd.handle_error(e)
|
34
|
-
end
|
35
|
-
end
|
6
|
+
warn "Calling `require 'dead_end/auto'` is deprecated, please `require 'dead_end'` instead."
|
@@ -85,8 +85,8 @@ module DeadEnd
|
|
85
85
|
#
|
86
86
|
class CleanDocument
|
87
87
|
def initialize(source:)
|
88
|
-
|
89
|
-
@document = CodeLine.from_source(
|
88
|
+
lines = clean_sweep(source: source)
|
89
|
+
@document = CodeLine.from_source(lines.join, lines: lines)
|
90
90
|
end
|
91
91
|
|
92
92
|
# Call all of the document "cleaners"
|
@@ -161,7 +161,7 @@ module DeadEnd
|
|
161
161
|
else
|
162
162
|
line
|
163
163
|
end
|
164
|
-
end
|
164
|
+
end
|
165
165
|
end
|
166
166
|
|
167
167
|
# Smushes all heredoc lines into one line
|
@@ -54,6 +54,8 @@ module DeadEnd
|
|
54
54
|
@code_lines = code_lines
|
55
55
|
@frontier = InsertionSort.new
|
56
56
|
@unvisited_lines = @code_lines.sort_by(&:indent_index)
|
57
|
+
@visited_lines = {}
|
58
|
+
|
57
59
|
@has_run = false
|
58
60
|
@check_next = true
|
59
61
|
end
|
@@ -128,7 +130,13 @@ module DeadEnd
|
|
128
130
|
end
|
129
131
|
|
130
132
|
def register_indent_block(block)
|
131
|
-
|
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
|
132
140
|
self
|
133
141
|
end
|
134
142
|
|
data/lib/dead_end/code_line.rb
CHANGED
@@ -26,9 +26,10 @@ module DeadEnd
|
|
26
26
|
|
27
27
|
# Returns an array of CodeLine objects
|
28
28
|
# from the source string
|
29
|
-
def self.from_source(source)
|
30
|
-
|
31
|
-
source
|
29
|
+
def self.from_source(source, lines: nil)
|
30
|
+
lines ||= source.lines
|
31
|
+
lex_array_for_line = LexAll.new(source: source, source_lines: lines).each_with_object(Hash.new { |h, k| h[k] = [] }) { |lex, hash| hash[lex.line] << lex }
|
32
|
+
lines.map.with_index do |line, index|
|
32
33
|
CodeLine.new(
|
33
34
|
line: line,
|
34
35
|
index: index,
|
@@ -42,28 +43,20 @@ module DeadEnd
|
|
42
43
|
@lex = lex
|
43
44
|
@line = line
|
44
45
|
@index = index
|
45
|
-
@original = line
|
46
|
+
@original = line
|
46
47
|
@line_number = @index + 1
|
48
|
+
strip_line = line.dup
|
49
|
+
strip_line.lstrip!
|
47
50
|
|
48
|
-
if
|
51
|
+
if strip_line.empty?
|
49
52
|
@empty = true
|
50
53
|
@indent = 0
|
51
54
|
else
|
52
55
|
@empty = false
|
53
|
-
@indent =
|
56
|
+
@indent = line.length - strip_line.length
|
54
57
|
end
|
55
58
|
|
56
|
-
|
57
|
-
end_count = 0
|
58
|
-
@lex.each do |lex|
|
59
|
-
kw_count += 1 if lex.is_kw?
|
60
|
-
end_count += 1 if lex.is_end?
|
61
|
-
end
|
62
|
-
|
63
|
-
kw_count -= oneliner_method_count
|
64
|
-
|
65
|
-
@is_kw = (kw_count - end_count) > 0
|
66
|
-
@is_end = (end_count - kw_count) > 0
|
59
|
+
set_kw_end
|
67
60
|
end
|
68
61
|
|
69
62
|
# Used for stable sort via indentation level
|
@@ -179,8 +172,7 @@ module DeadEnd
|
|
179
172
|
#
|
180
173
|
# For some reason this introduces `on_ignore_newline` but with BEG type
|
181
174
|
def ignore_newline_not_beg?
|
182
|
-
|
183
|
-
!!(lex_value && !lex_value.expr_beg?)
|
175
|
+
@ignore_newline_not_beg
|
184
176
|
end
|
185
177
|
|
186
178
|
# Determines if the given line has a trailing slash
|
@@ -206,11 +198,22 @@ module DeadEnd
|
|
206
198
|
#
|
207
199
|
# ENDFN -> BEG (token = '=' ) -> END
|
208
200
|
#
|
209
|
-
private def
|
201
|
+
private def set_kw_end
|
210
202
|
oneliner_count = 0
|
211
203
|
in_oneliner_def = nil
|
212
204
|
|
205
|
+
kw_count = 0
|
206
|
+
end_count = 0
|
207
|
+
|
208
|
+
@ignore_newline_not_beg = false
|
213
209
|
@lex.each do |lex|
|
210
|
+
kw_count += 1 if lex.is_kw?
|
211
|
+
end_count += 1 if lex.is_end?
|
212
|
+
|
213
|
+
if lex.type == :on_ignored_nl
|
214
|
+
@ignore_newline_not_beg = !lex.expr_beg?
|
215
|
+
end
|
216
|
+
|
214
217
|
if in_oneliner_def.nil?
|
215
218
|
in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN)
|
216
219
|
elsif lex.state.allbits?(Ripper::EXPR_ENDFN)
|
@@ -227,7 +230,10 @@ module DeadEnd
|
|
227
230
|
end
|
228
231
|
end
|
229
232
|
|
230
|
-
oneliner_count
|
233
|
+
kw_count -= oneliner_count
|
234
|
+
|
235
|
+
@is_kw = (kw_count - end_count) > 0
|
236
|
+
@is_end = (end_count - kw_count) > 0
|
231
237
|
end
|
232
238
|
end
|
233
239
|
end
|
@@ -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
|
@@ -61,6 +61,17 @@ module DeadEnd
|
|
61
61
|
# ^^^
|
62
62
|
# Means it's a string or a symbol `"{"` rather than being
|
63
63
|
# part of a data structure (like a hash) `{ a: b }`
|
64
|
+
# ignore it.
|
65
|
+
when :on_words_beg, :on_symbos_beg, :on_qwords_beg,
|
66
|
+
:on_qsymbols_beg, :on_regexp_beg, :on_tstring_beg
|
67
|
+
# ^^^
|
68
|
+
# Handle shorthand syntaxes like `%Q{ i am a string }`
|
69
|
+
#
|
70
|
+
# The start token will be the full thing `%Q{` but we
|
71
|
+
# need to count it as if it's a `{`. Any token
|
72
|
+
# can be used
|
73
|
+
char = lex.token[-1]
|
74
|
+
@count_for_char[char] += 1 if @count_for_char.key?(char)
|
64
75
|
when :on_embexpr_beg
|
65
76
|
# ^^^
|
66
77
|
# Embedded string expressions like `"#{foo} <-embed"`
|
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,25 @@ 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
|
-
|
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
|
+
}
|
25
32
|
end
|
26
33
|
|
27
34
|
def to_a
|
data/lib/dead_end/lex_value.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module DeadEnd
|
2
4
|
# Value object for accessing lex values
|
3
5
|
#
|
@@ -13,19 +15,21 @@ module DeadEnd
|
|
13
15
|
class LexValue
|
14
16
|
attr_reader :line, :type, :token, :state
|
15
17
|
|
16
|
-
def initialize(line, type, token, state)
|
18
|
+
def initialize(line, type, token, state, last_lex = nil)
|
17
19
|
@line = line
|
18
20
|
@type = type
|
19
21
|
@token = token
|
20
22
|
@state = state
|
21
23
|
|
22
|
-
set_kw_end
|
24
|
+
set_kw_end(last_lex)
|
23
25
|
end
|
24
26
|
|
25
|
-
private def set_kw_end
|
27
|
+
private def set_kw_end(last_lex)
|
26
28
|
@is_end = false
|
27
29
|
@is_kw = false
|
28
30
|
return if type != :on_kw
|
31
|
+
#
|
32
|
+
return if last_lex && last_lex.fname? # https://github.com/ruby/ruby/commit/776759e300e4659bb7468e2b97c8c2d4359a2953
|
29
33
|
|
30
34
|
case token
|
31
35
|
when "if", "unless", "while", "until"
|
@@ -39,6 +43,10 @@ module DeadEnd
|
|
39
43
|
end
|
40
44
|
end
|
41
45
|
|
46
|
+
def fname?
|
47
|
+
state.allbits?(Ripper::EXPR_FNAME)
|
48
|
+
end
|
49
|
+
|
42
50
|
def ignore_newline?
|
43
51
|
type == :on_ignored_nl
|
44
52
|
end
|
@@ -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
|
@@ -18,6 +18,12 @@ module DeadEnd
|
|
18
18
|
@errors << msg
|
19
19
|
end
|
20
20
|
|
21
|
+
alias_method :on_alias_error, :on_parse_error
|
22
|
+
alias_method :on_assign_error, :on_parse_error
|
23
|
+
alias_method :on_class_name_error, :on_parse_error
|
24
|
+
alias_method :on_param_error, :on_parse_error
|
25
|
+
alias_method :compile_error, :on_parse_error
|
26
|
+
|
21
27
|
def call
|
22
28
|
@run_once ||= begin
|
23
29
|
@errors = []
|
data/lib/dead_end/version.rb
CHANGED
data/lib/dead_end.rb
CHANGED
@@ -1,159 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "dead_end/
|
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
|
-
filename = e.message.split(":").first
|
21
|
-
$stderr.sync = true
|
22
|
-
|
23
|
-
call(
|
24
|
-
source: Pathname(filename).read,
|
25
|
-
filename: filename
|
26
|
-
)
|
27
|
-
|
28
|
-
raise e
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.record_dir(dir)
|
32
|
-
time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
|
33
|
-
dir = Pathname(dir)
|
34
|
-
symlink = dir.join("last").tap { |path| path.delete if path.exist? }
|
35
|
-
dir.join(time).tap { |path|
|
36
|
-
path.mkpath
|
37
|
-
FileUtils.symlink(path.basename, symlink)
|
38
|
-
}
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
|
42
|
-
search = nil
|
43
|
-
filename = nil if filename == DEFAULT_VALUE
|
44
|
-
Timeout.timeout(timeout) do
|
45
|
-
record_dir ||= ENV["DEBUG"] ? "tmp" : nil
|
46
|
-
search = CodeSearch.new(source, record_dir: record_dir).call
|
47
|
-
end
|
48
|
-
|
49
|
-
blocks = search.invalid_blocks
|
50
|
-
DisplayInvalidBlocks.new(
|
51
|
-
io: io,
|
52
|
-
blocks: blocks,
|
53
|
-
filename: filename,
|
54
|
-
terminal: terminal,
|
55
|
-
code_lines: search.code_lines
|
56
|
-
).call
|
57
|
-
rescue Timeout::Error => e
|
58
|
-
io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
|
59
|
-
io.puts e.backtrace.first(3).join($/)
|
60
|
-
end
|
61
|
-
|
62
|
-
# Used for counting spaces
|
63
|
-
module SpaceCount
|
64
|
-
def self.indent(string)
|
65
|
-
string.split(/\S/).first&.length || 0
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# This will tell you if the `code_lines` would be valid
|
70
|
-
# if you removed the `without_lines`. In short it's a
|
71
|
-
# way to detect if we've found the lines with syntax errors
|
72
|
-
# in our document yet.
|
73
|
-
#
|
74
|
-
# code_lines = [
|
75
|
-
# CodeLine.new(line: "def foo\n", index: 0)
|
76
|
-
# CodeLine.new(line: " def bar\n", index: 1)
|
77
|
-
# CodeLine.new(line: "end\n", index: 2)
|
78
|
-
# ]
|
79
|
-
#
|
80
|
-
# DeadEnd.valid_without?(
|
81
|
-
# without_lines: code_lines[1],
|
82
|
-
# code_lines: code_lines
|
83
|
-
# ) # => true
|
84
|
-
#
|
85
|
-
# DeadEnd.valid?(code_lines) # => false
|
86
|
-
def self.valid_without?(without_lines:, code_lines:)
|
87
|
-
lines = code_lines - Array(without_lines).flatten
|
88
|
-
|
89
|
-
if lines.empty?
|
90
|
-
true
|
91
|
-
else
|
92
|
-
valid?(lines)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
def self.invalid?(source)
|
97
|
-
source = source.join if source.is_a?(Array)
|
98
|
-
source = source.to_s
|
99
|
-
|
100
|
-
Ripper.new(source).tap(&:parse).error?
|
101
|
-
end
|
102
|
-
|
103
|
-
# Returns truthy if a given input source is valid syntax
|
104
|
-
#
|
105
|
-
# DeadEnd.valid?(<<~EOM) # => true
|
106
|
-
# def foo
|
107
|
-
# end
|
108
|
-
# EOM
|
109
|
-
#
|
110
|
-
# DeadEnd.valid?(<<~EOM) # => false
|
111
|
-
# def foo
|
112
|
-
# def bar # Syntax error here
|
113
|
-
# end
|
114
|
-
# EOM
|
115
|
-
#
|
116
|
-
# You can also pass in an array of lines and they'll be
|
117
|
-
# joined before evaluating
|
118
|
-
#
|
119
|
-
# DeadEnd.valid?(
|
120
|
-
# [
|
121
|
-
# "def foo\n",
|
122
|
-
# "end\n"
|
123
|
-
# ]
|
124
|
-
# ) # => true
|
125
|
-
#
|
126
|
-
# DeadEnd.valid?(
|
127
|
-
# [
|
128
|
-
# "def foo\n",
|
129
|
-
# " def bar\n", # Syntax error here
|
130
|
-
# "end\n"
|
131
|
-
# ]
|
132
|
-
# ) # => false
|
133
|
-
#
|
134
|
-
# As an FYI the CodeLine class instances respond to `to_s`
|
135
|
-
# so passing a CodeLine in as an object or as an array
|
136
|
-
# will convert it to it's code representation.
|
137
|
-
def self.valid?(source)
|
138
|
-
!invalid?(source)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
require_relative "dead_end/code_line"
|
143
|
-
require_relative "dead_end/code_block"
|
144
|
-
require_relative "dead_end/code_search"
|
145
|
-
require_relative "dead_end/code_frontier"
|
146
|
-
require_relative "dead_end/clean_document"
|
147
|
-
|
148
|
-
require_relative "dead_end/lex_all"
|
149
|
-
require_relative "dead_end/block_expand"
|
150
|
-
require_relative "dead_end/insertion_sort"
|
151
|
-
require_relative "dead_end/around_block_scan"
|
152
|
-
require_relative "dead_end/ripper_errors"
|
153
|
-
require_relative "dead_end/display_invalid_blocks"
|
154
|
-
require_relative "dead_end/parse_blocks_from_indent_line"
|
155
|
-
|
156
|
-
require_relative "dead_end/explain_syntax"
|
157
|
-
|
158
|
-
require_relative "dead_end/auto"
|
159
|
-
require_relative "dead_end/cli"
|
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.
|
4
|
+
version: 3.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- schneems
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-04 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,6 +47,7 @@ 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
|
@@ -54,6 +56,7 @@ files:
|
|
54
56
|
- lib/dead_end/lex_all.rb
|
55
57
|
- lib/dead_end/lex_value.rb
|
56
58
|
- lib/dead_end/parse_blocks_from_indent_line.rb
|
59
|
+
- lib/dead_end/pathname_from_message.rb
|
57
60
|
- lib/dead_end/ripper_errors.rb
|
58
61
|
- lib/dead_end/version.rb
|
59
62
|
homepage: https://github.com/zombocom/dead_end.git
|
@@ -77,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
80
|
- !ruby/object:Gem::Version
|
78
81
|
version: '0'
|
79
82
|
requirements: []
|
80
|
-
rubygems_version: 3.2.
|
83
|
+
rubygems_version: 3.2.32
|
81
84
|
signing_key:
|
82
85
|
specification_version: 4
|
83
86
|
summary: Find syntax errors in your source in a snap
|