morph-cli 0.1.0

AST-based codebase migration and codemod tool for JavaScript and TypeScript projects.
Documentation
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::core::ast::parser::parse_file;
use crate::utils::path::resolve_relative_import;
use swc_ecma_ast::*;

#[derive(Debug, Default, Clone)]
pub struct ImportGraph {
    /// Maps a file to the files it imports
    imports: HashMap<PathBuf, HashSet<PathBuf>>,
    /// Maps a file to the files that import it
    dependents: HashMap<PathBuf, HashSet<PathBuf>>,
    /// Maps a file to its exported names (basic tracking)
    exports: HashMap<PathBuf, HashSet<String>>,
}

impl ImportGraph {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn analyze_file(&mut self, path: &Path) -> anyhow::Result<()> {
        let parsed = parse_file(path)?;
        let mut local_imports = HashSet::new();
        let mut local_exports = HashSet::new();

        for item in &parsed.module.body {
            match item {
                ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
                    let src = import.src.value.to_string();
                    if src.starts_with('.') {
                        if let Some(resolved) = resolve_relative_import(path, &src) {
                            local_imports.insert(resolved);
                        }
                    }
                }
                ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
                    for spec in &export.specifiers {
                        match spec {
                            ExportSpecifier::Named(named) => {
                                let name = match &named.orig {
                                    ModuleExportName::Ident(id) => id.sym.to_string(),
                                    ModuleExportName::Str(s) => s.value.to_string(),
                                };
                                local_exports.insert(name);
                            }
                            _ => {}
                        }
                    }
                    if let Some(src) = &export.src {
                        let src_val = src.value.to_string();
                        if src_val.starts_with('.') {
                           if let Some(resolved) = resolve_relative_import(path, &src_val) {
                               local_imports.insert(resolved);
                           }
                        }
                    }
                }
                ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => {
                    match &export.decl {
                        Decl::Fn(f) => { local_exports.insert(f.ident.sym.to_string()); }
                        Decl::Class(c) => { local_exports.insert(c.ident.sym.to_string()); }
                        Decl::Var(v) => {
                            for decl in &v.decls {
                                if let Pat::Ident(id) = &decl.name {
                                    local_exports.insert(id.id.sym.to_string());
                                }
                            }
                        }
                        _ => {}
                    }
                }
                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export)) => {
                    local_exports.insert("default".to_string());
                    match &export.decl {
                        DefaultDecl::Fn(f) => {
                            if let Some(id) = &f.ident {
                                local_exports.insert(id.sym.to_string());
                            }
                        }
                        DefaultDecl::Class(c) => {
                            if let Some(id) = &c.ident {
                                local_exports.insert(id.sym.to_string());
                            }
                        }
                        _ => {}
                    }
                }
                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(_)) => {
                    local_exports.insert("default".to_string());
                }
                _ => {}
            }
        }

        let path_buf = path.to_path_buf();
        for imported in &local_imports {
            self.dependents.entry(imported.clone()).or_default().insert(path_buf.clone());
        }
        self.imports.insert(path_buf.clone(), local_imports);
        self.exports.insert(path_buf, local_exports);

        Ok(())
    }

    pub fn get_imports(&self, path: &Path) -> Option<&HashSet<PathBuf>> {
        self.imports.get(path)
    }

    pub fn get_dependents(&self, path: &Path) -> Option<&HashSet<PathBuf>> {
        self.dependents.get(path)
    }

    pub fn get_exports(&self, path: &Path) -> Option<&HashSet<String>> {
        self.exports.get(path)
    }
}