morph-cli 0.1.0

AST-based codebase migration and codemod tool for JavaScript and TypeScript projects.
Documentation
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);
                }
            }
        }
        // Do not visit children to avoid registering the import's local identifier as a usage
    }

    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);
            }
        }
        // We must visit children (e.g. init expression)
        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);
    }
}