From ec025c001c831b625f7b47282585d74b6311acc9 Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Tue, 4 Apr 2023 01:05:10 +0100 Subject: [PATCH] feat: store import sites on dependencies (#251) --- Cargo.lock | 1 + Cargo.toml | 3 +- src/analyzer.rs | 6 + src/graph.rs | 369 +++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 344 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ec97fbf4..a5a58f26c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -415,6 +415,7 @@ checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index eab4b326e..5a0582541 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ data-url = "0.2.0" deno_ast = { version = "0.25.0", features = ["dep_graph", "module_specifier"] } deno_semver = "0.2.0" futures = "0.3.26" -indexmap = "1.9.2" +indexmap = { version = "1.9.2", features = ["serde"] } +monch = "0.4.1" once_cell = "1.16.0" parking_lot = "0.12.0" regex = "1.5.4" diff --git a/src/analyzer.rs b/src/analyzer.rs index 12904f529..006612624 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -157,6 +157,12 @@ pub enum ImportAssertions { Known(HashMap), } +impl Default for ImportAssertions { + fn default() -> Self { + Self::None + } +} + impl ImportAssertions { // can't use this type directly because we need to make it serialize & deserialize pub fn from_swc(value: deno_ast::swc::dep_graph::ImportAssertions) -> Self { diff --git a/src/graph.rs b/src/graph.rs index e2008a4fb..9ce1bad03 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -8,6 +8,7 @@ use crate::analyzer::PositionRange; use crate::analyzer::SpecifierWithRange; use crate::analyzer::TypeScriptReference; use crate::DefaultModuleAnalyzer; +use crate::ImportAssertions; use crate::ReferrerImports; use crate::module_specifier::resolve_import; @@ -470,7 +471,57 @@ fn is_false(v: &bool) -> bool { !v } -#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum ImportKind { + /// `import`/`export` + Es, + /// `import type`/`export type` + TsType, + /// `/// ` + TsReferencePath, + /// `/// ` + TsReferenceTypes, + /// `/** @jsxImportSource ... */` + JsxImportSource, + /// `/** @typedef { import("./types").Pet } Pet */` + JsDoc, +} + +impl ImportKind { + pub fn is_runtime(&self) -> bool { + match self { + ImportKind::Es | ImportKind::JsxImportSource => true, + ImportKind::TsType + | ImportKind::TsReferencePath + | ImportKind::TsReferenceTypes + | ImportKind::JsDoc => false, + } + } + + fn is_es(&self) -> bool { + matches!(self, ImportKind::Es) + } +} + +#[derive(Clone, Debug, Serialize)] +#[cfg_attr(test, derive(Eq, PartialEq))] +#[serde(rename_all = "camelCase")] +pub struct Import { + pub specifier: String, + #[serde(skip_serializing_if = "ImportKind::is_es")] + pub kind: ImportKind, + pub range: Range, + #[serde(skip_serializing_if = "is_false")] + pub is_dynamic: bool, + // Don't include assertions in `deno info --json`, since they may be unstable: + // https://github.com/denoland/deno/issues/17944. Assertion error strings + // eventually will be included in a separate `Import::errors`, however. + #[serde(skip_serializing)] + pub assertions: ImportAssertions, +} + +#[derive(Debug, Default, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct Dependency { #[serde(rename = "code", skip_serializing_if = "Resolution::is_none")] @@ -481,6 +532,10 @@ pub struct Dependency { pub is_dynamic: bool, #[serde(rename = "assertionType", skip_serializing_if = "Option::is_none")] pub maybe_assert_type: Option, + // TODO(nayeemrmn): Replace `maybe_assert_type` with this in the serialization + // for 2.0. + #[serde(skip_serializing)] + pub imports: Vec, } impl Dependency { @@ -713,7 +768,7 @@ fn to_result<'a>( /// module graph without requiring the dependencies to be analyzed. This is /// intended to be used for importing type dependencies or other externally /// defined dependencies, like JSX runtimes. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Serialize)] pub struct GraphImport { /// A map of resolved dependencies, where the key is the value originally /// provided for the import and the value is the resolved dependency. @@ -743,6 +798,7 @@ impl GraphImport { maybe_code: Resolution::None, maybe_type, maybe_assert_type: None, + imports: vec![], }, ) }) @@ -1632,28 +1688,45 @@ pub(crate) fn parse_esm_module_from_module_info( .dependencies .entry(specifier.text.clone()) .or_default(); + let range = + Range::from_position_range(module.specifier.clone(), specifier.range); if dep.maybe_type.is_none() { - let range = Range::from_position_range( - module.specifier.clone(), - specifier.range, - ); - dep.maybe_type = resolve(&specifier.text, range, maybe_resolver); + dep.maybe_type = + resolve(&specifier.text, range.clone(), maybe_resolver); } + dep.imports.push(Import { + specifier: specifier.text, + kind: ImportKind::TsReferencePath, + range, + is_dynamic: false, + assertions: Default::default(), + }); } TypeScriptReference::Types(specifier) => { let range = Range::from_position_range(module.specifier.clone(), specifier.range); - let dep_resolution = resolve(&specifier.text, range, maybe_resolver); + let dep_resolution = + resolve(&specifier.text, range.clone(), maybe_resolver); if is_untyped(&module.media_type) { module.maybe_types_dependency = Some(TypesDependency { specifier: specifier.text.clone(), dependency: dep_resolution, }); } else { - let dep = module.dependencies.entry(specifier.text).or_default(); + let dep = module + .dependencies + .entry(specifier.text.clone()) + .or_default(); if dep.maybe_type.is_none() { dep.maybe_type = dep_resolution; } + dep.imports.push(Import { + specifier: specifier.text, + kind: ImportKind::TsReferenceTypes, + range, + is_dynamic: false, + assertions: Default::default(), + }); } } } @@ -1689,13 +1762,21 @@ pub(crate) fn parse_esm_module_from_module_info( .dependencies .entry(specifier_text.clone()) .or_default(); + let range = Range::from_position_range( + module.specifier.clone(), + import_source.range, + ); if dep.maybe_code.is_none() { - let range = Range::from_position_range( - module.specifier.clone(), - import_source.range, - ); - dep.maybe_code = resolve(&specifier_text, range, maybe_resolver); + dep.maybe_code = + resolve(&specifier_text, range.clone(), maybe_resolver); } + dep.imports.push(Import { + specifier: specifier_text, + kind: ImportKind::JsxImportSource, + range, + is_dynamic: false, + assertions: Default::default(), + }); } } @@ -1705,11 +1786,18 @@ pub(crate) fn parse_esm_module_from_module_info( .dependencies .entry(specifier.text.clone()) .or_default(); + let range = + Range::from_position_range(module.specifier.clone(), specifier.range); if dep.maybe_type.is_none() { - let range = - Range::from_position_range(module.specifier.clone(), specifier.range); - dep.maybe_type = resolve(&specifier.text, range, maybe_resolver); + dep.maybe_type = resolve(&specifier.text, range.clone(), maybe_resolver); } + dep.imports.push(Import { + specifier: specifier.text, + kind: ImportKind::JsDoc, + range, + is_dynamic: false, + assertions: Default::default(), + }); } // Analyze the X-TypeScript-Types header @@ -1781,14 +1869,12 @@ pub(crate) fn parse_esm_module_from_module_info( if dep.maybe_assert_type.is_none() { dep.maybe_assert_type = desc.import_assertions.get("type").cloned(); } - let dep_resolution = resolve( - &desc.specifier, - Range::from_position_range( - module.specifier.clone(), - desc.specifier_range.clone(), - ), - maybe_resolver, + let range = Range::from_position_range( + module.specifier.clone(), + desc.specifier_range.clone(), ); + let dep_resolution = + resolve(&desc.specifier, range.clone(), maybe_resolver); if matches!( desc.kind, DependencyKind::ImportType | DependencyKind::ExportType @@ -1796,17 +1882,33 @@ pub(crate) fn parse_esm_module_from_module_info( if dep.maybe_type.is_none() { dep.maybe_type = dep_resolution; } - } else if dep.maybe_code.is_none() { - // This is a code import, the first one of that specifier in this module. - // Resolve and determine the initial `is_dynamic` value from it. - dep.maybe_code = dep_resolution; - dep.is_dynamic = desc.is_dynamic; + dep.imports.push(Import { + specifier: desc.specifier.clone(), + kind: ImportKind::TsType, + range, + is_dynamic: desc.is_dynamic, + assertions: desc.import_assertions.clone(), + }); } else { - // This is a code import, but not the first one of that specifier in this - // module. Maybe update the `is_dynamic` value. Static imports take - // precedence. Note that `@jsxImportSource` and `/// ` - // count as static imports for this purpose. - dep.is_dynamic = dep.is_dynamic && desc.is_dynamic; + if dep.maybe_code.is_none() { + // This is a code import, the first one of that specifier in this module. + // Resolve and determine the initial `is_dynamic` value from it. + dep.maybe_code = dep_resolution; + dep.is_dynamic = desc.is_dynamic; + } else { + // This is a code import, but not the first one of that specifier in this + // module. Maybe update the `is_dynamic` value. Static imports take + // precedence. Note that `@jsxImportSource` and `/// ` + // count as static imports for this purpose. + dep.is_dynamic = dep.is_dynamic && desc.is_dynamic; + } + dep.imports.push(Import { + specifier: desc.specifier.clone(), + kind: ImportKind::Es, + range, + is_dynamic: desc.is_dynamic, + assertions: desc.import_assertions.clone(), + }); } if dep.maybe_type.is_none() { let specifier = module.specifier.clone(); @@ -2514,6 +2616,7 @@ where #[cfg(test)] mod tests { use crate::DefaultModuleAnalyzer; + use crate::ImportAssertion; use pretty_assertions::assert_eq; use super::*; @@ -3015,4 +3118,202 @@ mod tests { .await; assert!(loader.loaded_bar); } + + #[tokio::test] + async fn dependency_imports() { + struct TestLoader; + impl Loader for TestLoader { + fn load( + &mut self, + specifier: &ModuleSpecifier, + is_dynamic: bool, + ) -> LoadFuture { + let specifier = specifier.clone(); + match specifier.as_str() { + "file:///foo.ts" => Box::pin(async move { + Ok(Some(LoadResponse::Module { + specifier: specifier.clone(), + maybe_headers: None, + content: " + /// + /// + /* @jsxImportSource file:///bar.ts */ + import 'file:///bar.ts'; + await import('file:///bar.ts'); + await import('file:///bar.ts', { assert: eval('') }); + import 'file:///baz.json' assert { type: 'json' }; + import type {} from 'file:///bar.ts'; + /** @typedef { import('file:///bar.ts') } bar */ + " + .into(), + })) + }), + "file:///bar.ts" => { + assert!(!is_dynamic); + Box::pin(async move { + Ok(Some(LoadResponse::Module { + specifier: specifier.clone(), + maybe_headers: None, + content: "".into(), + })) + }) + } + "file:///baz.json" => { + assert!(!is_dynamic); + Box::pin(async move { + Ok(Some(LoadResponse::Module { + specifier: specifier.clone(), + maybe_headers: None, + content: "{}".into(), + })) + }) + } + _ => unreachable!(), + } + } + } + let mut graph = ModuleGraph::new(GraphKind::All); + graph + .build( + vec![Url::parse("file:///foo.ts").unwrap()], + &mut TestLoader, + Default::default(), + ) + .await; + graph.valid().unwrap(); + let module = graph.get(&Url::parse("file:///foo.ts").unwrap()).unwrap(); + let module = module.esm().unwrap(); + let dependency_a = module.dependencies.get("file:///bar.ts").unwrap(); + let dependency_b = module.dependencies.get("file:///baz.json").unwrap(); + assert_eq!( + dependency_a.imports, + vec![ + Import { + specifier: "file:///bar.ts".to_string(), + kind: ImportKind::TsReferencePath, + range: Range { + specifier: Url::parse("file:///foo.ts").unwrap(), + start: Position { + line: 1, + character: 36 + }, + end: Position { + line: 1, + character: 52, + }, + }, + is_dynamic: false, + assertions: ImportAssertions::None, + }, + Import { + specifier: "file:///bar.ts".to_string(), + kind: ImportKind::TsReferenceTypes, + range: Range { + specifier: Url::parse("file:///foo.ts").unwrap(), + start: Position { + line: 2, + character: 37, + }, + end: Position { + line: 2, + character: 53, + }, + }, + is_dynamic: false, + assertions: ImportAssertions::None, + }, + Import { + specifier: "file:///bar.ts".to_string(), + kind: ImportKind::Es, + range: Range { + specifier: Url::parse("file:///foo.ts").unwrap(), + start: Position { + line: 4, + character: 23, + }, + end: Position { + line: 4, + character: 39, + }, + }, + is_dynamic: false, + assertions: ImportAssertions::None, + }, + Import { + specifier: "file:///bar.ts".to_string(), + kind: ImportKind::Es, + range: Range { + specifier: Url::parse("file:///foo.ts").unwrap(), + start: Position { + line: 5, + character: 29, + }, + end: Position { + line: 5, + character: 45, + }, + }, + is_dynamic: true, + assertions: ImportAssertions::None, + }, + Import { + specifier: "file:///bar.ts".to_string(), + kind: ImportKind::Es, + range: Range { + specifier: Url::parse("file:///foo.ts").unwrap(), + start: Position { + line: 6, + character: 29, + }, + end: Position { + line: 6, + character: 45, + }, + }, + is_dynamic: true, + assertions: ImportAssertions::Unknown, + }, + Import { + specifier: "file:///bar.ts".to_string(), + kind: ImportKind::TsType, + range: Range { + specifier: Url::parse("file:///foo.ts").unwrap(), + start: Position { + line: 8, + character: 36, + }, + end: Position { + line: 8, + character: 52, + }, + }, + is_dynamic: false, + assertions: ImportAssertions::None, + }, + ] + ); + assert_eq!( + dependency_b.imports, + vec![Import { + specifier: "file:///baz.json".to_string(), + kind: ImportKind::Es, + range: Range { + specifier: Url::parse("file:///foo.ts").unwrap(), + start: Position { + line: 7, + character: 23, + }, + end: Position { + line: 7, + character: 41, + }, + }, + is_dynamic: false, + assertions: ImportAssertions::Known(HashMap::from_iter(vec![( + "type".to_string(), + ImportAssertion::Known("json".to_string()) + )])), + },] + ); + } }