|
14 | 14 | reason: `$bag->empty()` is a genuine member call into that method and must still |
15 | 15 | resolve, and the construct's arguments must still be walked for calls. |
16 | 16 | """ |
| 17 | +import pytest |
| 18 | + |
17 | 19 | from graphify.extract import extract |
18 | 20 |
|
19 | 21 |
|
@@ -256,3 +258,98 @@ def test_non_construct_function_call_still_resolves(tmp_path): |
256 | 258 |
|
257 | 259 | resolved = _edges_between(result, caller_ids, target_ids) |
258 | 260 | assert resolved, "a genuine cross-file function call must still resolve" |
| 261 | + |
| 262 | + |
| 263 | +@pytest.mark.parametrize("name, statement", [ |
| 264 | + ("die", "die($value);"), |
| 265 | + ("eval", "eval($value);"), |
| 266 | + ("array", "$result = array($value);"), |
| 267 | + ("exit", "exit($value);"), |
| 268 | + ("list", "list($value) = $values;"), |
| 269 | + ("unset", "unset($value);"), |
| 270 | +]) |
| 271 | +def test_construct_named_methods_keep_only_real_member_calls(tmp_path, name, statement): |
| 272 | + """Keyword use must not bind, while the same-named member remains callable.""" |
| 273 | + (tmp_path / "Bag.php").write_text( |
| 274 | + "<?php\nclass Bag {\n" |
| 275 | + f" public function {name}($value) {{ return $value; }}\n" |
| 276 | + f" public function constructUse($value, $values) {{ {statement} }}\n" |
| 277 | + f" public function memberUse($value) {{ return $this->{name}($value); }}\n" |
| 278 | + "}\n", |
| 279 | + encoding="utf-8", |
| 280 | + ) |
| 281 | + (tmp_path / "outside.php").write_text( |
| 282 | + f"<?php\nfunction externalUse($value, $values) {{ {statement} }}\n", |
| 283 | + encoding="utf-8", |
| 284 | + ) |
| 285 | + result = _extract_php(tmp_path) |
| 286 | + target = _ids(result, "Bag.php", name) |
| 287 | + local = _ids(result, "Bag.php", "constructUse") |
| 288 | + external = _ids(result, "outside.php", "externalUse") |
| 289 | + member = _ids(result, "Bag.php", "memberUse") |
| 290 | + assert target and local and external and member |
| 291 | + assert not _edges_between(result, local | external, target) |
| 292 | + assert _edges_between(result, member, target) |
| 293 | + |
| 294 | + |
| 295 | +def test_builtin_named_self_method_uses_its_own_class_and_php_case_rules(tmp_path): |
| 296 | + source = tmp_path / "Bag.php" |
| 297 | + source.write_text( |
| 298 | + "<?php\nclass Bag { public function list() {}\n" |
| 299 | + " public function useList() { $this->LIST(); } }\n" |
| 300 | + "class Other { public function list() {} }\n", |
| 301 | + encoding="utf-8", |
| 302 | + ) |
| 303 | + result = _extract_php(tmp_path) |
| 304 | + bag = next(n["id"] for n in result["nodes"] if n["label"] == "Bag") |
| 305 | + other = next(n["id"] for n in result["nodes"] if n["label"] == "Other") |
| 306 | + targets = _ids(result, "Bag.php", "list") |
| 307 | + bag_targets = {e["target"] for e in result["edges"] |
| 308 | + if e["source"] == bag and e["relation"] == "method"} & targets |
| 309 | + other_targets = {e["target"] for e in result["edges"] |
| 310 | + if e["source"] == other and e["relation"] == "method"} & targets |
| 311 | + caller = _ids(result, "Bag.php", "useList") |
| 312 | + assert bag_targets and other_targets and caller |
| 313 | + assert _edges_between(result, caller, bag_targets) |
| 314 | + assert not _edges_between(result, caller, other_targets) |
| 315 | + |
| 316 | + |
| 317 | +def test_builtin_named_self_call_does_not_bind_to_another_class(tmp_path): |
| 318 | + (tmp_path / "Bag.php").write_text( |
| 319 | + "<?php\nclass Bag { public function useList() { $this->list(); } }\n" |
| 320 | + "class Other { public function list() {} }\n", |
| 321 | + encoding="utf-8", |
| 322 | + ) |
| 323 | + result = _extract_php(tmp_path) |
| 324 | + caller = _ids(result, "Bag.php", "useList") |
| 325 | + target = _ids(result, "Bag.php", "list") |
| 326 | + assert caller and target |
| 327 | + assert not _edges_between(result, caller, target) |
| 328 | + |
| 329 | + |
| 330 | +def test_builtin_named_self_call_does_not_bind_to_a_free_function(tmp_path): |
| 331 | + (tmp_path / "Bag.php").write_text( |
| 332 | + "<?php\nfunction open() {}\n" |
| 333 | + "class Bag { public function useOpen() { $this->open(); } }\n", |
| 334 | + encoding="utf-8", |
| 335 | + ) |
| 336 | + result = _extract_php(tmp_path) |
| 337 | + caller = _ids(result, "Bag.php", "useOpen") |
| 338 | + target = _ids(result, "Bag.php", "open") |
| 339 | + assert caller and target |
| 340 | + assert not _edges_between(result, caller, target) |
| 341 | + |
| 342 | + |
| 343 | +def test_builtin_named_this_call_without_a_class_owner_stays_unresolved(tmp_path): |
| 344 | + (tmp_path / "Bag.php").write_text( |
| 345 | + "<?php\nfunction useList() { $this->list(); }\n" |
| 346 | + "function makeListCallable() { return function() { $this->list(); }; }\n" |
| 347 | + "class Other { public function list() {} }\n", |
| 348 | + encoding="utf-8", |
| 349 | + ) |
| 350 | + result = _extract_php(tmp_path) |
| 351 | + target = _ids(result, "Bag.php", "list") |
| 352 | + assert target and _ids(result, "Bag.php", "useList") |
| 353 | + assert _ids(result, "Bag.php", "makeListCallable") |
| 354 | + assert not any(e["relation"] == "calls" and e["target"] in target |
| 355 | + for e in result["edges"]) |
0 commit comments