Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-06-13 - Fast AST Traversal
**Learning:** In hot-path AST traversal, deep `yield from` recursion and repeated `isinstance()` checks create measurable overhead (especially generator frame creation). AST node classes are final (not subclassed), meaning `type(node) is ast.X` is perfectly safe and much faster.
**Action:** When writing utilities that traverse the entire AST (like `iter_calls_in_function_body`), use `type() is` checks and eager list-appending (`out.append`) instead of `yield from`. Cast the result to an iterator if needed to maintain API compatibility.
58 changes: 31 additions & 27 deletions src/wardline/scanner/ast_primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,39 +104,43 @@ 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``.
"""

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)
out: list[ast.Call] = []
append = out.append

def walk_node(current: ast.AST) -> None:
typ = type(current)
if typ is ast.FunctionDef or typ is ast.AsyncFunctionDef:
for decorator in current.decorator_list: # type: ignore[attr-defined]
walk_node(decorator)
for default in current.args.defaults: # type: ignore[attr-defined]
walk_node(default)
for kw_default in current.args.kw_defaults: # type: ignore[attr-defined]
if kw_default is not None:
walk_node(kw_default)
return
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)
if typ is ast.ClassDef:
for decorator in current.decorator_list: # type: ignore[attr-defined]
walk_node(decorator)
for base in current.bases: # type: ignore[attr-defined]
walk_node(base)
for keyword in current.keywords: # type: ignore[attr-defined]
walk_node(keyword.value)
return
if isinstance(current, ast.Lambda):
yield from _walk_argument_defaults(current.args)
if typ is ast.Lambda:
for default in current.args.defaults: # type: ignore[attr-defined]
walk_node(default)
for kw_default in current.args.kw_defaults: # type: ignore[attr-defined]
if kw_default is not None:
walk_node(kw_default)
return
if isinstance(current, ast.Call):
yield current
if typ is ast.Call:
append(current) # type: ignore[arg-type]
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)
walk_node(child)

for stmt in node.body:
yield from walk_node(stmt)
walk_node(stmt)
return iter(out)


def resolve_self_method_fqn(
Expand Down
Loading