diff --git a/.changepacks/changepack_log_resolver_reference_index.json b/.changepacks/changepack_log_resolver_reference_index.json new file mode 100644 index 000000000..d9343a00e --- /dev/null +++ b/.changepacks/changepack_log_resolver_reference_index.json @@ -0,0 +1,8 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch", + "bindings/devup-ui-binding/package.json": "Patch" + }, + "note": "Resolving the imports of a file parses it once to find where it references each module, instead of once per import resolved. A component with ten imports was parsed again ten times during extraction just to locate possible resolver errors; diagnostics are unchanged", + "date": "2026-11-15T00:00:00.000Z" +} diff --git a/libs/binding/src/resolver_state.rs b/libs/binding/src/resolver_state.rs index 31d1b6092..a82a4a482 100644 --- a/libs/binding/src/resolver_state.rs +++ b/libs/binding/src/resolver_state.rs @@ -1,7 +1,8 @@ +use std::cell::OnceCell; use std::collections::{BTreeSet, HashMap}; use extractor::ResolvedModule; -use extractor::module_reference::{ModuleReferenceLocation, original_module_reference_locations}; +use extractor::module_reference::{ModuleReferenceLocation, original_module_references}; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] struct OriginSite { @@ -18,6 +19,18 @@ struct OriginSites { struct ModuleSource { code: String, ingress: Option, + /// Where the code references each module, found on the first request + references: OnceCell>>, +} + +impl ModuleSource { + fn new(code: &str, ingress: Option) -> Self { + Self { + code: code.to_string(), + ingress, + references: OnceCell::new(), + } + } } /// A callback request with at least one original reference site. @@ -55,13 +68,7 @@ pub struct ResolverState { impl ResolverState { pub fn new(filename: &str, source: &str) -> Self { Self { - sources: HashMap::from([( - filename.to_string(), - ModuleSource { - code: source.to_string(), - ingress: None, - }, - )]), + sources: HashMap::from([(filename.to_string(), ModuleSource::new(source, None))]), failures: BTreeSet::new(), } } @@ -69,30 +76,26 @@ impl ResolverState { pub fn cache(&mut self, module: &ResolvedModule, request: &ResolverRequest) { self.sources.insert( module.path.clone(), - ModuleSource { - code: module.code.clone(), - ingress: Some(request.origins.clone()), - }, + ModuleSource::new(&module.code, Some(request.origins.clone())), ); } pub fn add_source(&mut self, filename: &str, code: &str) { - self.sources.insert( - filename.to_string(), - ModuleSource { - code: code.to_string(), - ingress: None, - }, - ); + self.sources + .insert(filename.to_string(), ModuleSource::new(code, None)); } /// Bind original references before calling JS; recursive exploratory reads /// without a semantic reference retain the real import that loaded the module. pub fn request(&self, specifier: &str, importer: &str) -> Option { let source = self.sources.get(importer)?; - let mut sites = original_module_reference_locations(importer, &source.code, specifier) + let mut sites = source + .references + .get_or_init(|| original_module_references(importer, &source.code)) + .get(specifier) .into_iter() - .map(|location| OriginSite { + .flatten() + .map(|&location| OriginSite { filename: importer.to_string(), location, }); diff --git a/libs/extractor/src/module_reference.rs b/libs/extractor/src/module_reference.rs index ec9100b59..170dbfb0b 100644 --- a/libs/extractor/src/module_reference.rs +++ b/libs/extractor/src/module_reference.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use oxc_allocator::Allocator; use oxc_ast::{ AstKind, @@ -24,6 +26,18 @@ pub fn original_module_reference_locations( source: &str, specifier: &str, ) -> Vec { + original_module_references(filename, source) + .remove(specifier) + .unwrap_or_default() +} + +/// The locations of every module the source references, by specifier, from one +/// parse: what `original_module_reference_locations` finds for each of them. +#[must_use] +pub fn original_module_references( + filename: &str, + source: &str, +) -> HashMap> { let allocator = Allocator::default(); let source_type = SourceType::from_path(filename).unwrap_or_else(|_| SourceType::ts()); let parsed = Parser::new(&allocator, source, source_type).parse(); @@ -36,10 +50,13 @@ pub fn original_module_reference_locations( Statement::ExportAllDeclaration(export) => Some(&export.source), _ => None, }; - if let Some(literal) = literal - && literal.value == specifier - { - offsets.push(literal.span.start); + if let Some(literal) = literal { + offsets.extend( + literal + .value + .as_str() + .map(|value| (literal.span.start, value)), + ); } } let semantic = SemanticBuilder::new() @@ -55,39 +72,49 @@ pub fn original_module_reference_locations( let node = semantic.scoping().get_reference(*reference).node_id(); if let AstKind::CallExpression(call) = semantic.nodes().parent_kind(node) && let [Argument::StringLiteral(literal)] = call.arguments.as_slice() - && literal.value == specifier { - offsets.push(literal.span.start); + offsets.extend( + literal + .value + .as_str() + .map(|value| (literal.span.start, value)), + ); } } } offsets.sort_unstable(); offsets.dedup(); - offsets - .into_iter() - .filter_map(|offset| { - let prefix = source.get(..usize::try_from(offset).ok()?)?; - let mut location = ModuleReferenceLocation { line: 1, column: 1 }; - let mut chars = prefix.chars().peekable(); - while let Some(character) = chars.next() { - match character { - '\r' => { - if chars.peek() == Some(&'\n') { - chars.next(); - } - location.line += 1; - location.column = 1; - } - '\n' | '\u{2028}' | '\u{2029}' => { - location.line += 1; - location.column = 1; + let mut references = HashMap::>::new(); + let mut location = ModuleReferenceLocation { line: 1, column: 1 }; + let mut chars = source.char_indices().peekable(); + for (offset, specifier) in offsets { + let offset = usize::try_from(offset).unwrap_or(usize::MAX); + while let Some(&(index, character)) = chars.peek() { + if index >= offset { + break; + } + chars.next(); + match character { + '\r' => { + if chars.peek().is_some_and(|&(_, next)| next == '\n') { + chars.next(); } - _ => location.column += 1, + location.line += 1; + location.column = 1; + } + '\n' | '\u{2028}' | '\u{2029}' => { + location.line += 1; + location.column = 1; } + _ => location.column += 1, } - Some(location) - }) - .collect() + } + references + .entry(specifier.to_string()) + .or_default() + .push(location); + } + references } #[cfg(test)] diff --git a/libs/extractor/src/module_reference_tests.rs b/libs/extractor/src/module_reference_tests.rs index d1d5fdcdb..8ab98675a 100644 --- a/libs/extractor/src/module_reference_tests.rs +++ b/libs/extractor/src/module_reference_tests.rs @@ -109,3 +109,33 @@ fn locations_when_duplicate_specifiers_and_unicode_are_present() { ] ); } + +#[test] +fn references_index_every_specifier_from_one_parse() { + // Given + let source = "import { a } from './a';\nconst b = require('./b');\nexport * from './a';"; + // When + let references = original_module_references("theme.ts", source); + // Then + assert_eq!(references.len(), 2); + assert_eq!( + references["./a"], + vec![ + ModuleReferenceLocation { + line: 1, + column: 19 + }, + ModuleReferenceLocation { + line: 3, + column: 15 + }, + ] + ); + assert_eq!( + references["./b"], + vec![ModuleReferenceLocation { + line: 2, + column: 19 + }] + ); +}