mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 07:59:40 +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.
(cherry picked from commit c8c3af1960)
This commit is contained in:
1 parent
f2b65b78ec
commit
cd3fd9436e
3 files changed
+39
-3
No files matched your search
+2
-2
@@ -1459,7 +1459,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
|
||||
@@ -1477,7 +1477,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,
|
||||
)
|
||||
|
||||
|
||||
@@ -4928,7 +4928,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