Skip to main content

morph_cli/core/ast/
semantic.rs

1use std::collections::{HashMap, HashSet};
2use swc_ecma_ast::*;
3use swc_ecma_visit::{Visit, VisitWith};
4
5#[derive(Debug, Default, Clone)]
6pub struct SemanticModel {
7    pub imports: HashMap<String, ImportInfo>,
8    pub exports: HashSet<String>,
9    pub declarations: HashSet<String>,
10    pub usages: HashSet<String>,
11    pub collisions: HashSet<String>,
12}
13
14#[derive(Debug, Clone)]
15pub struct ImportInfo {
16    pub local_name: String,
17    pub original_name: Option<String>,
18    pub source: String,
19    pub is_default: bool,
20}
21
22impl SemanticModel {
23    pub fn new(module: &Module) -> Self {
24        let mut analyzer = SemanticAnalyzer::default();
25        module.visit_with(&mut analyzer);
26        analyzer.model
27    }
28
29    pub fn get_unused_imports(&self) -> Vec<String> {
30        self.imports
31            .keys()
32            .filter(|name| !self.usages.contains(*name))
33            .cloned()
34            .collect()
35    }
36
37    pub fn has_collision(&self, name: &str) -> bool {
38        self.declarations.contains(name) || self.imports.contains_key(name)
39    }
40}
41
42#[derive(Default)]
43struct SemanticAnalyzer {
44    model: SemanticModel,
45    in_export: bool,
46}
47
48impl SemanticAnalyzer {
49    fn declare(&mut self, name: String) {
50        if !self.model.declarations.insert(name.clone()) {
51            self.model.collisions.insert(name);
52        }
53    }
54}
55
56impl Visit for SemanticAnalyzer {
57    fn visit_import_decl(&mut self, import: &ImportDecl) {
58        let source = import.src.value.to_string();
59        for specifier in &import.specifiers {
60            match specifier {
61                ImportSpecifier::Default(def) => {
62                    let name = def.local.sym.to_string();
63                    self.model.imports.insert(
64                        name.clone(),
65                        ImportInfo {
66                            local_name: name.clone(),
67                            original_name: None,
68                            source: source.clone(),
69                            is_default: true,
70                        },
71                    );
72                    self.declare(name);
73                }
74                ImportSpecifier::Named(named) => {
75                    let local = named.local.sym.to_string();
76                    let original = named.imported.as_ref().map(|ext| match ext {
77                        ModuleExportName::Ident(id) => id.sym.to_string(),
78                        ModuleExportName::Str(s) => s.value.to_string(),
79                    });
80                    self.model.imports.insert(
81                        local.clone(),
82                        ImportInfo {
83                            local_name: local.clone(),
84                            original_name: original,
85                            source: source.clone(),
86                            is_default: false,
87                        },
88                    );
89                    self.declare(local);
90                }
91                ImportSpecifier::Namespace(ns) => {
92                    let name = ns.local.sym.to_string();
93                    self.model.imports.insert(
94                        name.clone(),
95                        ImportInfo {
96                            local_name: name.clone(),
97                            original_name: None,
98                            source: source.clone(),
99                            is_default: false,
100                        },
101                    );
102                    self.declare(name);
103                }
104            }
105        }
106        // Do not visit children to avoid registering the import's local identifier as a usage
107    }
108
109    fn visit_export_decl(&mut self, export: &ExportDecl) {
110        self.in_export = true;
111        export.visit_children_with(self);
112        self.in_export = false;
113    }
114
115    fn visit_named_export(&mut self, export: &NamedExport) {
116        for spec in &export.specifiers {
117            if let ExportSpecifier::Named(named) = spec {
118                let name = match &named.orig {
119                    ModuleExportName::Ident(id) => id.sym.to_string(),
120                    ModuleExportName::Str(s) => s.value.to_string(),
121                };
122                self.model.exports.insert(name.clone());
123                self.model.usages.insert(name);
124            }
125        }
126        export.visit_children_with(self);
127    }
128
129    fn visit_var_declarator(&mut self, var: &VarDeclarator) {
130        if let Pat::Ident(ident) = &var.name {
131            let name = ident.id.sym.to_string();
132            self.declare(name.clone());
133            if self.in_export {
134                self.model.exports.insert(name);
135            }
136        }
137        // We must visit children (e.g. init expression)
138        var.visit_children_with(self);
139    }
140
141    fn visit_fn_decl(&mut self, func: &FnDecl) {
142        let name = func.ident.sym.to_string();
143        self.declare(name.clone());
144        if self.in_export {
145            self.model.exports.insert(name);
146        }
147        func.visit_children_with(self);
148    }
149
150    fn visit_class_decl(&mut self, class: &ClassDecl) {
151        let name = class.ident.sym.to_string();
152        self.declare(name.clone());
153        if self.in_export {
154            self.model.exports.insert(name);
155        }
156        class.visit_children_with(self);
157    }
158
159    fn visit_ident(&mut self, ident: &Ident) {
160        self.model.usages.insert(ident.sym.to_string());
161        ident.visit_children_with(self);
162    }
163}