use std::collections::{HashMap, HashSet};
use swc_ecma_ast::*;
use swc_ecma_visit::{Visit, VisitWith};
#[derive(Debug, Default, Clone)]
pub struct SemanticModel {
pub imports: HashMap<String, ImportInfo>,
pub exports: HashSet<String>,
pub declarations: HashSet<String>,
pub usages: HashSet<String>,
pub collisions: HashSet<String>,
}
#[derive(Debug, Clone)]
pub struct ImportInfo {
pub local_name: String,
pub original_name: Option<String>,
pub source: String,
pub is_default: bool,
}
impl SemanticModel {
pub fn new(module: &Module) -> Self {
let mut analyzer = SemanticAnalyzer::default();
module.visit_with(&mut analyzer);
analyzer.model
}
pub fn get_unused_imports(&self) -> Vec<String> {
self.imports
.keys()
.filter(|name| !self.usages.contains(*name))
.cloned()
.collect()
}
pub fn has_collision(&self, name: &str) -> bool {
self.declarations.contains(name) || self.imports.contains_key(name)
}
}
#[derive(Default)]
struct SemanticAnalyzer {
model: SemanticModel,
in_export: bool,
}
impl SemanticAnalyzer {
fn declare(&mut self, name: String) {
if !self.model.declarations.insert(name.clone()) {
self.model.collisions.insert(name);
}
}
}
impl Visit for SemanticAnalyzer {
fn visit_import_decl(&mut self, import: &ImportDecl) {
let source = import.src.value.to_string();
for specifier in &import.specifiers {
match specifier {
ImportSpecifier::Default(def) => {
let name = def.local.sym.to_string();
self.model.imports.insert(
name.clone(),
ImportInfo {
local_name: name.clone(),
original_name: None,
source: source.clone(),
is_default: true,
},
);
self.declare(name);
}
ImportSpecifier::Named(named) => {
let local = named.local.sym.to_string();
let original = named.imported.as_ref().map(|ext| match ext {
ModuleExportName::Ident(id) => id.sym.to_string(),
ModuleExportName::Str(s) => s.value.to_string(),
});
self.model.imports.insert(
local.clone(),
ImportInfo {
local_name: local.clone(),
original_name: original,
source: source.clone(),
is_default: false,
},
);
self.declare(local);
}
ImportSpecifier::Namespace(ns) => {
let name = ns.local.sym.to_string();
self.model.imports.insert(
name.clone(),
ImportInfo {
local_name: name.clone(),
original_name: None,
source: source.clone(),
is_default: false,
},
);
self.declare(name);
}
}
}
}
fn visit_export_decl(&mut self, export: &ExportDecl) {
self.in_export = true;
export.visit_children_with(self);
self.in_export = false;
}
fn visit_named_export(&mut self, export: &NamedExport) {
for spec in &export.specifiers {
if let ExportSpecifier::Named(named) = spec {
let name = match &named.orig {
ModuleExportName::Ident(id) => id.sym.to_string(),
ModuleExportName::Str(s) => s.value.to_string(),
};
self.model.exports.insert(name.clone());
self.model.usages.insert(name);
}
}
export.visit_children_with(self);
}
fn visit_var_declarator(&mut self, var: &VarDeclarator) {
if let Pat::Ident(ident) = &var.name {
let name = ident.id.sym.to_string();
self.declare(name.clone());
if self.in_export {
self.model.exports.insert(name);
}
}
var.visit_children_with(self);
}
fn visit_fn_decl(&mut self, func: &FnDecl) {
let name = func.ident.sym.to_string();
self.declare(name.clone());
if self.in_export {
self.model.exports.insert(name);
}
func.visit_children_with(self);
}
fn visit_class_decl(&mut self, class: &ClassDecl) {
let name = class.ident.sym.to_string();
self.declare(name.clone());
if self.in_export {
self.model.exports.insert(name);
}
class.visit_children_with(self);
}
fn visit_ident(&mut self, ident: &Ident) {
self.model.usages.insert(ident.sym.to_string());
ident.visit_children_with(self);
}
}