mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-22 21:45:58 +00:00
`self.product.name` dot-syntax now emits an `accesses` edge and `@selector(method)` emits a `calls` edge, both resolved only to an unambiguous in-scope definition (a sibling method of the same class for dot-syntax; exactly one method by exact selector name for @selector) so no false-edge fan-out occurs when multiple classes share a name. Hardened over the original PR: resolution now matches the method node id EXACTLY (a method id is _make_id(container, name)) rather than by `endswith` suffix. The substring match would mis-resolve `self.name` to a sibling `-surname` (false positive) and, when a substring-colliding sibling existed, suppress the correct edge (false negative); exact matching fixes both. Adds substring-collision regression tests (`-name`/`-surname`, `-doThing`/`-reallyDoThing`). Completes the #1475 ObjC follow-ups (Bug 5 dot-syntax accesses, Bug 6b @selector target-action). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
1801da0634
commit
0792b419fc
@@ -1214,6 +1214,136 @@ def test_objc_alloc_init_unknown_class_no_resolved_edge(tmp_path):
|
||||
assert e["target"] not in sourced_ids, f"unexpected resolved ref: {e}"
|
||||
|
||||
|
||||
def test_objc_dot_syntax_property_accesses_edge(tmp_path):
|
||||
"""self.name dot-syntax resolves to an accesses edge within the same class."""
|
||||
p = tmp_path / "Dog.m"
|
||||
p.write_text(
|
||||
"@implementation Dog\n"
|
||||
"- (NSString *)name { return @\"Rex\"; }\n"
|
||||
"- (void)greet { NSLog(@\"%@\", self.name); }\n"
|
||||
"@end\n"
|
||||
)
|
||||
r = extract_objc(p)
|
||||
accesses = [(e["source"], e["target"]) for e in r["edges"]
|
||||
if e["relation"] == "accesses"]
|
||||
nid2label = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
assert len(accesses) == 1
|
||||
assert nid2label[accesses[0][1]] == "-name"
|
||||
|
||||
|
||||
def test_objc_dot_syntax_no_fanout_two_same_named_properties(tmp_path):
|
||||
"""Two classes each declaring -name: self.name in A must NOT fan out to B's -name."""
|
||||
p = tmp_path / "AB.m"
|
||||
p.write_text(
|
||||
"@implementation A\n"
|
||||
"- (NSString *)name { return @\"A\"; }\n"
|
||||
"- (void)show { NSLog(@\"%@\", self.name); }\n"
|
||||
"@end\n"
|
||||
"@implementation B\n"
|
||||
"- (NSString *)name { return @\"B\"; }\n"
|
||||
"- (void)show { NSLog(@\"%@\", self.name); }\n"
|
||||
"@end\n"
|
||||
)
|
||||
r = extract_objc(p)
|
||||
accesses = [e for e in r["edges"] if e["relation"] == "accesses"]
|
||||
assert len(accesses) == 2, f"expected 2 scoped accesses, got {len(accesses)}: {accesses}"
|
||||
nid2label = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
for e in accesses:
|
||||
src_label = nid2label[e["source"]]
|
||||
tgt_label = nid2label[e["target"]]
|
||||
assert src_label == "-show" and tgt_label == "-name"
|
||||
|
||||
|
||||
def test_objc_dot_syntax_unresolvable_property_zero_edges(tmp_path):
|
||||
"""Accessing a property not defined in the current class produces zero accesses edges."""
|
||||
p = tmp_path / "X.m"
|
||||
p.write_text(
|
||||
"@implementation X\n"
|
||||
"- (void)run { NSLog(@\"%@\", self.missing); }\n"
|
||||
"@end\n"
|
||||
)
|
||||
r = extract_objc(p)
|
||||
accesses = [e for e in r["edges"] if e["relation"] == "accesses"]
|
||||
assert len(accesses) == 0
|
||||
|
||||
|
||||
def test_objc_selector_expression_calls_edge(tmp_path):
|
||||
"""@selector(uniqueMethod) with exactly one match produces a calls edge."""
|
||||
p = tmp_path / "Sched.m"
|
||||
p.write_text(
|
||||
"@implementation Sched\n"
|
||||
"- (void)fetch { }\n"
|
||||
"- (void)schedule { [self performSelector:@selector(fetch)]; }\n"
|
||||
"@end\n"
|
||||
)
|
||||
r = extract_objc(p)
|
||||
nid2label = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
sel_calls = [(nid2label.get(e["source"]), nid2label.get(e["target"]))
|
||||
for e in r["edges"]
|
||||
if e["relation"] == "calls" and e.get("context") == "call"]
|
||||
assert ("-schedule", "-fetch") in sel_calls
|
||||
|
||||
|
||||
def test_objc_selector_no_fanout_two_same_named_methods(tmp_path):
|
||||
"""@selector(doThing) with two doThing methods must emit zero calls edges."""
|
||||
p = tmp_path / "Dual.m"
|
||||
p.write_text(
|
||||
"@implementation A\n"
|
||||
"- (void)doThing { }\n"
|
||||
"- (void)run { [self performSelector:@selector(doThing)]; }\n"
|
||||
"@end\n"
|
||||
"@implementation B\n"
|
||||
"- (void)doThing { }\n"
|
||||
"@end\n"
|
||||
)
|
||||
r = extract_objc(p)
|
||||
nid2label = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
sel_edges = [e for e in r["edges"]
|
||||
if e["relation"] == "calls"
|
||||
and nid2label.get(e["target"], "").endswith("doThing")]
|
||||
assert len(sel_edges) == 0, f"expected 0 selector edges with ambiguous name, got {sel_edges}"
|
||||
|
||||
|
||||
def test_objc_dot_syntax_substring_sibling_exact_match(tmp_path):
|
||||
"""A substring-colliding sibling must neither be falsely matched nor suppress
|
||||
the real match: `self.name` with both `-name` and `-surname` present resolves
|
||||
to `-name` ONLY (exact id, not a `endswith` suffix) (#1475)."""
|
||||
p = tmp_path / "Person.m"
|
||||
p.write_text(
|
||||
"@implementation Person\n"
|
||||
"- (NSString *)name { return @\"n\"; }\n"
|
||||
"- (NSString *)surname { return @\"s\"; }\n"
|
||||
"- (void)show { NSLog(@\"%@\", self.name); }\n"
|
||||
"@end\n"
|
||||
)
|
||||
r = extract_objc(p)
|
||||
nid2label = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
accesses = [(nid2label.get(e["source"]), nid2label.get(e["target"]))
|
||||
for e in r["edges"] if e["relation"] == "accesses"]
|
||||
assert ("-show", "-name") in accesses, f"self.name must resolve to -name: {accesses}"
|
||||
assert ("-show", "-surname") not in accesses, f"self.name must NOT match -surname: {accesses}"
|
||||
|
||||
|
||||
def test_objc_selector_substring_method_exact_match(tmp_path):
|
||||
"""@selector(doThing) must resolve to `-doThing` exactly, not be suppressed by
|
||||
a substring-colliding `-reallyDoThing` (exact match, not suffix) (#1475)."""
|
||||
p = tmp_path / "Worker.m"
|
||||
p.write_text(
|
||||
"@implementation Worker\n"
|
||||
"- (void)doThing { }\n"
|
||||
"- (void)reallyDoThing { }\n"
|
||||
"- (void)run { [self performSelector:@selector(doThing)]; }\n"
|
||||
"@end\n"
|
||||
)
|
||||
r = extract_objc(p)
|
||||
nid2label = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
sel_calls = [(nid2label.get(e["source"]), nid2label.get(e["target"]))
|
||||
for e in r["edges"]
|
||||
if e["relation"] == "calls" and e.get("context") == "call"]
|
||||
assert ("-run", "-doThing") in sel_calls, f"@selector(doThing) must resolve to -doThing: {sel_calls}"
|
||||
assert ("-run", "-reallyDoThing") not in sel_calls
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Go
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user