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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33a97fb38c24c4c0f5637b458a73525e7fcb3a1803e15d6478f009fd590afa0c
4
- data.tar.gz: 16646ee686117659558ee48bb3b56f4fc48f01e7549b60240495f8f830794da8
3
+ metadata.gz: e6a847df9db6582ba1912ba3a2a64a76ba18e38d43c38413ac885fb993cbce07
4
+ data.tar.gz: c6e6872fd7276af7fac719d173da9f07af9fed0760d05385d1b6b6217449b9f0
5
5
  SHA512:
6
- metadata.gz: 228531308329b907adb5d78c8c3b031b954b49338cb13d2fb31aa30919b870a61c3809b17b9a67f0d7449049c6c456f33d47e7a1abb7597aad45f09262a5682d
7
- data.tar.gz: 293349ee401c24887dea1459b84dbf3b2b76a93934d02759b1ccf8ff3cdb376f66025dc664f16ab5bb484982661e3804078e474e7fadcfb602d3657f581f7095
6
+ metadata.gz: 9699f20efc393a3fc7c09312a29818416bb255c3078c45a4a4f75deda3f2becefe415bc98565928a001ff88638433868d89656d9382b6b736a781f51d17e1901
7
+ data.tar.gz: 48410be2cb3bbfd0c241ded954d11c960abc551fc896c907ed05d6c49ea37ec116ee92556cd713c9b5fe2f033fe3eb16606e788b97a61196f97a2050f839ded2
@@ -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: [ 'ruby-head', '3.1', '3.2' ]
23
+ ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
17
24
  steps:
18
- - uses: actions/checkout@v4
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
@@ -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.1.0.dev")
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)/}) }
@@ -1,13 +1,13 @@
1
1
  require_relative "version"
2
2
 
3
3
  module ErrorHighlight
4
- # Identify the code fragment at that a given exception occurred.
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 that the exception occurred.
10
- # :args points the arguments of the method call that the exception occurred.
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 = 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
@@ -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 require some work to support Unicode (or multibyte) characters.
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
- if OPT_GETCONSTANT_PATH && @node.type == :COLON2
108
- # In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`)
109
- # is compiled to one instruction (opt_getconstant_path).
110
- # @node points to the node of the whole `Foo::Bar::Baz` even if `Foo`
111
- # or `Foo::Bar` causes NameError.
112
- # So we try to spot the sub-node that causes the NameError by using
113
- # `NameError#name`.
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
- @snippet = @fetch[@node.last_lineno]
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
- @snippet = @fetch[nd_lhs.last_lineno]
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
- @snippet = @fetch[nd_lhs.last_lineno]
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
- @snippet = @fetch[nd_lhs.last_lineno]
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
- spot = ErrorHighlight.spot(self)
7
- return "" unless spot
8
- return ErrorHighlight.formatter.message_for(spot)
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
- 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 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
 
@@ -1,3 +1,3 @@
1
1
  module ErrorHighlight
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.1"
3
3
  end
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.6.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: 2023-12-09 00:00:00.000000000 Z
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.1.0.dev
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: 3.5.0.dev
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: []