From ad652950a3313103eb7da2fbeb7e895287f12745 Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 17:49:49 -0600 Subject: [PATCH 1/2] resolve: memoize jsDeclarationReachable's candidate-side walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parent-chain climb from a candidate's declaration point to its variable_declarator/function_declaration depends only on the candidate — the reference enters only through visible(scope, refPoint). On minified bundles a hot name yields hundreds of same-file candidates and the climb re-ran once per ref. Memoize the walk result per candidate id (three states: declarator found with scope, found global, not found). The per-ref cost collapses to a hash hit plus the visible() range check. --- codegraph-kernel/src/resolve/js_scope_upstream.rs | 12 +++++++++++- codegraph-kernel/src/resolve/mod.rs | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/codegraph-kernel/src/resolve/js_scope_upstream.rs b/codegraph-kernel/src/resolve/js_scope_upstream.rs index da02f1aea..008fcd5a3 100644 --- a/codegraph-kernel/src/resolve/js_scope_upstream.rs +++ b/codegraph-kernel/src/resolve/js_scope_upstream.rs @@ -315,6 +315,13 @@ 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). + if let Some(&hit) = self.js_decl_scope_memo.get(&n.id) { + return hit.map(|scope| visible(scope, point)); + } let mut current = tree .root_node() .descendant_for_point_range(declaration, declaration); @@ -328,10 +335,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(n.id.clone(), Some(scope)); + return Some(visible(scope, point)); } current = node.parent(); } + self.js_decl_scope_memo.insert(n.id.clone(), None); None } diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index 81fe5e357..06c8d335b 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. + /// Some(Some(s)): declarator found, scope s. Some(None): found, + /// global scope (always visible). None: no declarator — verdict None. + js_decl_scope_memo: HashMap>>, 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(), From 9bca1843892d4ca38b1ba9f08598c920afd78fdf Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 18:04:56 -0600 Subject: [PATCH 2/2] resolve: key js_decl_scope_memo by ref language too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit on #566: parsed_tree selects its parser from r.language, so the same candidate can walk a different tree under typescript vs javascript refs — memoizing by candidate id alone could reuse a scope computed under the wrong parse. Key by (candidate id, ref language); type alias for the three-state value keeps clippy's complexity gate. --- .../src/resolve/js_scope_upstream.rs | 16 +++++++++++++--- codegraph-kernel/src/resolve/mod.rs | 8 ++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/codegraph-kernel/src/resolve/js_scope_upstream.rs b/codegraph-kernel/src/resolve/js_scope_upstream.rs index 008fcd5a3..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> { @@ -319,7 +325,11 @@ impl KernelResolver { // 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). - if let Some(&hit) = self.js_decl_scope_memo.get(&n.id) { + // 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 @@ -336,12 +346,12 @@ impl KernelResolver { .is_some_and(|name| source.text()[name.start_byte()..name.end_byte()] == n.name) { let scope = binding_scope(node); - self.js_decl_scope_memo.insert(n.id.clone(), Some(scope)); + 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(n.id.clone(), None); + 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 06c8d335b..f2babf951 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -769,10 +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. - /// Some(Some(s)): declarator found, scope s. Some(None): found, - /// global scope (always visible). None: no declarator — verdict None. - js_decl_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,