error_highlight 0.5.1 → 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: 97374a7c69ab159c324e0592fae950fa9c065e9f3cd51130dbc2d39b65caa9c4
4
- data.tar.gz: 470ef6d7ce73943e6e0260a581211987286fd366cde3663ad06a25dccb5f7234
3
+ metadata.gz: c61fd8e89a64b337d10859ca487015ad69c87555057ce7f15b274ce799a5beb3
4
+ data.tar.gz: 946058938dc70f583672f9adf1450a1514e4922638e051b2755545017f266963
5
5
  SHA512:
6
- metadata.gz: 6fceb2e95ac91526ebbb3b51e19c3a03cb888e77642045b4ab9b050abc0ab09f5e783a93edf7b22587e811d6b85cd1a798551ea2cebcaf1c82364318cee5c89b
7
- data.tar.gz: 5972069a5065563327401a9fcff877ecaaeda664c15d3bf4cc6285f97366d46ede0742c6ebc81e2a86738bc39f0193a32164c59eb10675c43e540ed8bb414d2b
6
+ metadata.gz: b5a627e7b0a5710be27e62444a5127353e592cb5dd075ec0a25aa957fdf92e7c3e730b30a0fe5b4d1d35eabd9cde9dd6abafbc22a3f071ad7af8f39f991ee634
7
+ data.tar.gz: 6e72a445acf82b23d8a6deb6537504dfe64b50d74a808ff33104da5dccad6970256f115ef1dc786a25136457ce18b801d0d466b7b8c8dac4ba0d70ab3b5732cc
@@ -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.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' ]
23
+ ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
17
24
  steps:
18
- - uses: actions/checkout@v3
25
+ - uses: actions/checkout@v4
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@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
@@ -98,9 +122,58 @@ module ErrorHighlight
98
122
  end
99
123
  end
100
124
 
125
+ OPT_GETCONSTANT_PATH = (RUBY_VERSION.split(".").map {|s| s.to_i } <=> [3, 2]) >= 0
126
+ private_constant :OPT_GETCONSTANT_PATH
127
+
101
128
  def spot
102
129
  return nil unless @node
103
130
 
131
+ if OPT_GETCONSTANT_PATH
132
+ # In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`)
133
+ # is compiled to one instruction (opt_getconstant_path).
134
+ # @node points to the node of the whole `Foo::Bar::Baz` even if `Foo`
135
+ # or `Foo::Bar` causes NameError.
136
+ # So we try to spot the sub-node that causes the NameError by using
137
+ # `NameError#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
168
+ subnodes << node
169
+ end
170
+
171
+ # If we found only one sub-node whose name is equal to @name, use it
172
+ return nil if subnodes.size != 1
173
+ @node = subnodes.first
174
+ end
175
+ end
176
+
104
177
  case @node.type
105
178
 
106
179
  when :CALL, :QCALL
@@ -165,6 +238,48 @@ module ErrorHighlight
165
238
 
166
239
  when :OP_CDECL
167
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
+
168
283
  end
169
284
 
170
285
  if @snippet && @beg_column && @end_column && @beg_column < @end_column
@@ -508,6 +623,207 @@ module ErrorHighlight
508
623
  @beg_lineno = @end_lineno = lineno
509
624
  @snippet = @fetch[lineno]
510
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
511
827
  end
512
828
 
513
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.5.1"
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.5.1
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: 2022-11-14 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.3.7
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: []