mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 08:59:26 +00:00
fix: guard Ruby constant rebindings
This commit is contained in:
1 parent
891ebfae53
commit
5f28f2bb3e
2 files changed
+93
-13
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user