fix: guard Ruby constant rebindings

This commit is contained in:
oleksii-tumanov committed 2026-09-15 04:16:00 -05:00
1 parent 891ebfae53
commit 5f28f2bb3e
2 files changed
+93 -13

No files matched your search

+19 -13
View File
@@ -3141,21 +3141,28 @@ def _ruby_external_method_owners(root_node, source: bytes) -> list[str]:
"""Return explicit constant owners modified outside their class body."""
owners: set[str] = set()
def collect_constant_refs(node) -> None:
if node.type in {"constant", "scope_resolution"}:
raw = _ruby_const_full_name(node, source)
if raw:
owners.add(raw)
return
for child in node.children:
if child.is_named:
collect_constant_refs(child)
def visit(node) -> None:
if node.type == "assignment":
if node.type in {"assignment", "operator_assignment"}:
left = node.child_by_field_name("left")
if left is not None:
# Any write can change which superclass a constant reference
# denotes, including dynamic RHS and multiple assignment.
collect_constant_refs(left)
right = node.child_by_field_name("right")
if (
left is not None
and left.type in {"constant", "scope_resolution"}
and right is not None
and right.type in {"constant", "scope_resolution"}
):
raw = _ruby_const_full_name(left, source)
if raw:
# Constant aliases can change which superclass an
# unqualified reference denotes without changing its text.
owners.add(raw)
if node.type == "assignment" and right is not None:
# Treat both ends of a potential constant alias as unsafe. A
# later mutation through the alias affects the original owner.
collect_constant_refs(right)
if node.type in {"class", "module"}:
name = node.child_by_field_name("name")
if name is not None and name.type == "scope_resolution":
@@ -3178,7 +3185,6 @@ def _ruby_external_method_owners(root_node, source: bytes) -> list[str]:
raw = _ruby_const_full_name(owner, source)
if raw:
owners.add(raw)
return
if node.type == "call":
receiver = node.child_by_field_name("receiver")
method = node.child_by_field_name("method")
+74
View File
@@ -780,6 +780,38 @@ def test_inherited_call_stays_inferred_for_superclass_constant_alias(
assert _only_call(graph)["confidence"] == "INFERRED"
def test_inherited_call_stays_inferred_for_dynamic_superclass_constant_write(
tmp_path: Path,
) -> None:
_write(tmp_path, "base.rb", "class Base\n def helper; :stale; end\nend\n")
_write(
tmp_path,
"child.rb",
"Base = resolve_base()\nclass Child < Base\n"
" def call\n helper()\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_stays_inferred_for_multiple_constant_write(
tmp_path: Path,
) -> None:
_write(tmp_path, "base.rb", "class Base\n def helper; :stale; end\nend\n")
_write(
tmp_path,
"child.rb",
"Base, Spare = ExternalBase, Object\nclass Child < Base\n"
" def call\n helper()\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_stays_inferred_under_enclosing_refinement(
tmp_path: Path,
) -> None:
@@ -863,6 +895,48 @@ def test_external_owner_mutator_inside_singleton_class_stays_inferred(
assert _only_call(graph)["confidence"] == "INFERRED"
def test_external_owner_mutator_inside_external_eigenclass_stays_inferred(
tmp_path: Path,
) -> None:
_write(tmp_path, "base.rb", "class Base\n def self.helper; :base; 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",
"class Installer; end\nclass << Installer\n"
" Child.define_singleton_method(:helper) { :child }\nend\n",
)
graph = extract(sorted(tmp_path.glob("*.rb")), cache_root=tmp_path, parallel=False)
assert _only_call(graph)["confidence"] == "INFERRED"
def test_external_owner_mutator_through_constant_alias_stays_inferred(
tmp_path: Path,
) -> None:
_write(tmp_path, "base.rb", "class Base\n def self.helper; :base; 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 = Child\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_for_compact_reopening(
tmp_path: Path,
) -> None: