Files
graphify-labs--graphify/tests/test_pascal_call_scoping.py
T
wehub-resource-sync d88fd01084
CI / test (3.10) (push) Failing after 1s
CI / test (3.12) (push) Failing after 0s
CI / skillgen-check (push) Failing after 0s
CI / security-scan (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:09:14 +08:00

103 lines
3.9 KiB
Python

"""Regression tests for scoped call resolution in the Pascal/Delphi extractor.
Before this fix, both `extract_pascal` (tree-sitter path) and
`_extract_pascal_regex` (fallback path) resolved every call by a single
file-wide ``{method_name_lower: node_id}`` dict with no class scoping. Two
unrelated classes declaring a same-named method (a common Pascal/Delphi
pattern -- property accessors, generated wrapper classes such as TLB import
units) silently collapsed onto whichever declaration was inserted last,
producing wrong cross-class `calls` edges. See `sample_scoped_calls.pas`.
"""
from __future__ import annotations
import pytest
from pathlib import Path
FIXTURES = Path(__file__).parent / "fixtures"
FIXTURE_PATH = FIXTURES / "sample_scoped_calls.pas"
def _extractors():
from graphify.extract import extract_pascal, _extract_pascal_regex
return [extract_pascal, _extract_pascal_regex]
def _class_node_id(r, class_label):
matches = [n["id"] for n in r["nodes"] if n["label"] == class_label]
assert len(matches) == 1, f"expected exactly one node labeled {class_label!r}, got {matches}"
return matches[0]
def _method_node_id(r, class_label, method_label):
class_id = _class_node_id(r, class_label)
node_by_id = {n["id"]: n for n in r["nodes"]}
for e in r["edges"]:
if e["relation"] == "method" and e["source"] == class_id:
node = node_by_id.get(e["target"])
if node and node["label"] == method_label:
return node["id"]
raise AssertionError(f"no method edge {class_label}.{method_label} found")
def _has_call(r, src_id, tgt_id):
return any(
e["relation"] == "calls" and e["source"] == src_id and e["target"] == tgt_id
for e in r["edges"]
)
@pytest.mark.parametrize("extract", [
pytest.param(0, id="tree-sitter"),
pytest.param(1, id="regex-fallback"),
])
def test_calls_scoped_to_own_class(extract):
r = _extractors()[extract](FIXTURE_PATH)
first_configure = _method_node_id(r, "TFirstWidget", "Configure()")
first_reset = _method_node_id(r, "TFirstWidget", "Reset()")
assert _has_call(r, first_configure, first_reset)
@pytest.mark.parametrize("extract", [
pytest.param(0, id="tree-sitter"),
pytest.param(1, id="regex-fallback"),
])
def test_calls_do_not_cross_unrelated_classes(extract):
r = _extractors()[extract](FIXTURE_PATH)
first_configure = _method_node_id(r, "TFirstWidget", "Configure()")
second_reset = _method_node_id(r, "TSecondWidget", "Reset()")
assert not _has_call(r, first_configure, second_reset), (
"TFirstWidget.Configure must not resolve Reset() to the unrelated "
"TSecondWidget.Reset -- same-named methods on unrelated classes must "
"not collapse into a cross-class edge"
)
@pytest.mark.parametrize("extract", [
pytest.param(0, id="tree-sitter"),
pytest.param(1, id="regex-fallback"),
])
def test_calls_scoped_other_direction(extract):
r = _extractors()[extract](FIXTURE_PATH)
second_configure = _method_node_id(r, "TSecondWidget", "Configure()")
second_reset = _method_node_id(r, "TSecondWidget", "Reset()")
first_reset = _method_node_id(r, "TFirstWidget", "Reset()")
assert _has_call(r, second_configure, second_reset)
assert not _has_call(r, second_configure, first_reset), (
"TSecondWidget.Configure must not resolve Reset() to the unrelated "
"TFirstWidget.Reset"
)
@pytest.mark.parametrize("extract", [
pytest.param(0, id="tree-sitter"),
pytest.param(1, id="regex-fallback"),
])
def test_calls_resolve_via_ancestor_chain(extract):
r = _extractors()[extract](FIXTURE_PATH)
derived_run = _method_node_id(r, "TDerivedWidget", "Run()")
base_prepare = _method_node_id(r, "TBaseWidget", "Prepare()")
assert _has_call(r, derived_run, base_prepare), (
"TDerivedWidget.Run should resolve the inherited Prepare() to "
"TBaseWidget.Prepare via the inherits chain"
)