diff --git a/codegraph-kernel/src/resolve/js_scope_upstream.rs b/codegraph-kernel/src/resolve/js_scope_upstream.rs index da02f1aea..2adcc0637 100644 --- a/codegraph-kernel/src/resolve/js_scope_upstream.rs +++ b/codegraph-kernel/src/resolve/js_scope_upstream.rs @@ -49,6 +49,12 @@ fn visible(scope: Option<(Point, Point)>, point: Point) -> bool { scope.is_none_or(|(start, end)| point >= start && point <= end) } +/// jsDeclarationReachable's memoized walk result per (candidate, ref +/// language). Some(Some(s)): declarator found, scope s. Some(None): +/// found, global scope (always visible). None: no declarator — verdict +/// None. +pub(super) type JsDeclScope = Option>; + impl KernelResolver { /// Parse embedded scripts with the original file's byte and line positions. pub(super) fn js_binding_tree(&mut self, source: &Rc, r: &ResolveRefIn) -> Option> { @@ -315,6 +321,17 @@ impl KernelResolver { (n.start_line - 1).max(0) as usize, super::names::js_unit_to_byte(line, n.start_column.max(0) as usize), ); + // The declarator walk below depends only on the candidate — the ref + // enters only through `visible(scope, point)`. Memoize the walk so a + // name with hundreds of same-file candidates pays the parent-chain + // climb once, not once per ref (minified bundles are the hot case). + // Keyed by ref language too: parsed_tree picks its parser from + // r.language, so the same candidate walks a different tree under + // typescript vs javascript refs (review on #566). + let memo_key = (n.id.clone(), r.language.clone()); + if let Some(&hit) = self.js_decl_scope_memo.get(&memo_key) { + return hit.map(|scope| visible(scope, point)); + } let mut current = tree .root_node() .descendant_for_point_range(declaration, declaration); @@ -328,10 +345,13 @@ impl KernelResolver { .child_by_field_name("name") .is_some_and(|name| source.text()[name.start_byte()..name.end_byte()] == n.name) { - return Some(visible(binding_scope(node), point)); + let scope = binding_scope(node); + self.js_decl_scope_memo.insert(memo_key.clone(), Some(scope)); + return Some(visible(scope, point)); } current = node.parent(); } + self.js_decl_scope_memo.insert(memo_key, None); None } diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index 81fe5e357..f2babf951 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -769,6 +769,10 @@ pub struct KernelResolver { manifest_external_memo: HashMap>>, /// lexicalScopeOf, by candidate id: the scoping function body's lines. lexical_scope_memo: HashMap>, + /// jsDeclarationReachable's candidate-side walk, by (candidate id, + /// ref language) — parsed_tree selects its parser from the ref's + /// language, so the same candidate can walk a different tree. + js_decl_scope_memo: HashMap<(String, String), js_scope_upstream::JsDeclScope>, language_type_scope_memo: HashMap>, scala_package_object_membership_memo: HashMap>, cpp_default_declaration_memo: HashMap, @@ -978,6 +982,7 @@ impl KernelResolver { manifest_own_memo: HashMap::new(), manifest_external_memo: HashMap::new(), lexical_scope_memo: HashMap::new(), + js_decl_scope_memo: HashMap::new(), language_type_scope_memo: HashMap::new(), scala_package_object_membership_memo: HashMap::new(), cpp_default_declaration_memo: HashMap::new(),