error_highlight 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +27 -0
- data/.gitignore +8 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +71 -0
- data/Rakefile +12 -0
- data/error_highlight.gemspec +27 -0
- data/lib/error_highlight.rb +2 -0
- data/lib/error_highlight/base.rb +446 -0
- data/lib/error_highlight/core_ext.rb +48 -0
- data/lib/error_highlight/version.rb +3 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f6edc97e3eca88e8281056e7a3d9bceb448c0ac614b742e4ed38908a40e51e01
|
4
|
+
data.tar.gz: 255b537c7d07f8dfd01b747719472e57d24e91703b8df678d655a3fc3f71897d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a63da67176a0dabd493fdafa1de60155a26696d22ba633d17765b29a359b0e4ffcc30d4806236bf416c828427d32171aca4210bdb1e018e0347e4cefef3e953a
|
7
|
+
data.tar.gz: '08febd90564658489923f2ef0d2aab82a3796e52c7aa2a11ddcd6cc0d1f65abacd9b757b61bbb36bb59d7cf0e0c1eea820bce6d3b0bad3f771ead27714bc70ee'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
branches:
|
6
|
+
- 'master'
|
7
|
+
push:
|
8
|
+
branches:
|
9
|
+
- 'master'
|
10
|
+
|
11
|
+
jobs:
|
12
|
+
build:
|
13
|
+
runs-on: ubuntu-latest
|
14
|
+
strategy:
|
15
|
+
matrix:
|
16
|
+
ruby: [ 'ruby-head' ]
|
17
|
+
steps:
|
18
|
+
- uses: actions/checkout@v2
|
19
|
+
- uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: ${{ matrix.ruby }}
|
22
|
+
- name: Bundle install
|
23
|
+
run: |
|
24
|
+
bundle install
|
25
|
+
- name: Run the test suite
|
26
|
+
run: |
|
27
|
+
bundle exec rake TESTOPT=-v
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Yusuke Endoh
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# ErrorHighlight
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
Ruby 3.1 will ship with this gem and it will automatically be `require`d when a Ruby process starts up. No special setup is required.
|
6
|
+
|
7
|
+
Note: This gem works only on MRI and requires Ruby 3.1 or later because it depends on MRI's internal APIs that are available since 3.1.
|
8
|
+
|
9
|
+
## Examples
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
1.time {}
|
13
|
+
```
|
14
|
+
|
15
|
+
```
|
16
|
+
$ ruby test.rb
|
17
|
+
test.rb:1:in `<main>': undefined method `time' for 1:Integer (NoMethodError)
|
18
|
+
|
19
|
+
1.time {}
|
20
|
+
^^^^^
|
21
|
+
Did you mean? times
|
22
|
+
```
|
23
|
+
|
24
|
+
## More example
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
def extract_value(data)
|
28
|
+
data[:results].first[:value]
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
When `data` is `{ :results => [] }`, the following error messsage is shown:
|
33
|
+
|
34
|
+
```
|
35
|
+
$ ruby test.rb
|
36
|
+
test.rb:2:in `extract_value': undefined method `[]' for nil:NilClass (NoMethodError)
|
37
|
+
|
38
|
+
data[:results].first[:value]
|
39
|
+
^^^^^^^^
|
40
|
+
from test.rb:5:in `<main>'
|
41
|
+
```
|
42
|
+
|
43
|
+
When `data` is `nil`, it prints:
|
44
|
+
|
45
|
+
```
|
46
|
+
$ ruby test.rb
|
47
|
+
test.rb:2:in `extract_value': undefined method `[]' for nil:NilClass (NoMethodError)
|
48
|
+
|
49
|
+
data[:results].first[:value]
|
50
|
+
^^^^^^^^^^
|
51
|
+
from test.rb:5:in `<main>'
|
52
|
+
```
|
53
|
+
|
54
|
+
## Disabling `error_highlight`
|
55
|
+
|
56
|
+
Occasionally, you may want to disable the `error_highlight` gem for e.g. debugging issues in the error object itself. You
|
57
|
+
can disable it entirely by specifying `--disable-error_highlight` option to the `ruby` command:
|
58
|
+
|
59
|
+
```bash
|
60
|
+
$ uby --disable-error_highlight -e '1.time {}'
|
61
|
+
-e:1:in `<main>': undefined method `time' for 1:Integer (NoMethodError)
|
62
|
+
Did you mean? times
|
63
|
+
```
|
64
|
+
|
65
|
+
## Contributing
|
66
|
+
|
67
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/error_highlight.
|
68
|
+
|
69
|
+
## License
|
70
|
+
|
71
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
begin
|
5
|
+
require_relative "lib/error_highlight/version"
|
6
|
+
rescue LoadError # Fallback to load version file in ruby core repository
|
7
|
+
require_relative "version"
|
8
|
+
end
|
9
|
+
|
10
|
+
Gem::Specification.new do |spec|
|
11
|
+
spec.name = "error_highlight"
|
12
|
+
spec.version = ErrorHighlight::VERSION
|
13
|
+
spec.authors = ["Yusuke Endoh"]
|
14
|
+
spec.email = ["mame@ruby-lang.org"]
|
15
|
+
|
16
|
+
spec.summary = 'Shows a one-line code snippet with an underline in the error backtrace'
|
17
|
+
spec.description = 'The gem enhances Exception#message by adding a short explanation where the exception is raised'
|
18
|
+
spec.homepage = "https://github.com/ruby/error_highlight"
|
19
|
+
|
20
|
+
spec.license = "MIT"
|
21
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
|
22
|
+
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
end
|
@@ -0,0 +1,446 @@
|
|
1
|
+
require_relative "version"
|
2
|
+
|
3
|
+
module ErrorHighlight
|
4
|
+
# Identify the code fragment that seems associated with a given error
|
5
|
+
#
|
6
|
+
# Arguments:
|
7
|
+
# node: RubyVM::AbstractSyntaxTree::Node
|
8
|
+
# point: :name | :args
|
9
|
+
# name: The name associated with the NameError/NoMethodError
|
10
|
+
# fetch: A block to fetch a specified code line (or lines)
|
11
|
+
#
|
12
|
+
# Returns:
|
13
|
+
# {
|
14
|
+
# first_lineno: Integer,
|
15
|
+
# first_column: Integer,
|
16
|
+
# last_lineno: Integer,
|
17
|
+
# last_column: Integer,
|
18
|
+
# line: String,
|
19
|
+
# } | nil
|
20
|
+
def self.spot(...)
|
21
|
+
Spotter.new(...).spot
|
22
|
+
end
|
23
|
+
|
24
|
+
class Spotter
|
25
|
+
def initialize(node, point, name: nil, &fetch)
|
26
|
+
@node = node
|
27
|
+
@point = point
|
28
|
+
@name = name
|
29
|
+
|
30
|
+
# Not-implemented-yet options
|
31
|
+
@arg = nil # Specify the index or keyword at which argument caused the TypeError/ArgumentError
|
32
|
+
@multiline = false # Allow multiline spot
|
33
|
+
|
34
|
+
@fetch = fetch
|
35
|
+
end
|
36
|
+
|
37
|
+
def spot
|
38
|
+
return nil unless @node
|
39
|
+
|
40
|
+
case @node.type
|
41
|
+
|
42
|
+
when :CALL, :QCALL
|
43
|
+
case @point
|
44
|
+
when :name
|
45
|
+
spot_call_for_name
|
46
|
+
when :args
|
47
|
+
spot_call_for_args
|
48
|
+
end
|
49
|
+
|
50
|
+
when :ATTRASGN
|
51
|
+
case @point
|
52
|
+
when :name
|
53
|
+
spot_attrasgn_for_name
|
54
|
+
when :args
|
55
|
+
spot_attrasgn_for_args
|
56
|
+
end
|
57
|
+
|
58
|
+
when :OPCALL
|
59
|
+
case @point
|
60
|
+
when :name
|
61
|
+
spot_opcall_for_name
|
62
|
+
when :args
|
63
|
+
spot_opcall_for_args
|
64
|
+
end
|
65
|
+
|
66
|
+
when :FCALL
|
67
|
+
case @point
|
68
|
+
when :name
|
69
|
+
spot_fcall_for_name
|
70
|
+
when :args
|
71
|
+
spot_fcall_for_args
|
72
|
+
end
|
73
|
+
|
74
|
+
when :VCALL
|
75
|
+
spot_vcall
|
76
|
+
|
77
|
+
when :OP_ASGN1
|
78
|
+
case @point
|
79
|
+
when :name
|
80
|
+
spot_op_asgn1_for_name
|
81
|
+
when :args
|
82
|
+
spot_op_asgn1_for_args
|
83
|
+
end
|
84
|
+
|
85
|
+
when :OP_ASGN2
|
86
|
+
case @point
|
87
|
+
when :name
|
88
|
+
spot_op_asgn2_for_name
|
89
|
+
when :args
|
90
|
+
spot_op_asgn2_for_args
|
91
|
+
end
|
92
|
+
|
93
|
+
when :CONST
|
94
|
+
spot_vcall
|
95
|
+
|
96
|
+
when :COLON2
|
97
|
+
spot_colon2
|
98
|
+
|
99
|
+
when :COLON3
|
100
|
+
spot_vcall
|
101
|
+
|
102
|
+
when :OP_CDECL
|
103
|
+
spot_op_cdecl
|
104
|
+
end
|
105
|
+
|
106
|
+
if @line && @beg_column && @end_column && @beg_column < @end_column
|
107
|
+
return {
|
108
|
+
first_lineno: @beg_lineno,
|
109
|
+
first_column: @beg_column,
|
110
|
+
last_lineno: @end_lineno,
|
111
|
+
last_column: @end_column,
|
112
|
+
line: @line,
|
113
|
+
}
|
114
|
+
else
|
115
|
+
return nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# Example:
|
122
|
+
# x.foo
|
123
|
+
# ^^^^
|
124
|
+
# x.foo(42)
|
125
|
+
# ^^^^
|
126
|
+
# x&.foo
|
127
|
+
# ^^^^^
|
128
|
+
# x[42]
|
129
|
+
# ^^^^
|
130
|
+
# x += 1
|
131
|
+
# ^
|
132
|
+
def spot_call_for_name
|
133
|
+
nd_recv, mid, nd_args = @node.children
|
134
|
+
lineno = nd_recv.last_lineno
|
135
|
+
lines = @fetch[lineno, @node.last_lineno]
|
136
|
+
if mid == :[] && lines.match(/\G\s*(\[(?:\s*\])?)/, nd_recv.last_column)
|
137
|
+
@beg_column = $~.begin(1)
|
138
|
+
@line = lines[/.*\n/]
|
139
|
+
@beg_lineno = @end_lineno = lineno
|
140
|
+
if nd_args
|
141
|
+
if nd_recv.last_lineno == nd_args.last_lineno && @line.match(/\s*\]/, nd_args.last_column)
|
142
|
+
@end_column = $~.end(0)
|
143
|
+
end
|
144
|
+
else
|
145
|
+
if lines.match(/\G\s*?\[\s*\]/, nd_recv.last_column)
|
146
|
+
@end_column = $~.end(0)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
elsif lines.match(/\G\s*?(\&?\.)(\s*?)(#{ Regexp.quote(mid) }).*\n/, nd_recv.last_column)
|
150
|
+
lines = $` + $&
|
151
|
+
@beg_column = $~.begin($2.include?("\n") ? 3 : 1)
|
152
|
+
@end_column = $~.end(3)
|
153
|
+
if i = lines[..@beg_column].rindex("\n")
|
154
|
+
@beg_lineno = @end_lineno = lineno + lines[..@beg_column].count("\n")
|
155
|
+
@line = lines[i + 1..]
|
156
|
+
@beg_column -= i + 1
|
157
|
+
@end_column -= i + 1
|
158
|
+
else
|
159
|
+
@line = lines
|
160
|
+
@beg_lineno = @end_lineno = lineno
|
161
|
+
end
|
162
|
+
elsif mid.to_s =~ /\A\W+\z/ && lines.match(/\G\s*(#{ Regexp.quote(mid) })=.*\n/, nd_recv.last_column)
|
163
|
+
@line = $` + $&
|
164
|
+
@beg_column = $~.begin(1)
|
165
|
+
@end_column = $~.end(1)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Example:
|
170
|
+
# x.foo(42)
|
171
|
+
# ^^
|
172
|
+
# x[42]
|
173
|
+
# ^^
|
174
|
+
# x += 1
|
175
|
+
# ^
|
176
|
+
def spot_call_for_args
|
177
|
+
_nd_recv, _mid, nd_args = @node.children
|
178
|
+
if nd_args && nd_args.first_lineno == nd_args.last_lineno
|
179
|
+
fetch_line(nd_args.first_lineno)
|
180
|
+
@beg_column = nd_args.first_column
|
181
|
+
@end_column = nd_args.last_column
|
182
|
+
end
|
183
|
+
# TODO: support @arg
|
184
|
+
end
|
185
|
+
|
186
|
+
# Example:
|
187
|
+
# x.foo = 1
|
188
|
+
# ^^^^^^
|
189
|
+
# x[42] = 1
|
190
|
+
# ^^^^^^
|
191
|
+
def spot_attrasgn_for_name
|
192
|
+
nd_recv, mid, nd_args = @node.children
|
193
|
+
*nd_args, _nd_last_arg, _nil = nd_args.children
|
194
|
+
fetch_line(nd_recv.last_lineno)
|
195
|
+
if mid == :[]= && @line.match(/\G\s*(\[)/, nd_recv.last_column)
|
196
|
+
@beg_column = $~.begin(1)
|
197
|
+
args_last_column = $~.end(0)
|
198
|
+
if nd_args.last && nd_recv.last_lineno == nd_args.last.last_lineno
|
199
|
+
args_last_column = nd_args.last.last_column
|
200
|
+
end
|
201
|
+
if @line.match(/\s*\]\s*=/, args_last_column)
|
202
|
+
@end_column = $~.end(0)
|
203
|
+
end
|
204
|
+
elsif @line.match(/\G\s*(\.\s*#{ Regexp.quote(mid.to_s.sub(/=\z/, "")) }\s*=)/, nd_recv.last_column)
|
205
|
+
@beg_column = $~.begin(1)
|
206
|
+
@end_column = $~.end(1)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Example:
|
211
|
+
# x.foo = 1
|
212
|
+
# ^
|
213
|
+
# x[42] = 1
|
214
|
+
# ^^^^^^^
|
215
|
+
# x[] = 1
|
216
|
+
# ^^^^^
|
217
|
+
def spot_attrasgn_for_args
|
218
|
+
nd_recv, mid, nd_args = @node.children
|
219
|
+
fetch_line(nd_recv.last_lineno)
|
220
|
+
if mid == :[]= && @line.match(/\G\s*\[/, nd_recv.last_column)
|
221
|
+
@beg_column = $~.end(0)
|
222
|
+
if nd_recv.last_lineno == nd_args.last_lineno
|
223
|
+
@end_column = nd_args.last_column
|
224
|
+
end
|
225
|
+
elsif nd_args && nd_args.first_lineno == nd_args.last_lineno
|
226
|
+
@beg_column = nd_args.first_column
|
227
|
+
@end_column = nd_args.last_column
|
228
|
+
end
|
229
|
+
# TODO: support @arg
|
230
|
+
end
|
231
|
+
|
232
|
+
# Example:
|
233
|
+
# x + 1
|
234
|
+
# ^
|
235
|
+
# +x
|
236
|
+
# ^
|
237
|
+
def spot_opcall_for_name
|
238
|
+
nd_recv, op, nd_arg = @node.children
|
239
|
+
fetch_line(nd_recv.last_lineno)
|
240
|
+
if nd_arg
|
241
|
+
# binary operator
|
242
|
+
if @line.match(/\G\s*(#{ Regexp.quote(op) })/, nd_recv.last_column)
|
243
|
+
@beg_column = $~.begin(1)
|
244
|
+
@end_column = $~.end(1)
|
245
|
+
end
|
246
|
+
else
|
247
|
+
# unary operator
|
248
|
+
if @line[...nd_recv.first_column].match(/(#{ Regexp.quote(op.to_s.sub(/@\z/, "")) })\s*\(?\s*\z/)
|
249
|
+
@beg_column = $~.begin(1)
|
250
|
+
@end_column = $~.end(1)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Example:
|
256
|
+
# x + 1
|
257
|
+
# ^
|
258
|
+
def spot_opcall_for_args
|
259
|
+
_nd_recv, _op, nd_arg = @node.children
|
260
|
+
if nd_arg && nd_arg.first_lineno == nd_arg.last_lineno
|
261
|
+
# binary operator
|
262
|
+
fetch_line(nd_arg.first_lineno)
|
263
|
+
@beg_column = nd_arg.first_column
|
264
|
+
@end_column = nd_arg.last_column
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Example:
|
269
|
+
# foo(42)
|
270
|
+
# ^^^
|
271
|
+
# foo 42
|
272
|
+
# ^^^
|
273
|
+
def spot_fcall_for_name
|
274
|
+
mid, _nd_args = @node.children
|
275
|
+
fetch_line(@node.first_lineno)
|
276
|
+
if @line.match(/(#{ Regexp.quote(mid) })/, @node.first_column)
|
277
|
+
@beg_column = $~.begin(1)
|
278
|
+
@end_column = $~.end(1)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Example:
|
283
|
+
# foo(42)
|
284
|
+
# ^^
|
285
|
+
# foo 42
|
286
|
+
# ^^
|
287
|
+
def spot_fcall_for_args
|
288
|
+
_mid, nd_args = @node.children
|
289
|
+
if nd_args && nd_args.first_lineno == nd_args.last_lineno
|
290
|
+
# binary operator
|
291
|
+
fetch_line(nd_args.first_lineno)
|
292
|
+
@beg_column = nd_args.first_column
|
293
|
+
@end_column = nd_args.last_column
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Example:
|
298
|
+
# foo
|
299
|
+
# ^^^
|
300
|
+
def spot_vcall
|
301
|
+
if @node.first_lineno == @node.last_lineno
|
302
|
+
fetch_line(@node.last_lineno)
|
303
|
+
@beg_column = @node.first_column
|
304
|
+
@end_column = @node.last_column
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Example:
|
309
|
+
# x[1] += 42
|
310
|
+
# ^^^ (for [])
|
311
|
+
# x[1] += 42
|
312
|
+
# ^ (for +)
|
313
|
+
# x[1] += 42
|
314
|
+
# ^^^^^^ (for []=)
|
315
|
+
def spot_op_asgn1_for_name
|
316
|
+
nd_recv, op, nd_args, _nd_rhs = @node.children
|
317
|
+
fetch_line(nd_recv.last_lineno)
|
318
|
+
if @line.match(/\G\s*(\[)/, nd_recv.last_column)
|
319
|
+
bracket_beg_column = $~.begin(1)
|
320
|
+
args_last_column = $~.end(0)
|
321
|
+
if nd_args && nd_recv.last_lineno == nd_args.last_lineno
|
322
|
+
args_last_column = nd_args.last_column
|
323
|
+
end
|
324
|
+
if @line.match(/\s*\](\s*)(#{ Regexp.quote(op) })=()/, args_last_column)
|
325
|
+
case @name
|
326
|
+
when :[], :[]=
|
327
|
+
@beg_column = bracket_beg_column
|
328
|
+
@end_column = $~.begin(@name == :[] ? 1 : 3)
|
329
|
+
when op
|
330
|
+
@beg_column = $~.begin(2)
|
331
|
+
@end_column = $~.end(2)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Example:
|
338
|
+
# x[1] += 42
|
339
|
+
# ^^^^^^^^
|
340
|
+
def spot_op_asgn1_for_args
|
341
|
+
nd_recv, mid, nd_args, nd_rhs = @node.children
|
342
|
+
fetch_line(nd_recv.last_lineno)
|
343
|
+
if mid == :[]= && @line.match(/\G\s*\[/, nd_recv.last_column)
|
344
|
+
@beg_column = $~.end(0)
|
345
|
+
if nd_recv.last_lineno == nd_rhs.last_lineno
|
346
|
+
@end_column = nd_rhs.last_column
|
347
|
+
end
|
348
|
+
elsif nd_args && nd_args.first_lineno == nd_rhs.last_lineno
|
349
|
+
@beg_column = nd_args.first_column
|
350
|
+
@end_column = nd_rhs.last_column
|
351
|
+
end
|
352
|
+
# TODO: support @arg
|
353
|
+
end
|
354
|
+
|
355
|
+
# Example:
|
356
|
+
# x.foo += 42
|
357
|
+
# ^^^ (for foo)
|
358
|
+
# x.foo += 42
|
359
|
+
# ^ (for +)
|
360
|
+
# x.foo += 42
|
361
|
+
# ^^^^^^^ (for foo=)
|
362
|
+
def spot_op_asgn2_for_name
|
363
|
+
nd_recv, _qcall, attr, op, _nd_rhs = @node.children
|
364
|
+
fetch_line(nd_recv.last_lineno)
|
365
|
+
if @line.match(/\G\s*(\.)\s*#{ Regexp.quote(attr) }()\s*(#{ Regexp.quote(op) })(=)/, nd_recv.last_column)
|
366
|
+
case @name
|
367
|
+
when attr
|
368
|
+
@beg_column = $~.begin(1)
|
369
|
+
@end_column = $~.begin(2)
|
370
|
+
when op
|
371
|
+
@beg_column = $~.begin(3)
|
372
|
+
@end_column = $~.end(3)
|
373
|
+
when :"#{ attr }="
|
374
|
+
@beg_column = $~.begin(1)
|
375
|
+
@end_column = $~.end(4)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
# Example:
|
381
|
+
# x.foo += 42
|
382
|
+
# ^^
|
383
|
+
def spot_op_asgn2_for_args
|
384
|
+
_nd_recv, _qcall, _attr, _op, nd_rhs = @node.children
|
385
|
+
if nd_rhs.first_lineno == nd_rhs.last_lineno
|
386
|
+
fetch_line(nd_rhs.first_lineno)
|
387
|
+
@beg_column = nd_rhs.first_column
|
388
|
+
@end_column = nd_rhs.last_column
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# Example:
|
393
|
+
# Foo::Bar
|
394
|
+
# ^^^^^
|
395
|
+
def spot_colon2
|
396
|
+
nd_parent, const = @node.children
|
397
|
+
if nd_parent.last_lineno == @node.last_lineno
|
398
|
+
fetch_line(nd_parent.last_lineno)
|
399
|
+
@beg_column = nd_parent.last_column
|
400
|
+
@end_column = @node.last_column
|
401
|
+
else
|
402
|
+
@line = @fetch[@node.last_lineno]
|
403
|
+
if @line[...@node.last_column].match(/#{ Regexp.quote(const) }\z/)
|
404
|
+
@beg_column = $~.begin(0)
|
405
|
+
@end_column = $~.end(0)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Example:
|
411
|
+
# Foo::Bar += 1
|
412
|
+
# ^^^^^^^^
|
413
|
+
def spot_op_cdecl
|
414
|
+
nd_lhs, op, _nd_rhs = @node.children
|
415
|
+
*nd_parent_lhs, _const = nd_lhs.children
|
416
|
+
if @name == op
|
417
|
+
@line = @fetch[nd_lhs.last_lineno]
|
418
|
+
if @line.match(/\G\s*(#{ Regexp.quote(op) })=/, nd_lhs.last_column)
|
419
|
+
@beg_column = $~.begin(1)
|
420
|
+
@end_column = $~.end(1)
|
421
|
+
end
|
422
|
+
else
|
423
|
+
# constant access error
|
424
|
+
@end_column = nd_lhs.last_column
|
425
|
+
if nd_parent_lhs.empty? # example: ::C += 1
|
426
|
+
if nd_lhs.first_lineno == nd_lhs.last_lineno
|
427
|
+
@line = @fetch[nd_lhs.last_lineno]
|
428
|
+
@beg_column = nd_lhs.first_column
|
429
|
+
end
|
430
|
+
else # example: Foo::Bar::C += 1
|
431
|
+
if nd_parent_lhs.last.last_lineno == nd_lhs.last_lineno
|
432
|
+
@line = @fetch[nd_lhs.last_lineno]
|
433
|
+
@beg_column = nd_parent_lhs.last.last_column
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
def fetch_line(lineno)
|
440
|
+
@beg_lineno = @end_lineno = lineno
|
441
|
+
@line = @fetch[lineno]
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
private_constant :Spotter
|
446
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ErrorHighlight
|
2
|
+
module CoreExt
|
3
|
+
SKIP_TO_S_FOR_SUPER_LOOKUP = true
|
4
|
+
private_constant :SKIP_TO_S_FOR_SUPER_LOOKUP
|
5
|
+
|
6
|
+
def to_s
|
7
|
+
msg = super.dup
|
8
|
+
|
9
|
+
locs = backtrace_locations
|
10
|
+
return msg unless locs
|
11
|
+
|
12
|
+
loc = locs.first
|
13
|
+
begin
|
14
|
+
node = RubyVM::AbstractSyntaxTree.of(loc, save_script_lines: true)
|
15
|
+
opts = {}
|
16
|
+
|
17
|
+
case self
|
18
|
+
when NoMethodError, NameError
|
19
|
+
point = :name
|
20
|
+
opts[:name] = name
|
21
|
+
when TypeError, ArgumentError
|
22
|
+
point = :args
|
23
|
+
end
|
24
|
+
|
25
|
+
spot = ErrorHighlight.spot(node, point, **opts) do |lineno, last_lineno|
|
26
|
+
last_lineno ||= lineno
|
27
|
+
node.script_lines[lineno - 1 .. last_lineno - 1].join("")
|
28
|
+
end
|
29
|
+
|
30
|
+
rescue Errno::ENOENT
|
31
|
+
end
|
32
|
+
|
33
|
+
if spot
|
34
|
+
marker = " " * spot[:first_column] + "^" * (spot[:last_column] - spot[:first_column])
|
35
|
+
points = "\n\n#{ spot[:line] }#{ marker }"
|
36
|
+
msg << points if !msg.include?(points)
|
37
|
+
end
|
38
|
+
|
39
|
+
msg
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
NameError.prepend(CoreExt)
|
44
|
+
|
45
|
+
# temporarily disabled
|
46
|
+
#TypeError.prepend(CoreExt)
|
47
|
+
#ArgumentError.prepend(CoreExt)
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: error_highlight
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yusuke Endoh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-06-29 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: The gem enhances Exception#message by adding a short explanation where
|
14
|
+
the exception is raised
|
15
|
+
email:
|
16
|
+
- mame@ruby-lang.org
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".github/workflows/ruby.yml"
|
22
|
+
- ".gitignore"
|
23
|
+
- Gemfile
|
24
|
+
- LICENSE.txt
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- error_highlight.gemspec
|
28
|
+
- lib/error_highlight.rb
|
29
|
+
- lib/error_highlight/base.rb
|
30
|
+
- lib/error_highlight/core_ext.rb
|
31
|
+
- lib/error_highlight/version.rb
|
32
|
+
homepage: https://github.com/ruby/error_highlight
|
33
|
+
licenses:
|
34
|
+
- MIT
|
35
|
+
metadata: {}
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 3.1.0
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
requirements: []
|
51
|
+
rubygems_version: 3.3.0.dev
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: Shows a one-line code snippet with an underline in the error backtrace
|
55
|
+
test_files: []
|