refactor(extract): hoist PHP route helper to module level (#3409)

Address maintainer nits by moving _php_get_route_name and the
_PHP_ROUTING_VERBS frozenset out of the _extract_generic body
to the module level, preventing them from being repeatedly
re-initialized per file on large codebases.

(cherry picked from commit e845f15b68)
This commit is contained in:
nikhil2004 authored and safishamsi committed 2026-09-23 19:42:04 +01:00
1 parent 8227c468c2
commit 435f5a88e9
1 file changed
+60 -60
+60 -60
View File
@@ -3457,6 +3457,66 @@ def _lua_is_require_call(node, source: bytes) -> bool:
return False
return _read_text(name_node, source) == "require"
_PHP_ROUTING_VERBS = frozenset({"get", "post", "put", "patch", "delete", "options", "any", "match", "map"})
def _php_get_route_name(closure_node, src: bytes) -> str | None:
"""Walk up the AST to extract grouped routing prefixes (#3409)."""
prefixes = []
verb = None
curr = closure_node.parent
while curr is not None:
if curr.type in ("function_definition", "method_declaration", "class_declaration"):
break
if curr.type == "argument":
arg_list = curr.parent
if arg_list is not None and arg_list.type == "argument_list":
call = arg_list.parent
if call is not None and call.type in ("member_call_expression", "function_call_expression", "scoped_call_expression"):
name_node = call.child_by_field_name("name")
if name_node is None:
name_node = call.child_by_field_name("function")
raw_method = (_read_text(name_node, src) if name_node else "").lower()
path_text = None
for sibling in arg_list.children:
if sibling is curr:
break
if sibling.type in ("string", "encapsed_string"):
path_text = _read_text(sibling, src).strip("'\"")
break
if verb is None:
# The innermost call must be a routing verb with a path starting with '/'
if path_text is not None and path_text.startswith("/") and raw_method in _PHP_ROUTING_VERBS:
verb = raw_method.upper()
prefixes.append(path_text)
else:
return None # Not a valid route closure
else:
# Outer calls (e.g. group(), prefix()) just contribute their prefix if it starts with '/'
if path_text is not None and path_text.startswith("/"):
prefixes.append(path_text)
curr = call.parent
continue
elif curr.type in ("anonymous_function_creation_expression", "arrow_function"):
# Jump across the closure boundary to its containing argument
curr = curr.parent
continue
curr = curr.parent
if verb and prefixes:
# prefixes are inside-out (innermost path is first)
# e.g. ['/users/{id}', '/api/v1'] -> '/api/v1/users/{id}'
full_path = "/" + "/".join(p.strip("/") for p in reversed(prefixes) if p.strip("/"))
return f"{verb} {full_path}"
return None
def _extract_generic(
path: Path, config: LanguageConfig, *, source_override: bytes | None = None,
@@ -3559,66 +3619,6 @@ def _extract_generic(
php_closure_counts: dict[str, int] = {}
pending_listen_edges: list[tuple[str, str, int]] = []
def _php_get_route_name(closure_node, src: bytes) -> str | None:
"""Walk up the AST to extract grouped routing prefixes (#3409)."""
_routing_verbs = frozenset({"get", "post", "put", "patch", "delete", "options", "any", "match", "map"})
prefixes = []
verb = None
curr = closure_node.parent
while curr is not None:
if curr.type in ("function_definition", "method_declaration", "class_declaration"):
break
if curr.type == "argument":
arg_list = curr.parent
if arg_list is not None and arg_list.type == "argument_list":
call = arg_list.parent
if call is not None and call.type in ("member_call_expression", "function_call_expression", "scoped_call_expression"):
name_node = call.child_by_field_name("name")
if name_node is None:
name_node = call.child_by_field_name("function")
raw_method = (_read_text(name_node, src) if name_node else "").lower()
path_text = None
for sibling in arg_list.children:
if sibling is curr:
break
if sibling.type in ("string", "encapsed_string"):
path_text = _read_text(sibling, src).strip("'\"")
break
if verb is None:
# The innermost call must be a routing verb with a path starting with '/'
if path_text is not None and path_text.startswith("/") and raw_method in _routing_verbs:
verb = raw_method.upper()
prefixes.append(path_text)
else:
return None # Not a valid route closure
else:
# Outer calls (e.g. group(), prefix()) just contribute their prefix if it starts with '/'
if path_text is not None and path_text.startswith("/"):
prefixes.append(path_text)
curr = call.parent
continue
elif curr.type in ("anonymous_function_creation_expression", "arrow_function"):
# Jump across the closure boundary to its containing argument
curr = curr.parent
continue
curr = curr.parent
if verb and prefixes:
# prefixes are inside-out (innermost path is first)
# e.g. ['/users/{id}', '/api/v1'] -> '/api/v1/users/{id}'
full_path = "/" + "/".join(p.strip("/") for p in reversed(prefixes) if p.strip("/"))
return f"{verb} {full_path}"
return None
# tree-sitter-swift parses both `class Foo` and `extension Foo` as
# `class_declaration`. Same-file pairs collapse via seen_ids, but cross-file
# extensions don't (file stem is part of the id), so they're collected here