Skip to main content

morph_cli/core/ast/
graph.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use crate::core::ast::parser::parse_file;
4use crate::utils::path::resolve_relative_import;
5use swc_ecma_ast::*;
6
7#[derive(Debug, Default, Clone)]
8pub struct ImportGraph {
9    /// Maps a file to the files it imports
10    imports: HashMap<PathBuf, HashSet<PathBuf>>,
11    /// Maps a file to the files that import it
12    dependents: HashMap<PathBuf, HashSet<PathBuf>>,
13    /// Maps a file to its exported names (basic tracking)
14    exports: HashMap<PathBuf, HashSet<String>>,
15}
16
17impl ImportGraph {
18    pub fn new() -> Self {
19        Self::default()
20    }
21
22    pub fn analyze_file(&mut self, path: &Path) -> anyhow::Result<()> {
23        let parsed = parse_file(path)?;
24        let mut local_imports = HashSet::new();
25        let mut local_exports = HashSet::new();
26
27        for item in &parsed.module.body {
28            match item {
29                ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
30                    let src = import.src.value.to_string();
31                    if src.starts_with('.') {
32                        if let Some(resolved) = resolve_relative_import(path, &src) {
33                            local_imports.insert(resolved);
34                        }
35                    }
36                }
37                ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
38                    for spec in &export.specifiers {
39                        match spec {
40                            ExportSpecifier::Named(named) => {
41                                let name = match &named.orig {
42                                    ModuleExportName::Ident(id) => id.sym.to_string(),
43                                    ModuleExportName::Str(s) => s.value.to_string(),
44                                };
45                                local_exports.insert(name);
46                            }
47                            _ => {}
48                        }
49                    }
50                    if let Some(src) = &export.src {
51                        let src_val = src.value.to_string();
52                        if src_val.starts_with('.') {
53                           if let Some(resolved) = resolve_relative_import(path, &src_val) {
54                               local_imports.insert(resolved);
55                           }
56                        }
57                    }
58                }
59                ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => {
60                    match &export.decl {
61                        Decl::Fn(f) => { local_exports.insert(f.ident.sym.to_string()); }
62                        Decl::Class(c) => { local_exports.insert(c.ident.sym.to_string()); }
63                        Decl::Var(v) => {
64                            for decl in &v.decls {
65                                if let Pat::Ident(id) = &decl.name {
66                                    local_exports.insert(id.id.sym.to_string());
67                                }
68                            }
69                        }
70                        _ => {}
71                    }
72                }
73                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export)) => {
74                    local_exports.insert("default".to_string());
75                    match &export.decl {
76                        DefaultDecl::Fn(f) => {
77                            if let Some(id) = &f.ident {
78                                local_exports.insert(id.sym.to_string());
79                            }
80                        }
81                        DefaultDecl::Class(c) => {
82                            if let Some(id) = &c.ident {
83                                local_exports.insert(id.sym.to_string());
84                            }
85                        }
86                        _ => {}
87                    }
88                }
89                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(_)) => {
90                    local_exports.insert("default".to_string());
91                }
92                _ => {}
93            }
94        }
95
96        let path_buf = path.to_path_buf();
97        for imported in &local_imports {
98            self.dependents.entry(imported.clone()).or_default().insert(path_buf.clone());
99        }
100        self.imports.insert(path_buf.clone(), local_imports);
101        self.exports.insert(path_buf, local_exports);
102
103        Ok(())
104    }
105
106    pub fn get_imports(&self, path: &Path) -> Option<&HashSet<PathBuf>> {
107        self.imports.get(path)
108    }
109
110    pub fn get_dependents(&self, path: &Path) -> Option<&HashSet<PathBuf>> {
111        self.dependents.get(path)
112    }
113
114    pub fn get_exports(&self, path: &Path) -> Option<&HashSet<String>> {
115        self.exports.get(path)
116    }
117}