error_highlight 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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: []