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.
// Error subtrees are omitted; complete sibling declarations remain usable.
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 stack = vec![tree.root_node()];
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 children: Vec<_> = node.named_children(&mut cursor).collect();
for child in &children {
parents.insert(child.id(), node);
}
stack.extend(children.into_iter().rev());
}
// 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,
kind,
owner: None,
scope: lexical_scope(node, source),
scope: lexical_scope(node, source, &parents),
line: node.start_position().row + 1,
identity,
qualifier,
@ -418,7 +424,7 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
let ty = ancestor
.child_by_field_name("type")
.map(|n| base_type(n, source));
let scope = lexical_scope(ancestor, source);
let scope = lexical_scope(ancestor, source, &parents);
let candidates: Vec<_> = doc
.declarations
.iter()
@ -431,7 +437,13 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
declaration_nodes[*i],
&declarations,
&doc.declarations,
) == enclosing_function(ancestor, &declarations, &doc.declarations)
&parents,
) == enclosing_function(
ancestor,
&declarations,
&doc.declarations,
&parents,
)
})
.map(|(i, _)| i)
.collect();
@ -474,8 +486,8 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
{
continue;
}
let owner = enclosing_function(node, &declarations, &doc.declarations);
let scope = lexical_scope(node, source);
let owner = enclosing_function(node, &declarations, &doc.declarations, &parents);
let scope = lexical_scope(node, source, &parents);
if language == "rust" && node.kind() == "use_declaration" && !has_error(node) {
for (target, binding) in rust_imports(node, source) {
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) {
doc.relationships.push(Relationship {
owner,
shadowed: parameter_shadows(node, &target, source),
shadowed: parameter_shadows(node, &target, source, &parents),
target,
kind: EdgeKind::Calls,
member: false,
@ -580,6 +592,7 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
if node.kind() == "macro_invocation" {
let context = MacroContext {
position: node,
parents: &parents,
owner,
scope: &scope,
self_type: &self_type,
@ -632,7 +645,7 @@ pub(crate) fn extract(language: &str, path: &str, source: &str) -> Result<Docume
None
};
doc.relationships.push(Relationship {
shadowed: !member && parameter_shadows(node, &target, source),
shadowed: !member && parameter_shadows(node, &target, source, &parents),
owner,
target,
member,
@ -832,8 +845,20 @@ fn cpp_leaf_name(name: Node<'_>, source: &str) -> Option<String> {
Some(text(name, source).to_owned())
}
fn lexical_scope(node: Node<'_>, source: &str) -> String {
let mut parent = node.parent();
/// The parent recorded during traversal, else tree-sitter's (O(depth)) lookup.
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();
while let Some(n) = parent {
if matches!(
@ -846,25 +871,26 @@ fn lexical_scope(node: Node<'_>, source: &str) -> String {
.unwrap_or("<anonymous>"),
);
}
parent = n.parent();
parent = parent_of(n, parents);
}
names.reverse();
names.join("::")
}
fn enclosing_function(
node: Node<'_>,
fn enclosing_function<'tree>(
node: Node<'tree>,
ids: &HashMap<usize, usize>,
declarations: &[Declaration],
parents: &HashMap<usize, Node<'tree>>,
) -> Option<usize> {
let mut parent = node.parent();
let mut parent = parent_of(node, parents);
while let Some(n) = parent {
if let Some(&index) = ids.get(&n.id())
&& declarations[index].kind == NodeKind::Function
{
return Some(index);
}
parent = n.parent();
parent = parent_of(n, parents);
}
None
}
@ -1023,8 +1049,13 @@ fn exported_names(node: Node<'_>, name: &str) -> Vec<String> {
Vec::new()
}
fn parameter_shadows(node: Node<'_>, name: &str, source: &str) -> bool {
let mut parent = node.parent();
fn parameter_shadows<'tree>(
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 {
let signature = n
.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
}
@ -1090,7 +1121,7 @@ fn import_bindings(node: Node<'_>, module: &str, source: &str) -> Vec<ImportBind
local: local.into(),
exported: exported.into(),
module: module.into(),
scope: lexical_scope(node, source),
scope: lexical_scope(node, source, &HashMap::new()),
});
}
let mut cursor = n.walk();
@ -1160,7 +1191,7 @@ fn rust_imports(node: Node<'_>, source: &str) -> Vec<(String, Option<ImportBindi
local,
exported: exported.into(),
module: module.into(),
scope: lexical_scope(node, source),
scope: lexical_scope(node, source, &HashMap::new()),
})
};
output.push((target, binding));
@ -1847,6 +1878,7 @@ fn rust_fields(node: Node<'_>, source: &str) -> Vec<(String, String, CppType)> {
struct MacroContext<'a, 'tree> {
position: Node<'tree>,
parents: &'a HashMap<usize, Node<'tree>>,
owner: Option<usize>,
scope: &'a str,
self_type: &'a Option<String>,
@ -1943,7 +1975,12 @@ fn rust_macro_calls(
});
relationships.push(Relationship {
shadowed: !member
&& parameter_shadows(context.position, &target, context.source),
&& parameter_shadows(
context.position,
&target,
context.source,
context.parents,
),
owner: context.owner,
target,
member,