error_highlight 0.6.0 → 0.7.0
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 +20 -1
- data/lib/error_highlight/base.rb +300 -15
- data/lib/error_highlight/formatter.rb +57 -6
- data/lib/error_highlight/version.rb +1 -1
- metadata +4 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c61fd8e89a64b337d10859ca487015ad69c87555057ce7f15b274ce799a5beb3
|
4
|
+
data.tar.gz: 946058938dc70f583672f9adf1450a1514e4922638e051b2755545017f266963
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5a627e7b0a5710be27e62444a5127353e592cb5dd075ec0a25aa957fdf92e7c3e730b30a0fe5b4d1d35eabd9cde9dd6abafbc22a3f071ad7af8f39f991ee634
|
7
|
+
data.tar.gz: 6e72a445acf82b23d8a6deb6537504dfe64b50d74a808ff33104da5dccad6970256f115ef1dc786a25136457ce18b801d0d466b7b8c8dac4ba0d70ab3b5732cc
|
data/.github/workflows/ruby.yml
CHANGED
@@ -9,11 +9,18 @@ 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.1
|
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
25
|
- uses: actions/checkout@v4
|
19
26
|
- uses: ruby/setup-ruby@v1
|
@@ -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@v4
|
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
|
data/lib/error_highlight/base.rb
CHANGED
@@ -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
|
@@ -104,31 +128,49 @@ module ErrorHighlight
|
|
104
128
|
def spot
|
105
129
|
return nil unless @node
|
106
130
|
|
107
|
-
if OPT_GETCONSTANT_PATH
|
131
|
+
if OPT_GETCONSTANT_PATH
|
108
132
|
# In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`)
|
109
133
|
# is compiled to one instruction (opt_getconstant_path).
|
110
134
|
# @node points to the node of the whole `Foo::Bar::Baz` even if `Foo`
|
111
135
|
# or `Foo::Bar` causes NameError.
|
112
136
|
# So we try to spot the sub-node that causes the NameError by using
|
113
137
|
# `NameError#name`.
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
138
|
+
case @node.type
|
139
|
+
when :COLON2
|
140
|
+
subnodes = []
|
141
|
+
node = @node
|
142
|
+
while node.type == :COLON2
|
143
|
+
node2, const = node.children
|
144
|
+
subnodes << node if const == @name
|
145
|
+
node = node2
|
146
|
+
end
|
147
|
+
if node.type == :CONST || node.type == :COLON3
|
148
|
+
if node.children.first == @name
|
149
|
+
subnodes << node
|
150
|
+
end
|
151
|
+
|
152
|
+
# If we found only one sub-node whose name is equal to @name, use it
|
153
|
+
return nil if subnodes.size != 1
|
154
|
+
@node = subnodes.first
|
155
|
+
else
|
156
|
+
# Do nothing; opt_getconstant_path is used only when the const base is
|
157
|
+
# NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`)
|
158
|
+
end
|
159
|
+
when :constant_path_node
|
160
|
+
subnodes = []
|
161
|
+
node = @node
|
162
|
+
|
163
|
+
begin
|
164
|
+
subnodes << node if node.name == @name
|
165
|
+
end while (node = node.parent).is_a?(Prism::ConstantPathNode)
|
166
|
+
|
167
|
+
if node.is_a?(Prism::ConstantReadNode) && node.name == @name
|
123
168
|
subnodes << node
|
124
169
|
end
|
125
170
|
|
126
171
|
# If we found only one sub-node whose name is equal to @name, use it
|
127
172
|
return nil if subnodes.size != 1
|
128
173
|
@node = subnodes.first
|
129
|
-
else
|
130
|
-
# Do nothing; opt_getconstant_path is used only when the const base is
|
131
|
-
# NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`)
|
132
174
|
end
|
133
175
|
end
|
134
176
|
|
@@ -196,6 +238,48 @@ module ErrorHighlight
|
|
196
238
|
|
197
239
|
when :OP_CDECL
|
198
240
|
spot_op_cdecl
|
241
|
+
|
242
|
+
when :call_node
|
243
|
+
case @point_type
|
244
|
+
when :name
|
245
|
+
prism_spot_call_for_name
|
246
|
+
when :args
|
247
|
+
prism_spot_call_for_args
|
248
|
+
end
|
249
|
+
|
250
|
+
when :local_variable_operator_write_node
|
251
|
+
case @point_type
|
252
|
+
when :name
|
253
|
+
prism_spot_local_variable_operator_write_for_name
|
254
|
+
when :args
|
255
|
+
prism_spot_local_variable_operator_write_for_args
|
256
|
+
end
|
257
|
+
|
258
|
+
when :call_operator_write_node
|
259
|
+
case @point_type
|
260
|
+
when :name
|
261
|
+
prism_spot_call_operator_write_for_name
|
262
|
+
when :args
|
263
|
+
prism_spot_call_operator_write_for_args
|
264
|
+
end
|
265
|
+
|
266
|
+
when :index_operator_write_node
|
267
|
+
case @point_type
|
268
|
+
when :name
|
269
|
+
prism_spot_index_operator_write_for_name
|
270
|
+
when :args
|
271
|
+
prism_spot_index_operator_write_for_args
|
272
|
+
end
|
273
|
+
|
274
|
+
when :constant_read_node
|
275
|
+
prism_spot_constant_read
|
276
|
+
|
277
|
+
when :constant_path_node
|
278
|
+
prism_spot_constant_path
|
279
|
+
|
280
|
+
when :constant_path_operator_write_node
|
281
|
+
prism_spot_constant_path_operator_write
|
282
|
+
|
199
283
|
end
|
200
284
|
|
201
285
|
if @snippet && @beg_column && @end_column && @beg_column < @end_column
|
@@ -539,6 +623,207 @@ module ErrorHighlight
|
|
539
623
|
@beg_lineno = @end_lineno = lineno
|
540
624
|
@snippet = @fetch[lineno]
|
541
625
|
end
|
626
|
+
|
627
|
+
# Take a location from the prism parser and set the necessary instance
|
628
|
+
# variables.
|
629
|
+
def prism_location(location)
|
630
|
+
@beg_lineno = location.start_line
|
631
|
+
@beg_column = location.start_column
|
632
|
+
@end_lineno = location.end_line
|
633
|
+
@end_column = location.end_column
|
634
|
+
@snippet = @fetch[@beg_lineno, @end_lineno]
|
635
|
+
end
|
636
|
+
|
637
|
+
# Example:
|
638
|
+
# x.foo
|
639
|
+
# ^^^^
|
640
|
+
# x.foo(42)
|
641
|
+
# ^^^^
|
642
|
+
# x&.foo
|
643
|
+
# ^^^^^
|
644
|
+
# x[42]
|
645
|
+
# ^^^^
|
646
|
+
# x.foo = 1
|
647
|
+
# ^^^^^^
|
648
|
+
# x[42] = 1
|
649
|
+
# ^^^^^^
|
650
|
+
# x + 1
|
651
|
+
# ^
|
652
|
+
# +x
|
653
|
+
# ^
|
654
|
+
# foo(42)
|
655
|
+
# ^^^
|
656
|
+
# foo 42
|
657
|
+
# ^^^
|
658
|
+
# foo
|
659
|
+
# ^^^
|
660
|
+
def prism_spot_call_for_name
|
661
|
+
# Explicitly turn off foo.() syntax because error_highlight expects this
|
662
|
+
# to not work.
|
663
|
+
return nil if @node.name == :call && @node.message_loc.nil?
|
664
|
+
|
665
|
+
location = @node.message_loc || @node.call_operator_loc || @node.location
|
666
|
+
location = @node.call_operator_loc.join(location) if @node.call_operator_loc&.start_line == location.start_line
|
667
|
+
|
668
|
+
# If the method name ends with "=" but the message does not, then this is
|
669
|
+
# a method call using the "attribute assignment" syntax
|
670
|
+
# (e.g., foo.bar = 1). In this case we need to go retrieve the = sign and
|
671
|
+
# add it to the location.
|
672
|
+
if (name = @node.name).end_with?("=") && !@node.message.end_with?("=")
|
673
|
+
location = location.adjoin("=")
|
674
|
+
end
|
675
|
+
|
676
|
+
prism_location(location)
|
677
|
+
|
678
|
+
if !name.end_with?("=") && !name.match?(/[[:alpha:]_\[]/)
|
679
|
+
# If the method name is an operator, then error_highlight only
|
680
|
+
# highlights the first line.
|
681
|
+
fetch_line(location.start_line)
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
# Example:
|
686
|
+
# x.foo(42)
|
687
|
+
# ^^
|
688
|
+
# x[42]
|
689
|
+
# ^^
|
690
|
+
# x.foo = 1
|
691
|
+
# ^
|
692
|
+
# x[42] = 1
|
693
|
+
# ^^^^^^^
|
694
|
+
# x[] = 1
|
695
|
+
# ^^^^^
|
696
|
+
# x + 1
|
697
|
+
# ^
|
698
|
+
# foo(42)
|
699
|
+
# ^^
|
700
|
+
# foo 42
|
701
|
+
# ^^
|
702
|
+
def prism_spot_call_for_args
|
703
|
+
# Disallow highlighting arguments if there are no arguments.
|
704
|
+
return if @node.arguments.nil?
|
705
|
+
|
706
|
+
# Explicitly turn off foo.() syntax because error_highlight expects this
|
707
|
+
# to not work.
|
708
|
+
return nil if @node.name == :call && @node.message_loc.nil?
|
709
|
+
|
710
|
+
if @node.name == :[]= && @node.opening == "[" && (@node.arguments&.arguments || []).length == 1
|
711
|
+
prism_location(@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1).join(@node.arguments.location))
|
712
|
+
else
|
713
|
+
prism_location(@node.arguments.location)
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
717
|
+
# Example:
|
718
|
+
# x += 1
|
719
|
+
# ^
|
720
|
+
def prism_spot_local_variable_operator_write_for_name
|
721
|
+
prism_location(@node.binary_operator_loc.chop)
|
722
|
+
end
|
723
|
+
|
724
|
+
# Example:
|
725
|
+
# x += 1
|
726
|
+
# ^
|
727
|
+
def prism_spot_local_variable_operator_write_for_args
|
728
|
+
prism_location(@node.value.location)
|
729
|
+
end
|
730
|
+
|
731
|
+
# Example:
|
732
|
+
# x.foo += 42
|
733
|
+
# ^^^ (for foo)
|
734
|
+
# x.foo += 42
|
735
|
+
# ^ (for +)
|
736
|
+
# x.foo += 42
|
737
|
+
# ^^^^^^^ (for foo=)
|
738
|
+
def prism_spot_call_operator_write_for_name
|
739
|
+
if !@name.start_with?(/[[:alpha:]_]/)
|
740
|
+
prism_location(@node.binary_operator_loc.chop)
|
741
|
+
else
|
742
|
+
location = @node.message_loc
|
743
|
+
if @node.call_operator_loc.start_line == location.start_line
|
744
|
+
location = @node.call_operator_loc.join(location)
|
745
|
+
end
|
746
|
+
|
747
|
+
location = location.adjoin("=") if @name.end_with?("=")
|
748
|
+
prism_location(location)
|
749
|
+
end
|
750
|
+
end
|
751
|
+
|
752
|
+
# Example:
|
753
|
+
# x.foo += 42
|
754
|
+
# ^^
|
755
|
+
def prism_spot_call_operator_write_for_args
|
756
|
+
prism_location(@node.value.location)
|
757
|
+
end
|
758
|
+
|
759
|
+
# Example:
|
760
|
+
# x[1] += 42
|
761
|
+
# ^^^ (for [])
|
762
|
+
# x[1] += 42
|
763
|
+
# ^ (for +)
|
764
|
+
# x[1] += 42
|
765
|
+
# ^^^^^^ (for []=)
|
766
|
+
def prism_spot_index_operator_write_for_name
|
767
|
+
case @name
|
768
|
+
when :[]
|
769
|
+
prism_location(@node.opening_loc.join(@node.closing_loc))
|
770
|
+
when :[]=
|
771
|
+
prism_location(@node.opening_loc.join(@node.closing_loc).adjoin("="))
|
772
|
+
else
|
773
|
+
# Explicitly turn off foo[] += 1 syntax when the operator is not on
|
774
|
+
# the same line because error_highlight expects this to not work.
|
775
|
+
return nil if @node.binary_operator_loc.start_line != @node.opening_loc.start_line
|
776
|
+
|
777
|
+
prism_location(@node.binary_operator_loc.chop)
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
781
|
+
# Example:
|
782
|
+
# x[1] += 42
|
783
|
+
# ^^^^^^^^
|
784
|
+
def prism_spot_index_operator_write_for_args
|
785
|
+
opening_loc =
|
786
|
+
if @node.arguments.nil?
|
787
|
+
@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1)
|
788
|
+
else
|
789
|
+
@node.arguments.location
|
790
|
+
end
|
791
|
+
|
792
|
+
prism_location(opening_loc.join(@node.value.location))
|
793
|
+
end
|
794
|
+
|
795
|
+
# Example:
|
796
|
+
# Foo
|
797
|
+
# ^^^
|
798
|
+
def prism_spot_constant_read
|
799
|
+
prism_location(@node.location)
|
800
|
+
end
|
801
|
+
|
802
|
+
# Example:
|
803
|
+
# Foo::Bar
|
804
|
+
# ^^^^^
|
805
|
+
def prism_spot_constant_path
|
806
|
+
if @node.parent && @node.parent.location.end_line == @node.location.end_line
|
807
|
+
fetch_line(@node.parent.location.end_line)
|
808
|
+
prism_location(@node.delimiter_loc.join(@node.name_loc))
|
809
|
+
else
|
810
|
+
fetch_line(@node.location.end_line)
|
811
|
+
location = @node.name_loc
|
812
|
+
location = @node.delimiter_loc.join(location) if @node.delimiter_loc.end_line == location.start_line
|
813
|
+
prism_location(location)
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
# Example:
|
818
|
+
# Foo::Bar += 1
|
819
|
+
# ^^^^^^^^
|
820
|
+
def prism_spot_constant_path_operator_write
|
821
|
+
if @name == (target = @node.target).name
|
822
|
+
prism_location(target.delimiter_loc.join(target.name_loc))
|
823
|
+
else
|
824
|
+
prism_location(@node.binary_operator_loc.chop)
|
825
|
+
end
|
826
|
+
end
|
542
827
|
end
|
543
828
|
|
544
829
|
private_constant :Spotter
|
@@ -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, so it's not loaded when 'max_snippet_width' is set
|
60
|
+
require "io/console"
|
61
|
+
STDERR.winsize[1] if STDERR.tty?
|
62
|
+
rescue LoadError, NoMethodError, SystemCallError
|
63
|
+
# do not truncate when window size is not available
|
13
64
|
end
|
14
65
|
end
|
15
66
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.0
|
5
5
|
platform: ruby
|
6
|
+
original_platform: ''
|
6
7
|
authors:
|
7
8
|
- Yusuke Endoh
|
8
|
-
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-12-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: The gem enhances Exception#message by adding a short explanation where
|
14
14
|
the exception is raised
|
@@ -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
|
@@ -50,8 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
49
|
- !ruby/object:Gem::Version
|
51
50
|
version: '0'
|
52
51
|
requirements: []
|
53
|
-
rubygems_version: 3.
|
54
|
-
signing_key:
|
52
|
+
rubygems_version: 3.6.0.dev
|
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: []
|