Skip to content

Commit 69fbacf

Browse files
kddnewtonmame
authored andcommitted
Support for the prism compiler
1 parent f931b42 commit 69fbacf

File tree

2 files changed

+313
-4
lines changed

2 files changed

+313
-4
lines changed

lib/error_highlight/base.rb

Lines changed: 304 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ def self.spot(obj, **opts)
6060
rescue RuntimeError => error
6161
# RubyVM::AbstractSyntaxTree.of raises an error with a message that
6262
# 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.
6564
raise unless error.message.include?("prism")
65+
prism_find(loc, **opts)
6666
end
6767

6868
Spotter.new(node, **opts).spot
6969

70-
when RubyVM::AbstractSyntaxTree::Node
70+
when RubyVM::AbstractSyntaxTree::Node, Prism::Node
7171
Spotter.new(obj, **opts).spot
7272

7373
else
@@ -81,6 +81,71 @@ def self.spot(obj, **opts)
8181
return nil
8282
end
8383

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+
84149
class Spotter
85150
class NonAscii < Exception; end
86151
private_constant :NonAscii
@@ -205,6 +270,48 @@ def spot
205270

206271
when :OP_CDECL
207272
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+
208315
end
209316

210317
if @snippet && @beg_column && @end_column && @beg_column < @end_column
@@ -548,6 +655,200 @@ def fetch_line(lineno)
548655
@beg_lineno = @end_lineno = lineno
549656
@snippet = @fetch[lineno]
550657
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
551852
end
552853

553854
private_constant :Spotter

test/test_error_highlight.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
require "tempfile"
66

77
class ErrorHighlightTest < Test::Unit::TestCase
8+
# We can't revisit instruction sequences to find node ids if the prism
9+
# compiler was used instead of the parse.y compiler. In that case, we'll omit
10+
# some tests.
11+
def self.compiling_with_prism?
12+
RubyVM::InstructionSequence.compile("").to_a[4][:parser] == :prism
13+
end
14+
815
class DummyFormatter
916
def self.message_for(corrections)
1017
""
@@ -869,7 +876,7 @@ def test_COLON2_4
869876
end
870877
end
871878

872-
if ErrorHighlight.const_get(:Spotter).const_get(:OPT_GETCONSTANT_PATH)
879+
if ErrorHighlight.const_get(:Spotter).const_get(:OPT_GETCONSTANT_PATH) && !compiling_with_prism?
873880
def test_COLON2_5
874881
# Unfortunately, we cannot identify which `NotDefined` caused the NameError
875882
assert_error_message(NameError, <<~END) do
@@ -1335,6 +1342,7 @@ def test_spot_with_backtrace_location
13351342

13361343
def test_spot_with_node
13371344
omit unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location)
1345+
omit if ErrorHighlightTest.compiling_with_prism?
13381346

13391347
begin
13401348
raise_name_error

0 commit comments

Comments
 (0)