From 56093fd730b1c13dfed984c3ef5301d16aeb6f65 Mon Sep 17 00:00:00 2001 From: Harnoor Lal Date: Sun, 22 Feb 2026 15:29:35 -0800 Subject: [PATCH] fix(parser): extract side-effect imports from requested_modules Side-effect imports (import "./polyfill") were silently dropped because extract_all only read ModuleRecord.import_entries, which requires bindings. Scan requested_modules for import statements not covered by import/export entries, with a fast-path skip when all entries are already covered (the common case). Closes #171 --- src/lang/typescript/parser.rs | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/lang/typescript/parser.rs b/src/lang/typescript/parser.rs index 7660f2b..aa5669b 100644 --- a/src/lang/typescript/parser.rs +++ b/src/lang/typescript/parser.rs @@ -44,6 +44,12 @@ fn extract_all(source: &str, source_type: SourceType) -> ParseResult { extract_export_entries(&ret.module_record.star_export_entries, &mut positioned); extract_export_entries(&ret.module_record.indirect_export_entries, &mut positioned); + // --- Side-effect imports from requested_modules --- + // import_entries only contains imports with bindings (import { x } from "y"). + // Side-effect imports (import "polyfill") appear only in requested_modules. + // Called after import/export entries so `positioned` already has their offsets. + extract_side_effect_imports(&ret.module_record, &mut positioned); + // --- Dynamic imports from ModuleRecord --- for di in &ret.module_record.dynamic_imports { let start = di.module_request.start as usize; @@ -131,6 +137,45 @@ fn extract_import_entries( } } +/// Pick up side-effect imports (`import "polyfill"`) that have no bindings and +/// therefore no entry in `import_entries`. These appear in `requested_modules` +/// but are excluded from import/export entry lists. +/// +/// Must be called AFTER `extract_import_entries` and `extract_export_entries` so +/// that `positioned` already contains offsets for all binding-bearing statements. +fn extract_side_effect_imports( + record: &oxc_syntax::module_record::ModuleRecord<'_>, + positioned: &mut Vec, +) { + // Fast path: if every requested_module occurrence is already covered by an + // import/export entry in `positioned`, there are no side-effect-only imports. + let total_requested: usize = record.requested_modules.values().map(|v| v.len()).sum(); + if positioned.len() >= total_requested { + return; + } + + for (specifier, occurrences) in &record.requested_modules { + for req in occurrences { + let offset = req.statement_span.start; + if positioned.iter().any(|p| p.offset == offset) { + continue; + } + let kind = if req.is_type { + EdgeKind::TypeOnly + } else { + EdgeKind::Static + }; + positioned.push(PositionedImport { + offset, + import: RawImport { + specifier: specifier.to_string(), + kind, + }, + }); + } + } +} + /// Process `ModuleRecord` export entries (`star_export_entries` or `indirect_export_entries`), /// grouping by `module_request` to determine type-only status. fn extract_export_entries( @@ -629,4 +674,45 @@ mod tests { assert_eq!(result.imports[0].specifier, "./foo"); assert_eq!(result.unresolvable_dynamic, 0); } + + // --- Side-effect imports (#171) --- + + #[test] + fn side_effect_import() { + let imports = parse_ts(r#"import "./polyfill";"#); + assert_eq!(imports.len(), 1, "side-effect import should be extracted"); + assert_eq!(imports[0].specifier, "./polyfill"); + assert_eq!(imports[0].kind, EdgeKind::Static); + } + + #[test] + fn side_effect_import_with_named() { + // Side-effect import alongside named import from a different module + let imports = parse_ts( + r#" + import "./setup"; + import { foo } from "bar"; + "#, + ); + assert_eq!(imports.len(), 2); + assert_eq!(imports[0].specifier, "./setup"); + assert_eq!(imports[0].kind, EdgeKind::Static); + assert_eq!(imports[1].specifier, "bar"); + } + + #[test] + fn side_effect_import_not_duplicated() { + // If same module has both side-effect and named import, should produce one entry each + let imports = parse_ts( + r#" + import "./polyfill"; + import { x } from "./polyfill"; + "#, + ); + assert_eq!( + imports.len(), + 2, + "both import statements should produce entries" + ); + } }