Skip to content
Open
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-08-22 - Lazy Iterator Consumption in AST Traversal
**Learning:** Passing `ast.iter_child_nodes()` directly into generic AST traversal functions that accept `Iterable[ast.AST]` (instead of eagerly materializing them with `list(ast.iter_child_nodes())`) eliminates unnecessary memory allocations per AST node and speeds up recursive walks.
**Action:** When implementing or modifying AST traversal functions, ensure they accept `Iterable[ast.AST]` rather than `list[ast.AST]`, and avoid wrapping generators in `list()` unless multi-pass traversal or length checks are strictly required.
6 changes: 3 additions & 3 deletions src/wardline/scanner/flow_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import ast
from collections.abc import Mapping, Sequence
from collections.abc import Mapping, Iterable, Sequence
from dataclasses import dataclass

from wardline.core.finding import Finding, Location
Expand Down Expand Up @@ -52,7 +52,7 @@ def _arg_taint_local(
return None


def _find_assignment_callee(nodes: Sequence[ast.AST], name: str, entity_node: ast.AST) -> str | None:
def _find_assignment_callee(nodes: Iterable[ast.AST], name: str, entity_node: ast.AST) -> str | None:
result: str | None = None
for node in nodes:
if (
Expand All @@ -67,7 +67,7 @@ def _find_assignment_callee(nodes: Sequence[ast.AST], name: str, entity_node: as
if callee is not None and any(isinstance(t, ast.Name) and t.id == name for t in node.targets):
result = callee
for child in ast.iter_child_nodes(node):
nested = _find_assignment_callee([child] if isinstance(child, ast.stmt) else [], name, entity_node)
nested = _find_assignment_callee((child,) if isinstance(child, ast.stmt) else (), name, entity_node)
if nested is not None:
result = nested
return result
Expand Down
18 changes: 8 additions & 10 deletions src/wardline/scanner/taint/variable_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from wardline.core.taints import _PROVENANCE_CLASH, RAW_ZONE, TRUST_RANK, TaintState, combine

if TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Iterable, Iterator

# Serialisation sinks — calls that cross the representation boundary. Their
# output sheds validation provenance (raw bytes/str), so → UNKNOWN_RAW. This is
Expand Down Expand Up @@ -2519,7 +2519,7 @@ def compute_return_taint(
"""
returns: list[tuple[TaintState, str | None, ast.expr]] = []
_collect_return_paths(
list(func_node.body),
func_node.body,
function_taint,
taint_map,
var_taints,
Expand Down Expand Up @@ -2568,7 +2568,7 @@ def compute_return_callee(
"""
returns: list[tuple[TaintState, str | None, ast.expr]] = []
_collect_return_paths(
list(func_node.body),
func_node.body,
function_taint,
taint_map,
var_taints,
Expand All @@ -2589,14 +2589,14 @@ def compute_return_callee(
# a direct call. Provenance only — never changes a fire/no-fire decision.
for taint, callee, node in returns:
if taint == worst and callee is None and isinstance(node, ast.Name):
indirect = _assignment_callee(list(func_node.body), node.id, worst, function_taint, taint_map, var_taints)
indirect = _assignment_callee(func_node.body, node.id, worst, function_taint, taint_map, var_taints)
if indirect is not None:
return indirect
return None


def _assignment_callee(
nodes: list[ast.AST],
nodes: Iterable[ast.AST],
name: str,
worst: TaintState,
function_taint: TaintState,
Expand Down Expand Up @@ -2629,9 +2629,7 @@ def _assignment_callee(
and _resolve_expr(node.value, function_taint, taint_map, var_taints) == worst
):
result = callee
nested = _assignment_callee(
list(ast.iter_child_nodes(node)), name, worst, function_taint, taint_map, var_taints
)
nested = _assignment_callee(ast.iter_child_nodes(node), name, worst, function_taint, taint_map, var_taints)
if nested is not None:
result = nested
return result
Expand All @@ -2648,7 +2646,7 @@ def _return_callee(node: ast.expr) -> str | None:


def _collect_return_paths(
nodes: list[ast.AST],
nodes: Iterable[ast.AST],
function_taint: TaintState,
taint_map: dict[str, TaintState],
var_taints: dict[str, TaintState],
Expand Down Expand Up @@ -2686,7 +2684,7 @@ def _collect_return_paths(
_CURRENT_VAR_TYPES.reset(token_types)
out.append((taint, _return_callee(node.value), node.value))
_collect_return_paths(
list(ast.iter_child_nodes(node)),
ast.iter_child_nodes(node),
function_taint,
taint_map,
var_taints,
Expand Down
Loading