diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..6dece96c --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-06-25 - AST Node Iteration Traversal Optimization +**Learning:** AST traversal with recursive generators (`yield from`) and `ast.iter_child_nodes()` creates significant overhead due to multiple generator frames. This is especially true for hot-path traversals. +**Action:** Use an explicit stack instead of `yield from` recursion to preserve lazy evaluation and short-circuiting capabilities. Avoid eager list-appending (`list.append()`) to prevent computing the whole subtree when early matches exist. Reverse child nodes via `reversed()` before extending the stack to preserve traversal order. Critically, retain `isinstance(node, ast.AST)` checks to prevent `AttributeError`s when traversing `_fields`, as not all field values are AST nodes (some are strings or literals). diff --git a/src/wardline/scanner/ast_primitives.py b/src/wardline/scanner/ast_primitives.py index 70f565b3..d40596de 100644 --- a/src/wardline/scanner/ast_primitives.py +++ b/src/wardline/scanner/ast_primitives.py @@ -104,39 +104,61 @@ def iter_calls_in_function_body( Header expressions that execute in the enclosing scope (decorators, default values, base classes, metaclass keywords) are still attributed to ``node``. """ + stack: list[ast.AST] = list(reversed(node.body)) + while stack: + current = stack.pop() - def walk_node(current: ast.AST) -> Iterator[ast.Call]: if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)): - for decorator in current.decorator_list: - yield from walk_node(decorator) - yield from _walk_argument_defaults(current.args) - return + kw_defaults = current.args.kw_defaults + for i in range(len(kw_defaults) - 1, -1, -1): + val = kw_defaults[i] + if val is not None: + stack.append(val) + defaults = current.args.defaults + for i in range(len(defaults) - 1, -1, -1): + stack.append(defaults[i]) + decorators = current.decorator_list + for i in range(len(decorators) - 1, -1, -1): + stack.append(decorators[i]) + continue + if isinstance(current, ast.ClassDef): - for decorator in current.decorator_list: - yield from walk_node(decorator) - for base in current.bases: - yield from walk_node(base) - for keyword in current.keywords: - yield from walk_node(keyword.value) - return + keywords = current.keywords + for i in range(len(keywords) - 1, -1, -1): + stack.append(keywords[i].value) + bases = current.bases + for i in range(len(bases) - 1, -1, -1): + stack.append(bases[i]) + decorators = current.decorator_list + for i in range(len(decorators) - 1, -1, -1): + stack.append(decorators[i]) + continue + if isinstance(current, ast.Lambda): - yield from _walk_argument_defaults(current.args) - return + kw_defaults = current.args.kw_defaults + for i in range(len(kw_defaults) - 1, -1, -1): + val = kw_defaults[i] + if val is not None: + stack.append(val) + defaults = current.args.defaults + for i in range(len(defaults) - 1, -1, -1): + stack.append(defaults[i]) + continue + if isinstance(current, ast.Call): yield current - for child in ast.iter_child_nodes(current): - yield from walk_node(child) - - def _walk_argument_defaults(args: ast.arguments) -> Iterator[ast.Call]: - for default in args.defaults: - yield from walk_node(default) - for kw_default in args.kw_defaults: - if kw_default is None: - continue - yield from walk_node(kw_default) - for stmt in node.body: - yield from walk_node(stmt) + for field in reversed(current._fields): + try: + value = getattr(current, field) + except AttributeError: + continue + if isinstance(value, list): + for item in reversed(value): + if isinstance(item, ast.AST): + stack.append(item) + elif isinstance(value, ast.AST): + stack.append(value) def resolve_self_method_fqn(