fix/audit-findings #5

Merged
trevon merged 14 commits from fix/audit-findings into main 2026-09-25 02:39:30 +00:00
Showing only changes of commit f9804976bc - Show all commits

perf(source): record parents during traversal instead of re-walking from the root

tree-sitter's `Node::parent` re-descends from the root, so each call is
O(depth), and `enclosing_function`, `lexical_scope` and
`parameter_shadows` walked ancestors through it for every call site and
declaration: extraction time grew quadratically with nesting depth
(sample profile: >90% of time in ts_node_parent under those three).

The main traversal now records each node's parent, and the three
ancestor walks look it up (falling back to `Node::parent` for nodes the
traversal skips, so results are unchanged).

Measured on one C++ file with a call nested N blocks deep (release,
macOS arm64): N=2.5k/5k/10k/20k took 0.26/1.05/4.24/16.8 s before;
after, 20k takes 0.02 s with the same nodes and call edge. The full
suite (111 tests) passes unchanged.
trvon 2026-09-24 20:19:13 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -242,6 +242,9 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
// Iterative traversal avoids a Rust stack overflow on deeply nested source. // Iterative traversal avoids a Rust stack overflow on deeply nested source.
// Error subtrees are omitted; complete sibling declarations remain usable. // Error subtrees are omitted; complete sibling declarations remain usable.
let mut nodes = Vec::new(); let mut nodes = Vec::new();
// Parent of every traversed node. tree-sitter's `Node::parent` re-descends from
// the root (O(depth)), so ancestor walks through it were quadratic in nesting.
let mut parents: HashMap<usize, Node<'_>> = HashMap::new();
let mut errors: Vec<(usize, usize)> = Vec::new(); let mut errors: Vec<(usize, usize)> = Vec::new();
let mut stack = vec![tree.root_node()]; let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() { while let Some(node) = stack.pop() {
@ -312,6 +315,9 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
} }
let mut cursor = node.walk(); let mut cursor = node.walk();
let children: Vec<_> = node.named_children(&mut cursor).collect(); let children: Vec<_> = node.named_children(&mut cursor).collect();
for child in &children {
parents.insert(child.id(), node);
}
stack.extend(children.into_iter().rev()); stack.extend(children.into_iter().rev());
} }
// Anonymous MISSING tokens (`#endif`, `;`, `}`) are not named children; find // Anonymous MISSING tokens (`#endif`, `;`, `}`) are not named children; find
@ -395,7 +401,7 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
name, name,
kind, kind,
owner: None, owner: None,
scope: lexical_scope(node, source), scope: lexical_scope(node, source, &parents),
line: node.start_position().row + 1, line: node.start_position().row + 1,
identity, identity,
qualifier, qualifier,
@ -418,7 +424,7 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
let ty = ancestor let ty = ancestor
.child_by_field_name("type") .child_by_field_name("type")
.map(|n| base_type(n, source)); .map(|n| base_type(n, source));
let scope = lexical_scope(ancestor, source); let scope = lexical_scope(ancestor, source, &parents);
let candidates: Vec<_> = doc let candidates: Vec<_> = doc
.declarations .declarations
.iter() .iter()
@ -431,7 +437,13 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
declaration_nodes[*i], declaration_nodes[*i],
&declarations, &declarations,
&doc.declarations, &doc.declarations,
) == enclosing_function(ancestor, &declarations, &doc.declarations) &parents,
) == enclosing_function(
ancestor,
&declarations,
&doc.declarations,
&parents,
)
}) })
.map(|(i, _)| i) .map(|(i, _)| i)
.collect(); .collect();
@ -474,8 +486,8 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
{ {
continue; continue;
} }
let owner = enclosing_function(node, &declarations, &doc.declarations); let owner = enclosing_function(node, &declarations, &doc.declarations, &parents);
let scope = lexical_scope(node, source); let scope = lexical_scope(node, source, &parents);
if language == "rust" && node.kind() == "use_declaration" && !has_error(node) { if language == "rust" && node.kind() == "use_declaration" && !has_error(node) {
for (target, binding) in rust_imports(node, source) { for (target, binding) in rust_imports(node, source) {
if owner.is_none() { if owner.is_none() {
@ -539,7 +551,7 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
for (target, line) in cpp_vexing_calls(node, source) { for (target, line) in cpp_vexing_calls(node, source) {
doc.relationships.push(Relationship { doc.relationships.push(Relationship {
owner, owner,
shadowed: parameter_shadows(node, &target, source), shadowed: parameter_shadows(node, &target, source, &parents),
target, target,
kind: EdgeKind::Calls, kind: EdgeKind::Calls,
member: false, member: false,
@ -580,6 +592,7 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
if node.kind() == "macro_invocation" { if node.kind() == "macro_invocation" {
let context = MacroContext { let context = MacroContext {
position: node, position: node,
parents: &parents,
owner, owner,
scope: &scope, scope: &scope,
self_type: &self_type, self_type: &self_type,
@ -632,7 +645,7 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
None None
}; };
doc.relationships.push(Relationship { doc.relationships.push(Relationship {
shadowed: !member && parameter_shadows(node, &target, source), shadowed: !member && parameter_shadows(node, &target, source, &parents),
owner, owner,
target, target,
member, member,
@ -832,8 +845,20 @@ fn cpp_leaf_name(name: Node<'_>, source: &str) -> Option<String> {
Some(text(name, source).to_owned()) Some(text(name, source).to_owned())
} }
fn lexical_scope(node: Node<'_>, source: &str) -> String { /// The parent recorded during traversal, else tree-sitter's (O(depth)) lookup.
let mut parent = node.parent(); fn parent_of<'tree>(
node: Node<'tree>,
parents: &HashMap<usize, Node<'tree>>,
) -> Option<Node<'tree>> {
parents.get(&node.id()).copied().or_else(|| node.parent())
}
fn lexical_scope<'tree>(
node: Node<'tree>,
source: &str,
parents: &HashMap<usize, Node<'tree>>,
) -> String {
let mut parent = parent_of(node, parents);
let mut names = Vec::new(); let mut names = Vec::new();
while let Some(n) = parent { while let Some(n) = parent {
if matches!( if matches!(
@ -846,25 +871,26 @@ fn lexical_scope(node: Node<'_>, source: &str) -> String {
.unwrap_or("<anonymous>"), .unwrap_or("<anonymous>"),
); );
} }
parent = n.parent(); parent = parent_of(n, parents);
} }
names.reverse(); names.reverse();
names.join("::") names.join("::")
} }
fn enclosing_function( fn enclosing_function<'tree>(
node: Node<'_>, node: Node<'tree>,
ids: &HashMap<usize, usize>, ids: &HashMap<usize, usize>,
declarations: &[Declaration], declarations: &[Declaration],
parents: &HashMap<usize, Node<'tree>>,
) -> Option<usize> { ) -> Option<usize> {
let mut parent = node.parent(); let mut parent = parent_of(node, parents);
while let Some(n) = parent { while let Some(n) = parent {
if let Some(&index) = ids.get(&n.id()) if let Some(&index) = ids.get(&n.id())
&& declarations[index].kind == NodeKind::Function && declarations[index].kind == NodeKind::Function
{ {
return Some(index); return Some(index);
} }
parent = n.parent(); parent = parent_of(n, parents);
} }
None None
} }
@ -1023,8 +1049,13 @@ fn exported_names(node: Node<'_>, name: &str) -> Vec<String> {
Vec::new() Vec::new()
} }
fn parameter_shadows(node: Node<'_>, name: &str, source: &str) -> bool { fn parameter_shadows<'tree>(
let mut parent = node.parent(); node: Node<'tree>,
name: &str,
source: &str,
parents: &HashMap<usize, Node<'tree>>,
) -> bool {
let mut parent = parent_of(node, parents);
while let Some(n) = parent { while let Some(n) = parent {
let signature = n let signature = n
.child_by_field_name("signature") .child_by_field_name("signature")
@ -1058,7 +1089,7 @@ fn parameter_shadows(node: Node<'_>, name: &str, source: &str) -> bool {
); );
} }
} }
parent = n.parent(); parent = parent_of(n, parents);
} }
false false
} }
@ -1090,7 +1121,7 @@ fn import_bindings(node: Node<'_>, module: &str, source: &str) -> Vec<ImportBind
local: local.into(), local: local.into(),
exported: exported.into(), exported: exported.into(),
module: module.into(), module: module.into(),
scope: lexical_scope(node, source), scope: lexical_scope(node, source, &HashMap::new()),
}); });
} }
let mut cursor = n.walk(); let mut cursor = n.walk();
@ -1160,7 +1191,7 @@ fn rust_imports(node: Node<'_>, source: &str) -> Vec<(String, Option<ImportBindi
local, local,
exported: exported.into(), exported: exported.into(),
module: module.into(), module: module.into(),
scope: lexical_scope(node, source), scope: lexical_scope(node, source, &HashMap::new()),
}) })
}; };
output.push((target, binding)); output.push((target, binding));
@ -1847,6 +1878,7 @@ fn rust_fields(node: Node<'_>, source: &str) -> Vec<(String, String, CppType)> {
struct MacroContext<'a, 'tree> { struct MacroContext<'a, 'tree> {
position: Node<'tree>, position: Node<'tree>,
parents: &'a HashMap<usize, Node<'tree>>,
owner: Option<usize>, owner: Option<usize>,
scope: &'a str, scope: &'a str,
self_type: &'a Option<String>, self_type: &'a Option<String>,
@ -1943,7 +1975,12 @@ fn rust_macro_calls(
}); });
relationships.push(Relationship { relationships.push(Relationship {
shadowed: !member shadowed: !member
&& parameter_shadows(context.position, &target, context.source), && parameter_shadows(
context.position,
&target,
context.source,
context.parents,
),
owner: context.owner, owner: context.owner,
target, target,
member, member,