Skip to content

Commit 0a4db7d

Browse files
committed
Identify which node in Foo::Bar::Baz causes a NameError
In Ruby 3.2 or later, a nested constant access like `Foo::Bar::Baz` is compiled to one instruction by the optimization ruby/ruby#6187 We try to spot which sub-node caues a NameError in question based on the constant name. We will give up if the same constant name is accessed in a nested access (`Foo::Foo`). Fixes #31
1 parent 44e109e commit 0a4db7d

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

lib/error_highlight/base.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,40 @@ def initialize(node, point_type: :name, name: nil)
9898
end
9999
end
100100

101+
OPT_GETCONSTANT_PATH = (RUBY_VERSION.split(".").map {|s| s.to_i } <=> [3, 2]) >= 0
102+
private_constant :OPT_GETCONSTANT_PATH
103+
101104
def spot
102105
return nil unless @node
103106

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`.
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
123+
subnodes << node
124+
end
125+
126+
# If we found only one sub-node whose name is equal to @name, use it
127+
return nil if subnodes.size != 1
128+
@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+
end
133+
end
134+
104135
case @node.type
105136

106137
when :CALL, :QCALL

test/test_error_highlight.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,54 @@ def test_COLON2_2
818818
end
819819
end
820820

821+
def test_COLON2_3
822+
assert_error_message(NameError, <<~END) do
823+
uninitialized constant ErrorHighlightTest::NotDefined
824+
825+
ErrorHighlightTest::NotDefined::Foo
826+
^^^^^^^^^^^^
827+
END
828+
829+
ErrorHighlightTest::NotDefined::Foo
830+
end
831+
end
832+
833+
def test_COLON2_4
834+
assert_error_message(NameError, <<~END) do
835+
uninitialized constant ErrorHighlightTest::NotDefined
836+
837+
::ErrorHighlightTest::NotDefined::Foo
838+
^^^^^^^^^^^^
839+
END
840+
841+
::ErrorHighlightTest::NotDefined::Foo
842+
end
843+
end
844+
845+
if ErrorHighlight.const_get(:Spotter).const_get(:OPT_GETCONSTANT_PATH)
846+
def test_COLON2_5
847+
# Unfortunately, we cannot identify which `NotDefined` caused the NameError
848+
assert_error_message(NameError, <<~END) do
849+
uninitialized constant ErrorHighlightTest::NotDefined
850+
END
851+
852+
ErrorHighlightTest::NotDefined::NotDefined
853+
end
854+
end
855+
else
856+
def test_COLON2_5
857+
assert_error_message(NameError, <<~END) do
858+
uninitialized constant ErrorHighlightTest::NotDefined
859+
860+
ErrorHighlightTest::NotDefined::NotDefined
861+
^^^^^^^^^^^^
862+
END
863+
864+
ErrorHighlightTest::NotDefined::NotDefined
865+
end
866+
end
867+
end
868+
821869
def test_COLON3
822870
assert_error_message(NameError, <<~END) do
823871
uninitialized constant NotDefined

0 commit comments

Comments
 (0)