error_highlight 0.6.0 → 0.7.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/.github/workflows/ruby.yml +21 -2
- data/.github/workflows/sync-ruby.yml +33 -0
- data/error_highlight.gemspec +1 -1
- data/lib/error_highlight/base.rb +414 -21
- data/lib/error_highlight/core_ext.rb +32 -3
- data/lib/error_highlight/formatter.rb +57 -6
- data/lib/error_highlight/version.rb +1 -1
- metadata +5 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6a847df9db6582ba1912ba3a2a64a76ba18e38d43c38413ac885fb993cbce07
|
|
4
|
+
data.tar.gz: c6e6872fd7276af7fac719d173da9f07af9fed0760d05385d1b6b6217449b9f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9699f20efc393a3fc7c09312a29818416bb255c3078c45a4a4f75deda3f2becefe415bc98565928a001ff88638433868d89656d9382b6b736a781f51d17e1901
|
|
7
|
+
data.tar.gz: 48410be2cb3bbfd0c241ded954d11c960abc551fc896c907ed05d6c49ea37ec116ee92556cd713c9b5fe2f033fe3eb16606e788b97a61196f97a2050f839ded2
|
data/.github/workflows/ruby.yml
CHANGED
|
@@ -9,13 +9,20 @@ on:
|
|
|
9
9
|
- 'master'
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
|
+
ruby-versions:
|
|
13
|
+
uses: ruby/actions/.github/workflows/ruby_versions.yml@master
|
|
14
|
+
with:
|
|
15
|
+
engine: cruby
|
|
16
|
+
min_version: 3.2
|
|
17
|
+
|
|
12
18
|
build:
|
|
19
|
+
needs: ruby-versions
|
|
13
20
|
runs-on: ubuntu-latest
|
|
14
21
|
strategy:
|
|
15
22
|
matrix:
|
|
16
|
-
ruby:
|
|
23
|
+
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
|
|
17
24
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
25
|
+
- uses: actions/checkout@v6
|
|
19
26
|
- uses: ruby/setup-ruby@v1
|
|
20
27
|
with:
|
|
21
28
|
ruby-version: ${{ matrix.ruby }}
|
|
@@ -25,3 +32,15 @@ jobs:
|
|
|
25
32
|
- name: Run the test suite
|
|
26
33
|
run: |
|
|
27
34
|
RUBYOPT=--disable-error_highlight bundle exec rake TESTOPT=-v
|
|
35
|
+
|
|
36
|
+
prism:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v6
|
|
40
|
+
- uses: ruby/setup-ruby@v1
|
|
41
|
+
with:
|
|
42
|
+
ruby-version: head
|
|
43
|
+
bundler-cache: true
|
|
44
|
+
- name: Run the test suite
|
|
45
|
+
run: |
|
|
46
|
+
RUBYOPT="--disable-error_highlight --parser=prism" bundle exec rake TESTOPT=-v
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Sync ruby
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [master]
|
|
5
|
+
jobs:
|
|
6
|
+
sync:
|
|
7
|
+
name: Sync ruby
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
if: ${{ github.repository_owner == 'ruby' }}
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
|
|
13
|
+
- name: Create GitHub App token
|
|
14
|
+
id: app-token
|
|
15
|
+
uses: actions/create-github-app-token@v2
|
|
16
|
+
with:
|
|
17
|
+
app-id: 2060836
|
|
18
|
+
private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }}
|
|
19
|
+
owner: ruby
|
|
20
|
+
repositories: ruby
|
|
21
|
+
|
|
22
|
+
- name: Sync to ruby/ruby
|
|
23
|
+
uses: convictional/trigger-workflow-and-wait@v1.6.5
|
|
24
|
+
with:
|
|
25
|
+
owner: ruby
|
|
26
|
+
repo: ruby
|
|
27
|
+
workflow_file_name: sync_default_gems.yml
|
|
28
|
+
github_token: ${{ steps.app-token.outputs.token }}
|
|
29
|
+
ref: master
|
|
30
|
+
client_payload: |
|
|
31
|
+
{"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"}
|
|
32
|
+
propagate_failure: true
|
|
33
|
+
wait_interval: 10
|
data/error_highlight.gemspec
CHANGED
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.homepage = "https://github.com/ruby/error_highlight"
|
|
19
19
|
|
|
20
20
|
spec.license = "MIT"
|
|
21
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 3.
|
|
21
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
|
22
22
|
|
|
23
23
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
24
24
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
data/lib/error_highlight/base.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
require_relative "version"
|
|
2
2
|
|
|
3
3
|
module ErrorHighlight
|
|
4
|
-
# Identify the code fragment
|
|
4
|
+
# Identify the code fragment where a given exception occurred.
|
|
5
5
|
#
|
|
6
6
|
# Options:
|
|
7
7
|
#
|
|
8
8
|
# point_type: :name | :args
|
|
9
|
-
# :name (default) points the method/variable name
|
|
10
|
-
# :args points the arguments of the method call
|
|
9
|
+
# :name (default) points to the method/variable name where the exception occurred.
|
|
10
|
+
# :args points to the arguments of the method call where the exception occurred.
|
|
11
11
|
#
|
|
12
12
|
# backtrace_location: Thread::Backtrace::Location
|
|
13
13
|
# It locates the code fragment of the given backtrace_location.
|
|
@@ -54,11 +54,20 @@ module ErrorHighlight
|
|
|
54
54
|
|
|
55
55
|
return nil unless Thread::Backtrace::Location === loc
|
|
56
56
|
|
|
57
|
-
node =
|
|
57
|
+
node =
|
|
58
|
+
begin
|
|
59
|
+
RubyVM::AbstractSyntaxTree.of(loc, keep_script_lines: true)
|
|
60
|
+
rescue RuntimeError => error
|
|
61
|
+
# RubyVM::AbstractSyntaxTree.of raises an error with a message that
|
|
62
|
+
# includes "prism" when the ISEQ was compiled with the prism compiler.
|
|
63
|
+
# In this case, we'll try to parse again with prism instead.
|
|
64
|
+
raise unless error.message.include?("prism")
|
|
65
|
+
prism_find(loc)
|
|
66
|
+
end
|
|
58
67
|
|
|
59
68
|
Spotter.new(node, **opts).spot
|
|
60
69
|
|
|
61
|
-
when RubyVM::AbstractSyntaxTree::Node
|
|
70
|
+
when RubyVM::AbstractSyntaxTree::Node, Prism::Node
|
|
62
71
|
Spotter.new(obj, **opts).spot
|
|
63
72
|
|
|
64
73
|
else
|
|
@@ -72,6 +81,21 @@ module ErrorHighlight
|
|
|
72
81
|
return nil
|
|
73
82
|
end
|
|
74
83
|
|
|
84
|
+
# Accepts a Thread::Backtrace::Location object and returns a Prism::Node
|
|
85
|
+
# corresponding to the backtrace location in the source code.
|
|
86
|
+
def self.prism_find(location)
|
|
87
|
+
require "prism"
|
|
88
|
+
return nil if Prism::VERSION < "1.0.0"
|
|
89
|
+
|
|
90
|
+
absolute_path = location.absolute_path
|
|
91
|
+
return unless absolute_path
|
|
92
|
+
|
|
93
|
+
node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
|
|
94
|
+
Prism.parse_file(absolute_path).value.breadth_first_search { |node| node.node_id == node_id }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private_class_method :prism_find
|
|
98
|
+
|
|
75
99
|
class Spotter
|
|
76
100
|
class NonAscii < Exception; end
|
|
77
101
|
private_constant :NonAscii
|
|
@@ -89,7 +113,7 @@ module ErrorHighlight
|
|
|
89
113
|
snippet = @node.script_lines[lineno - 1 .. last_lineno - 1].join("")
|
|
90
114
|
snippet += "\n" unless snippet.end_with?("\n")
|
|
91
115
|
|
|
92
|
-
# It
|
|
116
|
+
# It requires some work to support Unicode (or multibyte) characters.
|
|
93
117
|
# Tentatively, we stop highlighting if the code snippet has non-ascii characters.
|
|
94
118
|
# See https://github.com/ruby/error_highlight/issues/4
|
|
95
119
|
raise NonAscii unless snippet.ascii_only?
|
|
@@ -98,19 +122,17 @@ module ErrorHighlight
|
|
|
98
122
|
end
|
|
99
123
|
end
|
|
100
124
|
|
|
101
|
-
OPT_GETCONSTANT_PATH = (RUBY_VERSION.split(".").map {|s| s.to_i } <=> [3, 2]) >= 0
|
|
102
|
-
private_constant :OPT_GETCONSTANT_PATH
|
|
103
|
-
|
|
104
125
|
def spot
|
|
105
126
|
return nil unless @node
|
|
106
127
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
# In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`)
|
|
129
|
+
# is compiled to one instruction (opt_getconstant_path).
|
|
130
|
+
# @node points to the node of the whole `Foo::Bar::Baz` even if `Foo`
|
|
131
|
+
# or `Foo::Bar` causes NameError.
|
|
132
|
+
# So we try to spot the sub-node that causes the NameError by using
|
|
133
|
+
# `NameError#name`.
|
|
134
|
+
case @node.type
|
|
135
|
+
when :COLON2
|
|
114
136
|
subnodes = []
|
|
115
137
|
node = @node
|
|
116
138
|
while node.type == :COLON2
|
|
@@ -130,6 +152,21 @@ module ErrorHighlight
|
|
|
130
152
|
# Do nothing; opt_getconstant_path is used only when the const base is
|
|
131
153
|
# NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`)
|
|
132
154
|
end
|
|
155
|
+
when :constant_path_node
|
|
156
|
+
subnodes = []
|
|
157
|
+
node = @node
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
subnodes << node if node.name == @name
|
|
161
|
+
end while (node = node.parent).is_a?(Prism::ConstantPathNode)
|
|
162
|
+
|
|
163
|
+
if node.is_a?(Prism::ConstantReadNode) && node.name == @name
|
|
164
|
+
subnodes << node
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# If we found only one sub-node whose name is equal to @name, use it
|
|
168
|
+
return nil if subnodes.size != 1
|
|
169
|
+
@node = subnodes.first
|
|
133
170
|
end
|
|
134
171
|
|
|
135
172
|
case @node.type
|
|
@@ -196,6 +233,86 @@ module ErrorHighlight
|
|
|
196
233
|
|
|
197
234
|
when :OP_CDECL
|
|
198
235
|
spot_op_cdecl
|
|
236
|
+
|
|
237
|
+
when :DEFN
|
|
238
|
+
raise NotImplementedError if @point_type != :name
|
|
239
|
+
spot_defn
|
|
240
|
+
|
|
241
|
+
when :DEFS
|
|
242
|
+
raise NotImplementedError if @point_type != :name
|
|
243
|
+
spot_defs
|
|
244
|
+
|
|
245
|
+
when :LAMBDA
|
|
246
|
+
spot_lambda
|
|
247
|
+
|
|
248
|
+
when :ITER
|
|
249
|
+
spot_iter
|
|
250
|
+
|
|
251
|
+
when :call_node
|
|
252
|
+
case @point_type
|
|
253
|
+
when :name
|
|
254
|
+
prism_spot_call_for_name
|
|
255
|
+
when :args
|
|
256
|
+
prism_spot_call_for_args
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
when :local_variable_operator_write_node
|
|
260
|
+
case @point_type
|
|
261
|
+
when :name
|
|
262
|
+
prism_spot_local_variable_operator_write_for_name
|
|
263
|
+
when :args
|
|
264
|
+
prism_spot_local_variable_operator_write_for_args
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
when :call_operator_write_node
|
|
268
|
+
case @point_type
|
|
269
|
+
when :name
|
|
270
|
+
prism_spot_call_operator_write_for_name
|
|
271
|
+
when :args
|
|
272
|
+
prism_spot_call_operator_write_for_args
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
when :index_operator_write_node
|
|
276
|
+
case @point_type
|
|
277
|
+
when :name
|
|
278
|
+
prism_spot_index_operator_write_for_name
|
|
279
|
+
when :args
|
|
280
|
+
prism_spot_index_operator_write_for_args
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
when :constant_read_node
|
|
284
|
+
prism_spot_constant_read
|
|
285
|
+
|
|
286
|
+
when :constant_path_node
|
|
287
|
+
prism_spot_constant_path
|
|
288
|
+
|
|
289
|
+
when :constant_path_operator_write_node
|
|
290
|
+
prism_spot_constant_path_operator_write
|
|
291
|
+
|
|
292
|
+
when :def_node
|
|
293
|
+
case @point_type
|
|
294
|
+
when :name
|
|
295
|
+
prism_spot_def_for_name
|
|
296
|
+
when :args
|
|
297
|
+
raise NotImplementedError
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
when :lambda_node
|
|
301
|
+
case @point_type
|
|
302
|
+
when :name
|
|
303
|
+
prism_spot_lambda_for_name
|
|
304
|
+
when :args
|
|
305
|
+
raise NotImplementedError
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
when :block_node
|
|
309
|
+
case @point_type
|
|
310
|
+
when :name
|
|
311
|
+
prism_spot_block_for_name
|
|
312
|
+
when :args
|
|
313
|
+
raise NotImplementedError
|
|
314
|
+
end
|
|
315
|
+
|
|
199
316
|
end
|
|
200
317
|
|
|
201
318
|
if @snippet && @beg_column && @end_column && @beg_column < @end_column
|
|
@@ -260,6 +377,7 @@ module ErrorHighlight
|
|
|
260
377
|
end
|
|
261
378
|
elsif mid.to_s =~ /\A\W+\z/ && lines.match(/\G\s*(#{ Regexp.quote(mid) })=.*\n/, nd_recv.last_column)
|
|
262
379
|
@snippet = $` + $&
|
|
380
|
+
@beg_lineno = @end_lineno = lineno
|
|
263
381
|
@beg_column = $~.begin(1)
|
|
264
382
|
@end_column = $~.end(1)
|
|
265
383
|
end
|
|
@@ -386,7 +504,6 @@ module ErrorHighlight
|
|
|
386
504
|
def spot_fcall_for_args
|
|
387
505
|
_mid, nd_args = @node.children
|
|
388
506
|
if nd_args && nd_args.first_lineno == nd_args.last_lineno
|
|
389
|
-
# binary operator
|
|
390
507
|
fetch_line(nd_args.first_lineno)
|
|
391
508
|
@beg_column = nd_args.first_column
|
|
392
509
|
@end_column = nd_args.last_column
|
|
@@ -498,8 +615,9 @@ module ErrorHighlight
|
|
|
498
615
|
@beg_column = nd_parent.last_column
|
|
499
616
|
@end_column = @node.last_column
|
|
500
617
|
else
|
|
501
|
-
@
|
|
618
|
+
fetch_line(@node.last_lineno)
|
|
502
619
|
if @snippet[...@node.last_column].match(/#{ Regexp.quote(const) }\z/)
|
|
620
|
+
@beg_lineno = @end_lineno = @node.last_lineno
|
|
503
621
|
@beg_column = $~.begin(0)
|
|
504
622
|
@end_column = $~.end(0)
|
|
505
623
|
end
|
|
@@ -513,7 +631,7 @@ module ErrorHighlight
|
|
|
513
631
|
nd_lhs, op, _nd_rhs = @node.children
|
|
514
632
|
*nd_parent_lhs, _const = nd_lhs.children
|
|
515
633
|
if @name == op
|
|
516
|
-
|
|
634
|
+
fetch_line(nd_lhs.last_lineno)
|
|
517
635
|
if @snippet.match(/\G\s*(#{ Regexp.quote(op) })=/, nd_lhs.last_column)
|
|
518
636
|
@beg_column = $~.begin(1)
|
|
519
637
|
@end_column = $~.end(1)
|
|
@@ -523,22 +641,297 @@ module ErrorHighlight
|
|
|
523
641
|
@end_column = nd_lhs.last_column
|
|
524
642
|
if nd_parent_lhs.empty? # example: ::C += 1
|
|
525
643
|
if nd_lhs.first_lineno == nd_lhs.last_lineno
|
|
526
|
-
|
|
644
|
+
fetch_line(nd_lhs.last_lineno)
|
|
527
645
|
@beg_column = nd_lhs.first_column
|
|
528
646
|
end
|
|
529
647
|
else # example: Foo::Bar::C += 1
|
|
530
648
|
if nd_parent_lhs.last.last_lineno == nd_lhs.last_lineno
|
|
531
|
-
|
|
649
|
+
fetch_line(nd_lhs.last_lineno)
|
|
532
650
|
@beg_column = nd_parent_lhs.last.last_column
|
|
533
651
|
end
|
|
534
652
|
end
|
|
535
653
|
end
|
|
536
654
|
end
|
|
537
655
|
|
|
656
|
+
# Example:
|
|
657
|
+
# def bar; end
|
|
658
|
+
# ^^^
|
|
659
|
+
def spot_defn
|
|
660
|
+
mid, = @node.children
|
|
661
|
+
fetch_line(@node.first_lineno)
|
|
662
|
+
if @snippet.match(/\Gdef\s+(#{ Regexp.quote(mid) }\b)/, @node.first_column)
|
|
663
|
+
@beg_column = $~.begin(1)
|
|
664
|
+
@end_column = $~.end(1)
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Example:
|
|
669
|
+
# def Foo.bar; end
|
|
670
|
+
# ^^^^
|
|
671
|
+
def spot_defs
|
|
672
|
+
nd_recv, mid, = @node.children
|
|
673
|
+
fetch_line(nd_recv.last_lineno)
|
|
674
|
+
if @snippet.match(/\G\s*(\.\s*#{ Regexp.quote(mid) }\b)/, nd_recv.last_column)
|
|
675
|
+
@beg_column = $~.begin(1)
|
|
676
|
+
@end_column = $~.end(1)
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# Example:
|
|
681
|
+
# -> { ... }
|
|
682
|
+
# ^^
|
|
683
|
+
def spot_lambda
|
|
684
|
+
fetch_line(@node.first_lineno)
|
|
685
|
+
if @snippet.match(/\G->/, @node.first_column)
|
|
686
|
+
@beg_column = $~.begin(0)
|
|
687
|
+
@end_column = $~.end(0)
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Example:
|
|
692
|
+
# lambda { ... }
|
|
693
|
+
# ^
|
|
694
|
+
# define_method :foo do
|
|
695
|
+
# ^^
|
|
696
|
+
def spot_iter
|
|
697
|
+
_nd_fcall, nd_scope = @node.children
|
|
698
|
+
fetch_line(nd_scope.first_lineno)
|
|
699
|
+
if @snippet.match(/\G(?:do\b|\{)/, nd_scope.first_column)
|
|
700
|
+
@beg_column = $~.begin(0)
|
|
701
|
+
@end_column = $~.end(0)
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
538
705
|
def fetch_line(lineno)
|
|
539
706
|
@beg_lineno = @end_lineno = lineno
|
|
540
707
|
@snippet = @fetch[lineno]
|
|
541
708
|
end
|
|
709
|
+
|
|
710
|
+
# Take a location from the prism parser and set the necessary instance
|
|
711
|
+
# variables.
|
|
712
|
+
def prism_location(location)
|
|
713
|
+
@beg_lineno = location.start_line
|
|
714
|
+
@beg_column = location.start_column
|
|
715
|
+
@end_lineno = location.end_line
|
|
716
|
+
@end_column = location.end_column
|
|
717
|
+
@snippet = @fetch[@beg_lineno, @end_lineno]
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Example:
|
|
721
|
+
# x.foo
|
|
722
|
+
# ^^^^
|
|
723
|
+
# x.foo(42)
|
|
724
|
+
# ^^^^
|
|
725
|
+
# x&.foo
|
|
726
|
+
# ^^^^^
|
|
727
|
+
# x[42]
|
|
728
|
+
# ^^^^
|
|
729
|
+
# x.foo = 1
|
|
730
|
+
# ^^^^^^
|
|
731
|
+
# x[42] = 1
|
|
732
|
+
# ^^^^^^
|
|
733
|
+
# x + 1
|
|
734
|
+
# ^
|
|
735
|
+
# +x
|
|
736
|
+
# ^
|
|
737
|
+
# foo(42)
|
|
738
|
+
# ^^^
|
|
739
|
+
# foo 42
|
|
740
|
+
# ^^^
|
|
741
|
+
# foo
|
|
742
|
+
# ^^^
|
|
743
|
+
def prism_spot_call_for_name
|
|
744
|
+
# Explicitly turn off foo.() syntax because error_highlight expects this
|
|
745
|
+
# to not work.
|
|
746
|
+
return nil if @node.name == :call && @node.message_loc.nil?
|
|
747
|
+
|
|
748
|
+
location = @node.message_loc || @node.call_operator_loc || @node.location
|
|
749
|
+
location = @node.call_operator_loc.join(location) if @node.call_operator_loc&.start_line == location.start_line
|
|
750
|
+
|
|
751
|
+
# If the method name ends with "=" but the message does not, then this is
|
|
752
|
+
# a method call using the "attribute assignment" syntax
|
|
753
|
+
# (e.g., foo.bar = 1). In this case we need to go retrieve the = sign and
|
|
754
|
+
# add it to the location.
|
|
755
|
+
if (name = @node.name).end_with?("=") && !@node.message.end_with?("=")
|
|
756
|
+
location = location.adjoin("=")
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
prism_location(location)
|
|
760
|
+
|
|
761
|
+
if !name.end_with?("=") && !name.match?(/[[:alpha:]_\[]/)
|
|
762
|
+
# If the method name is an operator, then error_highlight only
|
|
763
|
+
# highlights the first line.
|
|
764
|
+
fetch_line(location.start_line)
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# Example:
|
|
769
|
+
# x.foo(42)
|
|
770
|
+
# ^^
|
|
771
|
+
# x[42]
|
|
772
|
+
# ^^
|
|
773
|
+
# x.foo = 1
|
|
774
|
+
# ^
|
|
775
|
+
# x[42] = 1
|
|
776
|
+
# ^^^^^^^
|
|
777
|
+
# x[] = 1
|
|
778
|
+
# ^^^^^
|
|
779
|
+
# x + 1
|
|
780
|
+
# ^
|
|
781
|
+
# foo(42)
|
|
782
|
+
# ^^
|
|
783
|
+
# foo 42
|
|
784
|
+
# ^^
|
|
785
|
+
def prism_spot_call_for_args
|
|
786
|
+
# Disallow highlighting arguments if there are no arguments.
|
|
787
|
+
return if @node.arguments.nil?
|
|
788
|
+
|
|
789
|
+
# Explicitly turn off foo.() syntax because error_highlight expects this
|
|
790
|
+
# to not work.
|
|
791
|
+
return nil if @node.name == :call && @node.message_loc.nil?
|
|
792
|
+
|
|
793
|
+
if @node.name == :[]= && @node.opening == "[" && (@node.arguments&.arguments || []).length == 1
|
|
794
|
+
prism_location(@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1).join(@node.arguments.location))
|
|
795
|
+
else
|
|
796
|
+
prism_location(@node.arguments.location)
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Example:
|
|
801
|
+
# x += 1
|
|
802
|
+
# ^
|
|
803
|
+
def prism_spot_local_variable_operator_write_for_name
|
|
804
|
+
prism_location(@node.binary_operator_loc.chop)
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Example:
|
|
808
|
+
# x += 1
|
|
809
|
+
# ^
|
|
810
|
+
def prism_spot_local_variable_operator_write_for_args
|
|
811
|
+
prism_location(@node.value.location)
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# Example:
|
|
815
|
+
# x.foo += 42
|
|
816
|
+
# ^^^ (for foo)
|
|
817
|
+
# x.foo += 42
|
|
818
|
+
# ^ (for +)
|
|
819
|
+
# x.foo += 42
|
|
820
|
+
# ^^^^^^^ (for foo=)
|
|
821
|
+
def prism_spot_call_operator_write_for_name
|
|
822
|
+
if !@name.start_with?(/[[:alpha:]_]/)
|
|
823
|
+
prism_location(@node.binary_operator_loc.chop)
|
|
824
|
+
else
|
|
825
|
+
location = @node.message_loc
|
|
826
|
+
if @node.call_operator_loc.start_line == location.start_line
|
|
827
|
+
location = @node.call_operator_loc.join(location)
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
location = location.adjoin("=") if @name.end_with?("=")
|
|
831
|
+
prism_location(location)
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Example:
|
|
836
|
+
# x.foo += 42
|
|
837
|
+
# ^^
|
|
838
|
+
def prism_spot_call_operator_write_for_args
|
|
839
|
+
prism_location(@node.value.location)
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# Example:
|
|
843
|
+
# x[1] += 42
|
|
844
|
+
# ^^^ (for [])
|
|
845
|
+
# x[1] += 42
|
|
846
|
+
# ^ (for +)
|
|
847
|
+
# x[1] += 42
|
|
848
|
+
# ^^^^^^ (for []=)
|
|
849
|
+
def prism_spot_index_operator_write_for_name
|
|
850
|
+
case @name
|
|
851
|
+
when :[]
|
|
852
|
+
prism_location(@node.opening_loc.join(@node.closing_loc))
|
|
853
|
+
when :[]=
|
|
854
|
+
prism_location(@node.opening_loc.join(@node.closing_loc).adjoin("="))
|
|
855
|
+
else
|
|
856
|
+
# Explicitly turn off foo[] += 1 syntax when the operator is not on
|
|
857
|
+
# the same line because error_highlight expects this to not work.
|
|
858
|
+
return nil if @node.binary_operator_loc.start_line != @node.opening_loc.start_line
|
|
859
|
+
|
|
860
|
+
prism_location(@node.binary_operator_loc.chop)
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# Example:
|
|
865
|
+
# x[1] += 42
|
|
866
|
+
# ^^^^^^^^
|
|
867
|
+
def prism_spot_index_operator_write_for_args
|
|
868
|
+
opening_loc =
|
|
869
|
+
if @node.arguments.nil?
|
|
870
|
+
@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1)
|
|
871
|
+
else
|
|
872
|
+
@node.arguments.location
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
prism_location(opening_loc.join(@node.value.location))
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
# Example:
|
|
879
|
+
# Foo
|
|
880
|
+
# ^^^
|
|
881
|
+
def prism_spot_constant_read
|
|
882
|
+
prism_location(@node.location)
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Example:
|
|
886
|
+
# Foo::Bar
|
|
887
|
+
# ^^^^^
|
|
888
|
+
def prism_spot_constant_path
|
|
889
|
+
if @node.parent && @node.parent.location.end_line == @node.location.end_line
|
|
890
|
+
fetch_line(@node.parent.location.end_line)
|
|
891
|
+
prism_location(@node.delimiter_loc.join(@node.name_loc))
|
|
892
|
+
else
|
|
893
|
+
fetch_line(@node.location.end_line)
|
|
894
|
+
location = @node.name_loc
|
|
895
|
+
location = @node.delimiter_loc.join(location) if @node.delimiter_loc.end_line == location.start_line
|
|
896
|
+
prism_location(location)
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# Example:
|
|
901
|
+
# Foo::Bar += 1
|
|
902
|
+
# ^^^^^^^^
|
|
903
|
+
def prism_spot_constant_path_operator_write
|
|
904
|
+
if @name == (target = @node.target).name
|
|
905
|
+
prism_location(target.delimiter_loc.join(target.name_loc))
|
|
906
|
+
else
|
|
907
|
+
prism_location(@node.binary_operator_loc.chop)
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
# Example:
|
|
912
|
+
# def foo()
|
|
913
|
+
# ^^^
|
|
914
|
+
def prism_spot_def_for_name
|
|
915
|
+
location = @node.name_loc
|
|
916
|
+
location = @node.operator_loc.join(location) if @node.operator_loc
|
|
917
|
+
prism_location(location)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# Example:
|
|
921
|
+
# -> x, y { }
|
|
922
|
+
# ^^
|
|
923
|
+
def prism_spot_lambda_for_name
|
|
924
|
+
prism_location(@node.operator_loc)
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
# Example:
|
|
928
|
+
# lambda { }
|
|
929
|
+
# ^
|
|
930
|
+
# define_method :foo do |x, y|
|
|
931
|
+
# ^
|
|
932
|
+
def prism_spot_block_for_name
|
|
933
|
+
prism_location(@node.opening_loc)
|
|
934
|
+
end
|
|
542
935
|
end
|
|
543
936
|
|
|
544
937
|
private_constant :Spotter
|
|
@@ -3,9 +3,38 @@ require_relative "formatter"
|
|
|
3
3
|
module ErrorHighlight
|
|
4
4
|
module CoreExt
|
|
5
5
|
private def generate_snippet
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
if ArgumentError === self && message =~ /\A(?:wrong number of arguments|missing keyword[s]?|unknown keyword[s]?|no keywords accepted)\b/
|
|
7
|
+
locs = self.backtrace_locations
|
|
8
|
+
return "" if locs.size < 2
|
|
9
|
+
callee_loc, caller_loc = locs
|
|
10
|
+
callee_spot = ErrorHighlight.spot(self, backtrace_location: callee_loc, point_type: :name)
|
|
11
|
+
caller_spot = ErrorHighlight.spot(self, backtrace_location: caller_loc, point_type: :name)
|
|
12
|
+
if caller_spot && callee_spot &&
|
|
13
|
+
caller_loc.path == callee_loc.path &&
|
|
14
|
+
caller_loc.lineno == callee_loc.lineno &&
|
|
15
|
+
caller_spot == callee_spot
|
|
16
|
+
callee_loc = callee_spot = nil
|
|
17
|
+
end
|
|
18
|
+
ret = +"\n"
|
|
19
|
+
[["caller", caller_loc, caller_spot], ["callee", callee_loc, callee_spot]].each do |header, loc, spot|
|
|
20
|
+
out = nil
|
|
21
|
+
if loc
|
|
22
|
+
out = " #{ header }: #{ loc.path }:#{ loc.lineno }"
|
|
23
|
+
if spot
|
|
24
|
+
_, _, snippet, highlight = ErrorHighlight.formatter.message_for(spot).lines
|
|
25
|
+
out += "\n | #{ snippet } #{ highlight }"
|
|
26
|
+
else
|
|
27
|
+
# do nothing
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
ret << "\n" + out if out
|
|
31
|
+
end
|
|
32
|
+
ret
|
|
33
|
+
else
|
|
34
|
+
spot = ErrorHighlight.spot(self)
|
|
35
|
+
return "" unless spot
|
|
36
|
+
return ErrorHighlight.formatter.message_for(spot)
|
|
37
|
+
end
|
|
9
38
|
end
|
|
10
39
|
|
|
11
40
|
if Exception.method_defined?(:detailed_message)
|
|
@@ -1,15 +1,66 @@
|
|
|
1
1
|
module ErrorHighlight
|
|
2
2
|
class DefaultFormatter
|
|
3
|
+
MIN_SNIPPET_WIDTH = 20
|
|
4
|
+
|
|
3
5
|
def self.message_for(spot)
|
|
4
6
|
# currently only a one-line code snippet is supported
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
return "" unless spot[:first_lineno] == spot[:last_lineno]
|
|
8
|
+
|
|
9
|
+
snippet = spot[:snippet]
|
|
10
|
+
first_column = spot[:first_column]
|
|
11
|
+
last_column = spot[:last_column]
|
|
12
|
+
ellipsis = "..."
|
|
13
|
+
|
|
14
|
+
# truncate snippet to fit in the viewport
|
|
15
|
+
if max_snippet_width && snippet.size > max_snippet_width
|
|
16
|
+
available_width = max_snippet_width - ellipsis.size
|
|
17
|
+
center = first_column - max_snippet_width / 2
|
|
18
|
+
|
|
19
|
+
visible_start = last_column < available_width ? 0 : [center, 0].max
|
|
20
|
+
visible_end = visible_start + max_snippet_width
|
|
21
|
+
visible_start = snippet.size - max_snippet_width if visible_end > snippet.size
|
|
22
|
+
|
|
23
|
+
prefix = visible_start.positive? ? ellipsis : ""
|
|
24
|
+
suffix = visible_end < snippet.size ? ellipsis : ""
|
|
8
25
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
26
|
+
snippet = prefix + snippet[(visible_start + prefix.size)...(visible_end - suffix.size)] + suffix
|
|
27
|
+
snippet << "\n" unless snippet.end_with?("\n")
|
|
28
|
+
|
|
29
|
+
first_column -= visible_start
|
|
30
|
+
last_column = [last_column - visible_start, snippet.size - 1].min
|
|
12
31
|
end
|
|
32
|
+
|
|
33
|
+
indent = snippet[0...first_column].gsub(/[^\t]/, " ")
|
|
34
|
+
marker = indent + "^" * (last_column - first_column)
|
|
35
|
+
|
|
36
|
+
"\n\n#{ snippet }#{ marker }"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.max_snippet_width
|
|
40
|
+
return if Ractor.current[:__error_highlight_max_snippet_width__] == :disabled
|
|
41
|
+
|
|
42
|
+
Ractor.current[:__error_highlight_max_snippet_width__] ||= terminal_width
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.max_snippet_width=(width)
|
|
46
|
+
return Ractor.current[:__error_highlight_max_snippet_width__] = :disabled if width.nil?
|
|
47
|
+
|
|
48
|
+
width = width.to_i
|
|
49
|
+
|
|
50
|
+
if width < MIN_SNIPPET_WIDTH
|
|
51
|
+
warn "'max_snippet_width' adjusted to minimum value of #{MIN_SNIPPET_WIDTH}."
|
|
52
|
+
width = MIN_SNIPPET_WIDTH
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Ractor.current[:__error_highlight_max_snippet_width__] = width
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.terminal_width
|
|
59
|
+
# lazy load io/console to avoid loading it when 'max_snippet_width' is manually set
|
|
60
|
+
require "io/console"
|
|
61
|
+
$stderr.winsize[1] if $stderr.tty?
|
|
62
|
+
rescue LoadError, NoMethodError, SystemCallError
|
|
63
|
+
# skip truncation when terminal window size is unavailable
|
|
13
64
|
end
|
|
14
65
|
end
|
|
15
66
|
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: error_highlight
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Endoh
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: The gem enhances Exception#message by adding a short explanation where
|
|
14
13
|
the exception is raised
|
|
@@ -20,6 +19,7 @@ extra_rdoc_files: []
|
|
|
20
19
|
files:
|
|
21
20
|
- ".github/dependabot.yml"
|
|
22
21
|
- ".github/workflows/ruby.yml"
|
|
22
|
+
- ".github/workflows/sync-ruby.yml"
|
|
23
23
|
- ".gitignore"
|
|
24
24
|
- Gemfile
|
|
25
25
|
- LICENSE.txt
|
|
@@ -35,7 +35,6 @@ homepage: https://github.com/ruby/error_highlight
|
|
|
35
35
|
licenses:
|
|
36
36
|
- MIT
|
|
37
37
|
metadata: {}
|
|
38
|
-
post_install_message:
|
|
39
38
|
rdoc_options: []
|
|
40
39
|
require_paths:
|
|
41
40
|
- lib
|
|
@@ -43,15 +42,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
43
42
|
requirements:
|
|
44
43
|
- - ">="
|
|
45
44
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 3.
|
|
45
|
+
version: 3.2.0
|
|
47
46
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
47
|
requirements:
|
|
49
48
|
- - ">="
|
|
50
49
|
- !ruby/object:Gem::Version
|
|
51
50
|
version: '0'
|
|
52
51
|
requirements: []
|
|
53
|
-
rubygems_version:
|
|
54
|
-
signing_key:
|
|
52
|
+
rubygems_version: 4.0.1
|
|
55
53
|
specification_version: 4
|
|
56
54
|
summary: Shows a one-line code snippet with an underline in the error backtrace
|
|
57
55
|
test_files: []
|