Skip to content

Commit

Permalink
feat: add SWC dependency analyzer (#5015)
Browse files Browse the repository at this point in the history
This commit adds "analyze_dependencies" function that uses SWC
(by the means of AstParser) to perform analysis of static and dynamic
imports.
  • Loading branch information
bartlomieju authored Apr 30, 2020
1 parent 898773d commit f79cb08
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 0 deletions.
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ walkdir = "2.3.1"
warp = "0.2.2"
semver-parser = "0.9.0"
uuid = { version = "0.8.1", features = ["v4"] }
swc_ecma_visit = { version = "=0.1.0" }

[target.'cfg(windows)'.dependencies]
winapi = "0.3.8"
Expand Down
155 changes: 155 additions & 0 deletions cli/swc_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use crate::swc_ecma_parser::Session;
use crate::swc_ecma_parser::SourceFileInput;
use crate::swc_ecma_parser::Syntax;
use crate::swc_ecma_parser::TsConfig;
use swc_ecma_visit::Node;
use swc_ecma_visit::Visit;

use std::error::Error;
use std::fmt;
Expand Down Expand Up @@ -162,3 +164,156 @@ impl AstParser {
.unwrap_or_else(|| vec![])
}
}

struct DependencyVisitor {
dependencies: Vec<String>,
analyze_dynamic_imports: bool,
}

impl Visit for DependencyVisitor {
fn visit_import_decl(
&mut self,
import_decl: &swc_ecma_ast::ImportDecl,
_parent: &dyn Node,
) {
let src_str = import_decl.src.value.to_string();
self.dependencies.push(src_str);
}

fn visit_named_export(
&mut self,
named_export: &swc_ecma_ast::NamedExport,
_parent: &dyn Node,
) {
if let Some(src) = &named_export.src {
let src_str = src.value.to_string();
self.dependencies.push(src_str);
}
}

fn visit_export_all(
&mut self,
export_all: &swc_ecma_ast::ExportAll,
_parent: &dyn Node,
) {
let src_str = export_all.src.value.to_string();
self.dependencies.push(src_str);
}

fn visit_call_expr(
&mut self,
call_expr: &swc_ecma_ast::CallExpr,
_parent: &dyn Node,
) {
if !self.analyze_dynamic_imports {
return;
}

use swc_ecma_ast::Expr::*;
use swc_ecma_ast::ExprOrSuper::*;

let boxed_expr = match call_expr.callee.clone() {
Super(_) => return,
Expr(boxed) => boxed,
};

match &*boxed_expr {
Ident(ident) => {
if &ident.sym.to_string() != "import" {
return;
}
}
_ => return,
};

if let Some(arg) = call_expr.args.get(0) {
match &*arg.expr {
Lit(lit) => {
if let swc_ecma_ast::Lit::Str(str_) = lit {
let src_str = str_.value.to_string();
self.dependencies.push(src_str);
}
}
_ => return,
}
}
}
}

/// Given file name and source code return vector
/// of unresolved import specifiers.
///
/// Returned vector may contain duplicate entries.
///
/// Second argument allows to configure if dynamic
/// imports should be analyzed.
///
/// NOTE: Only statically analyzable dynamic imports
/// are considered; ie. the ones that have plain string specifier:
///
/// await import("./fizz.ts")
///
/// These imports will be ignored:
///
/// await import(`./${dir}/fizz.ts`)
/// await import("./" + "fizz.ts")
#[allow(unused)]
pub fn analyze_dependencies(
source_code: &str,
analyze_dynamic_imports: bool,
) -> Result<Vec<String>, SwcDiagnosticBuffer> {
let parser = AstParser::new();
parser.parse_module("root.ts", source_code, |parse_result| {
let module = parse_result?;
let mut collector = DependencyVisitor {
dependencies: vec![],
analyze_dynamic_imports,
};
collector.visit_module(&module, &module);
Ok(collector.dependencies)
})
}

#[test]
fn test_analyze_dependencies() {
let source = r#"
import { foo } from "./foo.ts";
export { bar } from "./foo.ts";
export * from "./bar.ts";
"#;

let dependencies =
analyze_dependencies(source, false).expect("Failed to parse");
assert_eq!(
dependencies,
vec![
"./foo.ts".to_string(),
"./foo.ts".to_string(),
"./bar.ts".to_string(),
]
);
}

#[test]
fn test_analyze_dependencies_dyn_imports() {
let source = r#"
import { foo } from "./foo.ts";
export { bar } from "./foo.ts";
export * from "./bar.ts";
const a = await import("./fizz.ts");
const a = await import("./" + "buzz.ts");
"#;

let dependencies =
analyze_dependencies(source, true).expect("Failed to parse");
assert_eq!(
dependencies,
vec![
"./foo.ts".to_string(),
"./foo.ts".to_string(),
"./bar.ts".to_string(),
"./fizz.ts".to_string(),
]
);
}

0 comments on commit f79cb08

Please sign in to comment.