fix: preserve Ruby constant lookup boundaries

This commit is contained in:
oleksii-tumanov committed 2026-09-15 04:25:31 -05:00
1 parent 1a7262e2a8
commit bd1a2d2651
3 files changed
+64 -7

No files matched your search

+12 -4
View File
@@ -3159,15 +3159,18 @@ def _ruby_external_method_owners(root_node, source: bytes) -> list[str]:
refs.update(constant_refs(child))
return refs
def alias_rhs_refs(node) -> set[str]:
def alias_rhs_refs(node, *, allow_array: bool = False) -> set[str]:
if node.type in {"constant", "scope_resolution"}:
raw = _ruby_const_full_name(node, source)
return {raw} if raw else set()
if node.type in {"parenthesized_statements", "right_assignment_list"}:
if node.type in {
"parenthesized_statements",
"right_assignment_list",
} or (allow_array and node.type == "array"):
refs: set[str] = set()
for child in node.children:
if child.is_named:
refs.update(alias_rhs_refs(child))
refs.update(alias_rhs_refs(child, allow_array=allow_array))
return refs
return set()
@@ -3189,7 +3192,12 @@ def _ruby_external_method_owners(root_node, source: bytes) -> list[str]:
# too. Only direct/parenthesized constant RHS forms qualify;
# ordinary values containing constant references do not.
owners.update(
prefix_marker(raw) for raw in alias_rhs_refs(right)
prefix_marker(raw)
for raw in alias_rhs_refs(
right,
allow_array=left is not None
and left.type == "left_assignment_list",
)
)
if node.type in {"class", "module"}:
name = node.child_by_field_name("name")
+3 -3
View File
@@ -224,13 +224,13 @@ def resolve_ruby_member_calls(
if len(candidates) > 1:
return None
if scope and len(ref_parts) > 1:
prefix_label = "::".join(
first_prefix_label = "::".join(
[
*[part for part in scope.split("::") if part],
*ref_parts[:-1],
ref_parts[0],
]
)
if class_labels.get(prefix_label):
if class_labels.get(first_prefix_label):
# Ruby bound the qualified prefix in this nearer lexical
# scope. Its inherited constants are not modelled, so do
# not retry the same prefix in an outer scope.
+49
View File
@@ -812,6 +812,25 @@ def test_inherited_call_stays_inferred_for_multiple_constant_write(
assert _only_call(graph)["confidence"] == "INFERRED"
def test_destructured_constant_alias_owner_stays_inferred(tmp_path: Path) -> None:
_write(tmp_path, "base.rb", "class Base\n def self.helper; :wrong; end\nend\n")
_write(
tmp_path,
"child.rb",
"class Child < Base\n def self.call\n helper()\n end\nend\n",
)
_write(
tmp_path,
"override.rb",
"AliasChild, Spare = [Child, Object]\n"
"AliasChild.define_singleton_method(:helper) { :child }\n",
)
graph = extract(sorted(tmp_path.glob("*.rb")), cache_root=tmp_path, parallel=False)
assert _only_call(graph)["confidence"] == "INFERRED"
def test_inherited_call_stays_inferred_under_enclosing_refinement(
tmp_path: Path,
) -> None:
@@ -1059,6 +1078,36 @@ def test_qualified_superclass_stops_at_nearer_lexical_prefix(
assert _only_call(graph)["confidence"] == "INFERRED"
def test_deep_qualified_superclass_stops_at_first_lexical_segment(
tmp_path: Path,
) -> None:
_write(
tmp_path,
"base.rb",
"module Outer\n module Services\n module API\n class Base\n"
" def self.helper; :wrong; end\n end\n end\n end\nend\n",
)
_write(
tmp_path,
"provider.rb",
"class Provider\n module API\n class Base\n class << self\n"
" attr_reader :helper\n end\n end\n end\nend\n",
)
_write(
tmp_path,
"child.rb",
"module Outer\n module Inner\n"
" class Services < ::Provider; end\n"
" class Child < Services::API::Base\n"
" def self.call\n helper()\n end\n end\n"
" end\nend\n",
)
graph = extract(sorted(tmp_path.glob("*.rb")), cache_root=tmp_path, parallel=False)
assert _only_call(graph)["confidence"] == "INFERRED"
def test_inherited_call_stops_at_ambiguous_nearer_method(tmp_path: Path) -> None:
_write(tmp_path, "grand.rb", "class Grand\n def helper; :grand; end\nend\n")
_write(