From 8ab466aafb43363c1b0eb5d437a0aa6ef037e6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sat, 29 Aug 2020 13:07:34 +0200 Subject: [PATCH] Add dependency analyzer (#1010) --- ecmascript/Cargo.toml | 2 + ecmascript/dep-graph/Cargo.toml | 19 ++ ecmascript/dep-graph/src/lib.rs | 350 ++++++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 ecmascript/dep-graph/Cargo.toml create mode 100644 ecmascript/dep-graph/src/lib.rs diff --git a/ecmascript/Cargo.toml b/ecmascript/Cargo.toml index 4b36644c8be..011d5faa0dd 100644 --- a/ecmascript/Cargo.toml +++ b/ecmascript/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" [features] codegen = ["swc_ecma_codegen"] +dep_graph = ["swc_ecma_dep_graph"] parser = ["swc_ecma_parser"] utils = ["swc_ecma_utils"] transforms = ["swc_ecma_transforms"] @@ -21,6 +22,7 @@ const-modules = ["swc_ecma_transforms", "swc_ecma_transforms/const-modules"] [dependencies] swc_ecma_ast = { version = "0.30.0", path ="./ast" } swc_ecma_codegen = { version = "0.34.0", path ="./codegen", optional = true } +swc_ecma_dep_graph = { version = "0.1.0", path = "./dep-graph", optional = true } swc_ecma_parser = { version = "0.36.0", path ="./parser", optional = true } swc_ecma_utils = { version = "0.20.0", path ="./utils", optional = true } swc_ecma_transforms = { version = "0.22.0", path ="./transforms", optional = true } diff --git a/ecmascript/dep-graph/Cargo.toml b/ecmascript/dep-graph/Cargo.toml new file mode 100644 index 00000000000..1b08869ce43 --- /dev/null +++ b/ecmascript/dep-graph/Cargo.toml @@ -0,0 +1,19 @@ +[package] +authors = ["강동윤 "] +description = "Dependency graph for the ecmascript" +documentation = "https://swc-project.github.io/rustdoc/swc_ecma_dep_graph/" +edition = "2018" +license = "Apache-2.0/MIT" +name = "swc_ecma_dep_graph" +repository = "https://github.com/swc-project/swc.git" +version = "0.1.0" + +[dependencies] +swc_atoms = {version = "0.2", path = "../../atoms"} +swc_common = {version = "0.10.1", path = "../../common"} +swc_ecma_ast = {version = "0.30.0", path = "../ast"} +swc_ecma_visit = {version = "0.16.0", path = "../visit"} + +[dev-dependencies] +swc_ecma_parser = {version = "0.36.2", path = "../parser"} +testing = { version = "0.10.0", path ="../../testing" } \ No newline at end of file diff --git a/ecmascript/dep-graph/src/lib.rs b/ecmascript/dep-graph/src/lib.rs new file mode 100644 index 00000000000..d326d3df826 --- /dev/null +++ b/ecmascript/dep-graph/src/lib.rs @@ -0,0 +1,350 @@ +use swc_atoms::JsWord; +use swc_common::{ + comments::{Comment, SingleThreadedComments}, + Loc, SourceMap, Span, DUMMY_SP, +}; +use swc_ecma_ast as ast; +use swc_ecma_visit::{self, Node, Visit, VisitWith}; + +pub fn analyze_dependencies( + module: &ast::Module, + source_map: &SourceMap, + comments: &SingleThreadedComments, +) -> Vec { + let mut v = DependencyCollector { + comments, + source_map, + items: vec![], + is_dynamic: false, + }; + module.visit_with(&ast::Invalid { span: DUMMY_SP }, &mut v); + v.items +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DependencyKind { + Import, + ImportType, + Export, + ExportType, + Require, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DependencyDescriptor { + pub kind: DependencyKind, + /// A flag indicating if the import is dynamic or not. + pub is_dynamic: bool, + /// Any leading comments associated with the dependency. This is used for + /// further processing of supported pragma that impact the dependency. + pub leading_comments: Vec, + /// The location of the import/export statement. + pub col: usize, + pub line: usize, + /// The text specifier associated with the import/export statement. + pub specifier: JsWord, +} + +struct DependencyCollector<'a> { + comments: &'a SingleThreadedComments, + pub items: Vec, + source_map: &'a SourceMap, + // This field is used to determine if currently visited "require" + // is top level and "static", or inside module body and "dynamic". + is_dynamic: bool, +} + +impl<'a> DependencyCollector<'a> { + fn get_location_and_comments(&self, span: Span) -> (Loc, Vec) { + let location = self.source_map.lookup_char_pos(span.lo); + let leading_comments = self + .comments + .with_leading(span.lo, |comments| comments.to_vec()); + (location, leading_comments) + } +} + +impl<'a> Visit for DependencyCollector<'a> { + fn visit_import_decl(&mut self, node: &ast::ImportDecl, _parent: &dyn Node) { + let specifier = node.src.value.clone(); + let span = node.span; + let (location, leading_comments) = self.get_location_and_comments(span); + let kind = if node.type_only { + DependencyKind::ImportType + } else { + DependencyKind::Import + }; + self.items.push(DependencyDescriptor { + kind, + is_dynamic: false, + leading_comments, + col: location.col_display, + line: location.line, + specifier, + }); + } + + fn visit_named_export(&mut self, node: &ast::NamedExport, _parent: &dyn Node) { + if let Some(src) = &node.src { + let specifier = src.value.clone(); + let span = node.span; + let (location, leading_comments) = self.get_location_and_comments(span); + let kind = if node.type_only { + DependencyKind::ExportType + } else { + DependencyKind::Export + }; + self.items.push(DependencyDescriptor { + kind, + is_dynamic: false, + leading_comments, + col: location.col_display, + line: location.line, + specifier, + }); + } + } + + fn visit_export_all(&mut self, node: &ast::ExportAll, _parent: &dyn Node) { + let specifier = node.src.value.clone(); + let span = node.span; + let (location, leading_comments) = self.get_location_and_comments(span); + self.items.push(DependencyDescriptor { + kind: DependencyKind::Export, + is_dynamic: false, + leading_comments, + col: location.col_display, + line: location.line, + specifier, + }); + } + + fn visit_ts_import_type(&mut self, node: &ast::TsImportType, _parent: &dyn Node) { + let specifier = node.arg.value.clone(); + let span = node.span; + let (location, leading_comments) = self.get_location_and_comments(span); + self.items.push(DependencyDescriptor { + kind: DependencyKind::ImportType, + is_dynamic: false, + leading_comments, + col: location.col_display, + line: location.line, + specifier, + }); + } + + fn visit_module_items(&mut self, items: &[ast::ModuleItem], _parent: &dyn Node) { + swc_ecma_visit::visit_module_items(self, items, _parent); + } + + fn visit_stmts(&mut self, items: &[ast::Stmt], _parent: &dyn Node) { + self.is_dynamic = true; + swc_ecma_visit::visit_stmts(self, items, _parent); + self.is_dynamic = false; + } + + fn visit_call_expr(&mut self, node: &ast::CallExpr, _parent: &dyn Node) { + use ast::{Expr::*, ExprOrSuper::*}; + + swc_ecma_visit::visit_call_expr(self, node, _parent); + let call_expr = match node.callee.clone() { + Super(_) => return, + Expr(boxed) => boxed, + }; + + let kind = match &*call_expr { + Ident(ident) => match ident.sym.to_string().as_str() { + "import" => DependencyKind::Import, + "require" => DependencyKind::Require, + _ => return, + }, + _ => return, + }; + + if let Some(arg) = node.args.get(0) { + if let Lit(lit) = &*arg.expr { + if let ast::Lit::Str(str_) = lit { + let specifier = str_.value.clone(); + let span = node.span; + let (location, leading_comments) = self.get_location_and_comments(span); + self.items.push(DependencyDescriptor { + kind, + is_dynamic: self.is_dynamic, + leading_comments, + col: location.col_display, + line: location.line, + specifier, + }); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use swc_common::{ + comments::{Comment, CommentKind}, + sync::Lrc, + BytePos, FileName, Span, SyntaxContext, + }; + use swc_ecma_parser::{lexer::Lexer, JscTarget, Parser, StringInput, Syntax, TsConfig}; + + fn helper( + file_name: &str, + source: &str, + ) -> Result<(ast::Module, Lrc, SingleThreadedComments), testing::StdErr> { + let output = ::testing::run_test(true, |cm, handler| { + let fm = + cm.new_source_file(FileName::Custom(file_name.to_string()), source.to_string()); + + let comments = SingleThreadedComments::default(); + let lexer: Lexer> = Lexer::new( + Syntax::Typescript(TsConfig { + dts: file_name.ends_with(".d.ts"), + tsx: file_name.contains("tsx"), + dynamic_import: true, + decorators: true, + no_early_errors: true, + ..Default::default() + }), + JscTarget::Es2015, + (&*fm).into(), + Some(&comments), + ); + + let mut p = Parser::new_from(lexer); + + let res = p + .parse_module() + .map_err(|e| e.into_diagnostic(&handler).emit()); + + for err in p.take_errors() { + err.into_diagnostic(&handler).emit(); + } + + if handler.has_errors() { + return Err(()); + } + + Ok((res.unwrap(), cm, comments)) + }); + + output + } + + #[test] + fn test_parsed_module_get_dependencies() { + let source = r#"import * as bar from "./test.ts"; +/** JSDoc */ +import type { Foo } from "./foo.d.ts"; +/// +export * as Buzz from "./buzz.ts"; +// @some-pragma +/** + * Foo + */ +export type { Fizz } from "./fizz.d.ts"; +const { join } = require("path"); + +// dynamic + +try { + const foo = await import("./foo.ts"); +} catch (e) { + // pass +} + +try { + const foo = require("some_package"); +} catch (e) { + // pass +} + "#; + let (module, source_map, comments) = helper("test.ts", &source).unwrap(); + // eprintln!("module {:#?}", module); + let dependencies = analyze_dependencies(&module, &source_map, &comments); + assert_eq!(dependencies.len(), 7); + assert_eq!( + dependencies, + vec![ + DependencyDescriptor { + kind: DependencyKind::Import, + is_dynamic: false, + leading_comments: Vec::new(), + col: 0, + line: 1, + specifier: JsWord::from("./test.ts") + }, + DependencyDescriptor { + kind: DependencyKind::ImportType, + is_dynamic: false, + leading_comments: vec![Comment { + kind: CommentKind::Block, + text: r#"* JSDoc "#.to_string(), + span: Span::new(BytePos(34), BytePos(46), SyntaxContext::empty()), + }], + col: 0, + line: 3, + specifier: JsWord::from("./foo.d.ts") + }, + DependencyDescriptor { + kind: DependencyKind::Export, + is_dynamic: false, + leading_comments: vec![Comment { + kind: CommentKind::Line, + text: r#"/ "#.to_string(), + span: Span::new(BytePos(86), BytePos(113), SyntaxContext::empty()), + }], + col: 0, + line: 5, + specifier: JsWord::from("./buzz.ts") + }, + DependencyDescriptor { + kind: DependencyKind::ExportType, + is_dynamic: false, + leading_comments: vec![ + Comment { + kind: CommentKind::Line, + text: r#" @some-pragma"#.to_string(), + span: Span::new(BytePos(149), BytePos(164), SyntaxContext::empty()), + }, + Comment { + kind: CommentKind::Block, + text: "*\n * Foo\n ".to_string(), + span: Span::new(BytePos(165), BytePos(179), SyntaxContext::empty()), + } + ], + col: 0, + line: 10, + specifier: JsWord::from("./fizz.d.ts") + }, + DependencyDescriptor { + kind: DependencyKind::Require, + is_dynamic: false, + leading_comments: Vec::new(), + col: 17, + line: 11, + specifier: JsWord::from("path") + }, + DependencyDescriptor { + kind: DependencyKind::Import, + is_dynamic: true, + leading_comments: Vec::new(), + col: 22, + line: 16, + specifier: JsWord::from("./foo.ts") + }, + DependencyDescriptor { + kind: DependencyKind::Require, + is_dynamic: true, + leading_comments: Vec::new(), + col: 16, + line: 22, + specifier: JsWord::from("some_package") + } + ] + ); + } +}