chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""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"
|
||||
)
|
||||
Reference in New Issue
Block a user