mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-17 09:29:27 +00:00
feat(extract): support PHP closures (#3409)
The bug:
PHP closures (`anonymous_function_creation_expression` and `arrow_function`)
were invisible to the graph, disconnecting API routes and callbacks from
the backend code.
The fix:
- Added closure syntax node types to _PHP_CONFIG's `function_types`.
- Updated engine.py to synthesize line-bound names (e.g., `{closure@42}`)
for unnamed closures matching these AST node types, enabling them to be
extracted as linkable nodes in the graph without ID collisions.
This commit is contained in:
1 parent
3f82bf7f83
commit
c8c3af1960
3 files changed
+39
-3
No files matched your search
+2
-2
@@ -1023,7 +1023,7 @@ _PHP_CONFIG = LanguageConfig(
|
||||
"enum_declaration",
|
||||
"trait_declaration",
|
||||
}),
|
||||
function_types=frozenset({"function_definition", "method_declaration"}),
|
||||
function_types=frozenset({"function_definition", "method_declaration", "anonymous_function_creation_expression", "arrow_function"}),
|
||||
import_types=frozenset({"namespace_use_clause"}),
|
||||
# object_creation_expression joins the dispatch set so `new Foo(...)` links
|
||||
# the constructing method to Foo (engine has a dedicated PHP branch: the
|
||||
@@ -1041,7 +1041,7 @@ _PHP_CONFIG = LanguageConfig(
|
||||
# enums wrap their members in an enum_declaration_list rather than a
|
||||
# declaration_list, so the body walk needs it to reach enum methods/cases.
|
||||
body_fallback_child_types=("declaration_list", "compound_statement", "enum_declaration_list"),
|
||||
function_boundary_types=frozenset({"function_definition", "method_declaration"}),
|
||||
function_boundary_types=frozenset({"function_definition", "method_declaration", "anonymous_function_creation_expression", "arrow_function"}),
|
||||
import_handler=_import_php,
|
||||
)
|
||||
|
||||
|
||||
@@ -4415,7 +4415,10 @@ def _extract_generic(
|
||||
func_name = _read_text(name_node, source) if name_node else None
|
||||
|
||||
if not func_name:
|
||||
return
|
||||
if t in ("anonymous_function_creation_expression", "arrow_function"):
|
||||
func_name = f"{{closure@{node.start_point[0] + 1}}}"
|
||||
else:
|
||||
return
|
||||
sanitized_name = (
|
||||
config.sanitize_symbol_name_fn(func_name)
|
||||
if config.sanitize_symbol_name_fn is not None
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from pathlib import Path
|
||||
from graphify.extract import extract_php
|
||||
import pytest
|
||||
|
||||
def test_php_closures(tmp_path):
|
||||
src = b'''<?php
|
||||
$app->get('/api', function() {
|
||||
return 1;
|
||||
});
|
||||
$fn = fn($x) => $x + 1;
|
||||
'''
|
||||
php_file = tmp_path / 'dummy.php'
|
||||
php_file.write_bytes(src)
|
||||
|
||||
res = extract_php(php_file)
|
||||
if res.get('error'):
|
||||
pytest.skip(res['error'])
|
||||
|
||||
nodes = {n['id']: n for n in res['nodes']}
|
||||
labels = {n['label']: n for n in res['nodes']}
|
||||
|
||||
# Assert closures are correctly generated
|
||||
assert '{closure@2}()' in labels, "Missing anonymous_function_creation_expression node"
|
||||
assert '{closure@5}()' in labels, "Missing arrow_function node"
|
||||
|
||||
closure_1 = labels['{closure@2}()']
|
||||
closure_2 = labels['{closure@5}()']
|
||||
|
||||
# Check edges
|
||||
edges = res['edges']
|
||||
assert any(e['source'] == 'dummy.php' and e['target'] == closure_1['id'] and e['relation'] == 'contains' for e in edges), "Missing contains edge for closure_1"
|
||||
assert any(e['source'] == 'dummy.php' and e['target'] == closure_2['id'] and e['relation'] == 'contains' for e in edges), "Missing contains edge for closure_2"
|
||||
|
||||
Reference in new issue
Block a user