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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33a97fb38c24c4c0f5637b458a73525e7fcb3a1803e15d6478f009fd590afa0c
4
- data.tar.gz: 16646ee686117659558ee48bb3b56f4fc48f01e7549b60240495f8f830794da8
3
+ metadata.gz: c61fd8e89a64b337d10859ca487015ad69c87555057ce7f15b274ce799a5beb3
4
+ data.tar.gz: 946058938dc70f583672f9adf1450a1514e4922638e051b2755545017f266963
5
5
  SHA512:
6
- metadata.gz: 228531308329b907adb5d78c8c3b031b954b49338cb13d2fb31aa30919b870a61c3809b17b9a67f0d7449049c6c456f33d47e7a1abb7597aad45f09262a5682d
7
- data.tar.gz: 293349ee401c24887dea1459b84dbf3b2b76a93934d02759b1ccf8ff3cdb376f66025dc664f16ab5bb484982661e3804078e474e7fadcfb602d3657f581f7095
6
+ metadata.gz: b5a627e7b0a5710be27e62444a5127353e592cb5dd075ec0a25aa957fdf92e7c3e730b30a0fe5b4d1d35eabd9cde9dd6abafbc22a3f071ad7af8f39f991ee634
7
+ data.tar.gz: 6e72a445acf82b23d8a6deb6537504dfe64b50d74a808ff33104da5dccad6970256f115ef1dc786a25136457ce18b801d0d466b7b8c8dac4ba0d70ab3b5732cc
@@ -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: [ 'ruby-head', '3.1', '3.2' ]
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
@@ -54,11 +54,20 @@ module ErrorHighlight
54
54
 
55
55
  return nil unless Thread::Backtrace::Location === loc
56
56
 
57
- node = RubyVM::AbstractSyntaxTree.of(loc, keep_script_lines: true)
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 && @node.type == :COLON2
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
- subnodes = []
115
- node = @node
116
- while node.type == :COLON2
117
- node2, const = node.children
118
- subnodes << node if const == @name
119
- node = node2
120
- end
121
- if node.type == :CONST || node.type == :COLON3
122
- if node.children.first == @name
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
- if spot[:first_lineno] == spot[:last_lineno]
6
- indent = spot[:snippet][0...spot[:first_column]].gsub(/[^\t]/, " ")
7
- marker = indent + "^" * (spot[:last_column] - spot[:first_column])
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
- "\n\n#{ spot[:snippet] }#{ marker }"
10
- else
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
 
@@ -1,3 +1,3 @@
1
1
  module ErrorHighlight
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
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.6.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: 2023-12-09 00:00:00.000000000 Z
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.5.0.dev
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: []