mirror of
https://github.com/swc-project/swc.git
synced 2024-12-24 22:22:34 +03:00
Add dependency analyzer (#1010)
This commit is contained in:
parent
274b112ef4
commit
8ab466aafb
@ -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 }
|
||||
|
19
ecmascript/dep-graph/Cargo.toml
Normal file
19
ecmascript/dep-graph/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
|
||||
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" }
|
350
ecmascript/dep-graph/src/lib.rs
Normal file
350
ecmascript/dep-graph/src/lib.rs
Normal file
@ -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<DependencyDescriptor> {
|
||||
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<Comment>,
|
||||
/// 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<DependencyDescriptor>,
|
||||
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<Comment>) {
|
||||
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<SourceMap>, 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<StringInput<'_>> = 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";
|
||||
/// <reference foo="bar" />
|
||||
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#"/ <reference foo="bar" />"#.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")
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user