@@ -60,14 +60,14 @@ def self.spot(obj, **opts)
60
60
rescue RuntimeError => error
61
61
# RubyVM::AbstractSyntaxTree.of raises an error with a message that
62
62
# includes "prism" when the ISEQ was compiled with the prism compiler.
63
- # In this case, we'll set the node to `nil`. In the future, we will
64
- # reparse with the prism parser and pass the parsed node to Spotter.
63
+ # In this case, we'll try to parse again with prism instead.
65
64
raise unless error . message . include? ( "prism" )
65
+ prism_find ( loc , **opts )
66
66
end
67
67
68
68
Spotter . new ( node , **opts ) . spot
69
69
70
- when RubyVM ::AbstractSyntaxTree ::Node
70
+ when RubyVM ::AbstractSyntaxTree ::Node , Prism :: Node
71
71
Spotter . new ( obj , **opts ) . spot
72
72
73
73
else
@@ -81,6 +81,71 @@ def self.spot(obj, **opts)
81
81
return nil
82
82
end
83
83
84
+ # Accepts a Thread::Backtrace::Location object and returns a Prism::Node
85
+ # corresponding to the location in the source code.
86
+ def self . prism_find ( loc , point_type : :name , name : nil )
87
+ require "prism"
88
+ return nil if Prism ::VERSION < "0.29.0"
89
+
90
+ path = loc . absolute_path
91
+ return unless path
92
+
93
+ lineno = loc . lineno
94
+ column = RubyVM ::AbstractSyntaxTree . node_id_for_backtrace_location ( loc )
95
+ tunnel = Prism . parse_file ( path ) . value . tunnel ( lineno , column )
96
+
97
+ # Prism provides the Prism::Node#tunnel API to find all of the nodes that
98
+ # correspond to the given line and column in the source code, with the first
99
+ # node in the list being the top-most node and the last node in the list
100
+ # being the bottom-most node.
101
+ tunnel . each_with_index . reverse_each . find do |part , index |
102
+ case part
103
+ when Prism ::CallNode , Prism ::CallOperatorWriteNode , Prism ::IndexOperatorWriteNode , Prism ::LocalVariableOperatorWriteNode
104
+ # If we find any of these nodes, we can stop searching as these are the
105
+ # nodes that triggered the exceptions.
106
+ break part
107
+ when Prism ::ConstantReadNode , Prism ::ConstantPathNode
108
+ if index != 0 && tunnel [ index - 1 ] . is_a? ( Prism ::ConstantPathOperatorWriteNode )
109
+ # If we're inside of a constant path operator write node, then this
110
+ # constant path may be highlighting a couple of different kinds of
111
+ # parts.
112
+ if part . name == name
113
+ # Explicitly turn off Foo::Bar += 1 where Foo and Bar are on
114
+ # different lines because error highlight expects this to not work.
115
+ break nil if part . delimiter_loc . end_line != part . name_loc . start_line
116
+
117
+ # Otherwise, because we have matched the name we can return this
118
+ # part.
119
+ break part
120
+ end
121
+
122
+ # If we haven't matched the name, it's the operator that we're looking
123
+ # for, and we can return the parent node here.
124
+ break tunnel [ index - 1 ]
125
+ elsif part . name == name
126
+ # If we have matched the name of the constant, then we can return this
127
+ # inner node as the node that triggered the exception.
128
+ break part
129
+ else
130
+ # If we are at the beginning of the tunnel or we are at the beginning
131
+ # of a constant lookup chain, then we will return this node.
132
+ break part if index == 0 || !tunnel [ index - 1 ] . is_a? ( Prism ::ConstantPathNode )
133
+ end
134
+ when Prism ::LocalVariableReadNode , Prism ::ParenthesesNode
135
+ # If we find any of these nodes, we want to continue searching up the
136
+ # tree because these nodes cannot trigger the exceptions.
137
+ false
138
+ else
139
+ # If we find a different kind of node that we haven't already handled,
140
+ # we don't know how to handle it so we'll stop searching and assume this
141
+ # is not an exception we can decorate.
142
+ break nil
143
+ end
144
+ end
145
+ end
146
+
147
+ private_class_method :prism_find
148
+
84
149
class Spotter
85
150
class NonAscii < Exception ; end
86
151
private_constant :NonAscii
@@ -205,6 +270,48 @@ def spot
205
270
206
271
when :OP_CDECL
207
272
spot_op_cdecl
273
+
274
+ when :call_node
275
+ case @point_type
276
+ when :name
277
+ prism_spot_call_for_name
278
+ when :args
279
+ prism_spot_call_for_args
280
+ end
281
+
282
+ when :local_variable_operator_write_node
283
+ case @point_type
284
+ when :name
285
+ prism_spot_local_variable_operator_write_for_name
286
+ when :args
287
+ prism_spot_local_variable_operator_write_for_args
288
+ end
289
+
290
+ when :call_operator_write_node
291
+ case @point_type
292
+ when :name
293
+ prism_spot_call_operator_write_for_name
294
+ when :args
295
+ prism_spot_call_operator_write_for_args
296
+ end
297
+
298
+ when :index_operator_write_node
299
+ case @point_type
300
+ when :name
301
+ prism_spot_index_operator_write_for_name
302
+ when :args
303
+ prism_spot_index_operator_write_for_args
304
+ end
305
+
306
+ when :constant_read_node
307
+ prism_spot_constant_read
308
+
309
+ when :constant_path_node
310
+ prism_spot_constant_path
311
+
312
+ when :constant_path_operator_write_node
313
+ prism_spot_constant_path_operator_write
314
+
208
315
end
209
316
210
317
if @snippet && @beg_column && @end_column && @beg_column < @end_column
@@ -548,6 +655,200 @@ def fetch_line(lineno)
548
655
@beg_lineno = @end_lineno = lineno
549
656
@snippet = @fetch [ lineno ]
550
657
end
658
+
659
+ # Take a location from the prism parser and set the necessary instance
660
+ # variables.
661
+ def prism_location ( location )
662
+ @beg_lineno = location . start_line
663
+ @beg_column = location . start_column
664
+ @end_lineno = location . end_line
665
+ @end_column = location . end_column
666
+ @snippet = @fetch [ @beg_lineno , @end_lineno ]
667
+ end
668
+
669
+ # Example:
670
+ # x.foo
671
+ # ^^^^
672
+ # x.foo(42)
673
+ # ^^^^
674
+ # x&.foo
675
+ # ^^^^^
676
+ # x[42]
677
+ # ^^^^
678
+ # x.foo = 1
679
+ # ^^^^^^
680
+ # x[42] = 1
681
+ # ^^^^^^
682
+ # x + 1
683
+ # ^
684
+ # +x
685
+ # ^
686
+ # foo(42)
687
+ # ^^^
688
+ # foo 42
689
+ # ^^^
690
+ # foo
691
+ # ^^^
692
+ def prism_spot_call_for_name
693
+ # Explicitly turn off foo.() syntax because error_highlight expects this
694
+ # to not work.
695
+ return nil if @node . name == :call && @node . message_loc . nil?
696
+
697
+ location = @node . message_loc || @node . call_operator_loc || @node . location
698
+ location = @node . call_operator_loc . join ( location ) if @node . call_operator_loc &.start_line == location . start_line
699
+
700
+ # If the method name ends with "=" but the message does not, then this is
701
+ # a method call using the "attribute assignment" syntax
702
+ # (e.g., foo.bar = 1). In this case we need to go retrieve the = sign and
703
+ # add it to the location.
704
+ if ( name = @node . name ) . end_with? ( "=" ) && !@node . message . end_with? ( "=" )
705
+ location = location . adjoin ( "=" )
706
+ end
707
+
708
+ prism_location ( location )
709
+
710
+ if !name . end_with? ( "=" ) && !name . match? ( /[[:alpha:]_\[ ]/ )
711
+ # If the method name is an operator, then error_highlight only
712
+ # highlights the first line.
713
+ fetch_line ( location . start_line )
714
+ end
715
+ end
716
+
717
+ # Example:
718
+ # x.foo(42)
719
+ # ^^
720
+ # x[42]
721
+ # ^^
722
+ # x.foo = 1
723
+ # ^
724
+ # x[42] = 1
725
+ # ^^^^^^^
726
+ # x[] = 1
727
+ # ^^^^^
728
+ # x + 1
729
+ # ^
730
+ # foo(42)
731
+ # ^^
732
+ # foo 42
733
+ # ^^
734
+ def prism_spot_call_for_args
735
+ # Explicitly turn off foo.() syntax because error_highlight expects this
736
+ # to not work.
737
+ return nil if @node . name == :call && @node . message_loc . nil?
738
+
739
+ if @node . name == :[]= && @node . opening == "[" && ( @node . arguments &.arguments || [ ] ) . length == 1
740
+ prism_location ( @node . opening_loc . copy ( start_offset : @node . opening_loc . start_offset + 1 ) . join ( @node . arguments . location ) )
741
+ else
742
+ prism_location ( @node . arguments . location )
743
+ end
744
+ end
745
+
746
+ # Example:
747
+ # x += 1
748
+ # ^
749
+ def prism_spot_local_variable_operator_write_for_name
750
+ prism_location ( @node . binary_operator_loc . chop )
751
+ end
752
+
753
+ # Example:
754
+ # x += 1
755
+ # ^
756
+ def prism_spot_local_variable_operator_write_for_args
757
+ prism_location ( @node . value . location )
758
+ end
759
+
760
+ # Example:
761
+ # x.foo += 42
762
+ # ^^^ (for foo)
763
+ # x.foo += 42
764
+ # ^ (for +)
765
+ # x.foo += 42
766
+ # ^^^^^^^ (for foo=)
767
+ def prism_spot_call_operator_write_for_name
768
+ if !@name . start_with? ( /[[:alpha:]_]/ )
769
+ prism_location ( @node . binary_operator_loc . chop )
770
+ else
771
+ location = @node . message_loc
772
+ if @node . call_operator_loc . start_line == location . start_line
773
+ location = @node . call_operator_loc . join ( location )
774
+ end
775
+
776
+ location = location . adjoin ( "=" ) if @name . end_with? ( "=" )
777
+ prism_location ( location )
778
+ end
779
+ end
780
+
781
+ # Example:
782
+ # x.foo += 42
783
+ # ^^
784
+ def prism_spot_call_operator_write_for_args
785
+ prism_location ( @node . value . location )
786
+ end
787
+
788
+ # Example:
789
+ # x[1] += 42
790
+ # ^^^ (for [])
791
+ # x[1] += 42
792
+ # ^ (for +)
793
+ # x[1] += 42
794
+ # ^^^^^^ (for []=)
795
+ def prism_spot_index_operator_write_for_name
796
+ case @name
797
+ when :[]
798
+ prism_location ( @node . opening_loc . join ( @node . closing_loc ) )
799
+ when :[]=
800
+ prism_location ( @node . opening_loc . join ( @node . closing_loc ) . adjoin ( "=" ) )
801
+ else
802
+ # Explicitly turn off foo[] += 1 syntax when the operator is not on
803
+ # the same line because error_highlight expects this to not work.
804
+ return nil if @node . binary_operator_loc . start_line != @node . opening_loc . start_line
805
+
806
+ prism_location ( @node . binary_operator_loc . chop )
807
+ end
808
+ end
809
+
810
+ # Example:
811
+ # x[1] += 42
812
+ # ^^^^^^^^
813
+ def prism_spot_index_operator_write_for_args
814
+ opening_loc =
815
+ if @node . arguments . nil?
816
+ @node . opening_loc . copy ( start_offset : @node . opening_loc . start_offset + 1 )
817
+ else
818
+ @node . arguments . location
819
+ end
820
+
821
+ prism_location ( opening_loc . join ( @node . value . location ) )
822
+ end
823
+
824
+ # Example:
825
+ # Foo
826
+ # ^^^
827
+ def prism_spot_constant_read
828
+ prism_location ( @node . location )
829
+ end
830
+
831
+ # Example:
832
+ # Foo::Bar
833
+ # ^^^^^
834
+ def prism_spot_constant_path
835
+ if @node . parent && @node . parent . location . end_line == @node . location . end_line
836
+ fetch_line ( @node . parent . location . end_line )
837
+ prism_location ( @node . delimiter_loc . join ( @node . name_loc ) )
838
+ else
839
+ fetch_line ( @node . location . end_line )
840
+ location = @node . name_loc
841
+ location = @node . delimiter_loc . join ( location ) if @node . delimiter_loc . end_line == location . start_line
842
+ prism_location ( location )
843
+ end
844
+ end
845
+
846
+ # Example:
847
+ # Foo::Bar += 1
848
+ # ^^^^^^^^
849
+ def prism_spot_constant_path_operator_write
850
+ prism_location ( @node . binary_operator_loc . chop )
851
+ end
551
852
end
552
853
553
854
private_constant :Spotter
0 commit comments