Skip to main content

presolve_compiler/
symbol_table.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3
4use crate::compilation_unit::CompilationUnit;
5use crate::component_graph::build_component_graph_for_module;
6use crate::semantic_id::{SemanticId, SemanticOwner};
7use crate::semantic_provenance::SourceProvenance;
8
9/// Local declarations indexed by source module before import resolution.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct SymbolTable {
12    pub modules: Vec<ModuleSymbolTable>,
13    pub diagnostics: Vec<SymbolDiagnostic>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ModuleSymbolTable {
18    pub path: std::path::PathBuf,
19    pub symbols: BTreeMap<String, ModuleSymbol>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ModuleSymbol {
24    pub name: String,
25    pub kind: SymbolKind,
26    pub id: SemanticId,
27    pub owner: SemanticOwner,
28    pub provenance: SourceProvenance,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum SymbolKind {
33    Component,
34    StateField,
35    Method,
36    TypeAlias,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct SymbolDiagnostic {
41    pub code: String,
42    pub module: std::path::PathBuf,
43    pub name: String,
44    pub message: String,
45}
46
47impl SymbolTable {
48    #[must_use]
49    pub fn module(&self, path: impl AsRef<Path>) -> Option<&ModuleSymbolTable> {
50        self.modules
51            .iter()
52            .find(|module| module.path == path.as_ref())
53    }
54
55    #[must_use]
56    pub fn resolve(&self, module_path: impl AsRef<Path>, name: &str) -> Option<&ModuleSymbol> {
57        self.module(module_path)
58            .and_then(|module| module.symbols.get(name))
59    }
60}
61
62#[must_use]
63pub fn build_symbol_table(unit: &CompilationUnit) -> SymbolTable {
64    let mut modules = Vec::new();
65    let mut diagnostics = Vec::new();
66
67    for file in unit.files() {
68        let graph = build_component_graph_for_module(file);
69        let mut symbols = BTreeMap::new();
70
71        for alias in &file.type_aliases {
72            let id = SemanticId::type_alias_in_module(&file.path, &alias.name);
73            insert_symbol(
74                &mut symbols,
75                &mut diagnostics,
76                &file.path,
77                ModuleSymbol {
78                    name: alias.name.clone(),
79                    kind: SymbolKind::TypeAlias,
80                    id,
81                    owner: SemanticOwner::Application,
82                    provenance: SourceProvenance::new(&file.path, alias.type_span),
83                },
84            );
85        }
86
87        for component in graph.components {
88            insert_symbol(
89                &mut symbols,
90                &mut diagnostics,
91                &file.path,
92                ModuleSymbol {
93                    name: component.class_name.clone(),
94                    kind: SymbolKind::Component,
95                    id: component.id.clone(),
96                    owner: component.owner.clone(),
97                    provenance: graph.provenance[&component.id].clone(),
98                },
99            );
100
101            for field in component.state_fields {
102                let id = field.id;
103                let provenance = graph.provenance[&id].clone();
104                insert_symbol(
105                    &mut symbols,
106                    &mut diagnostics,
107                    &file.path,
108                    ModuleSymbol {
109                        name: format!("{}.{}", component.class_name, field.name),
110                        kind: SymbolKind::StateField,
111                        id,
112                        owner: field.owner,
113                        provenance,
114                    },
115                );
116            }
117
118            for method in component.methods {
119                let id = method.id;
120                let provenance = graph.provenance[&id].clone();
121                insert_symbol(
122                    &mut symbols,
123                    &mut diagnostics,
124                    &file.path,
125                    ModuleSymbol {
126                        name: format!("{}.{}", component.class_name, method.name),
127                        kind: SymbolKind::Method,
128                        id,
129                        owner: method.owner,
130                        provenance,
131                    },
132                );
133            }
134        }
135
136        modules.push(ModuleSymbolTable {
137            path: file.path.clone(),
138            symbols,
139        });
140    }
141
142    SymbolTable {
143        modules,
144        diagnostics,
145    }
146}
147
148fn insert_symbol(
149    symbols: &mut BTreeMap<String, ModuleSymbol>,
150    diagnostics: &mut Vec<SymbolDiagnostic>,
151    module: &Path,
152    symbol: ModuleSymbol,
153) {
154    if symbols.contains_key(&symbol.name) {
155        diagnostics.push(SymbolDiagnostic {
156            code: "PSSYM1001".to_string(),
157            module: module.to_path_buf(),
158            name: symbol.name.clone(),
159            message: format!("duplicate local symbol `{}`", symbol.name),
160        });
161        return;
162    }
163
164    symbols.insert(symbol.name.clone(), symbol);
165}
166
167#[cfg(test)]
168mod tests {
169    use super::{build_symbol_table, SymbolKind};
170    use crate::{CompilationUnit, SemanticId, SemanticOwner};
171
172    #[test]
173    fn indexes_local_component_members_by_module() {
174        let unit = CompilationUnit::parse_sources([
175            (
176                "src/Counter.tsx",
177                r#"
178import { shared } from "./shared";
179
180@component("x-counter")
181class Counter extends Component {
182  count = state(0);
183
184  increment() {
185    this.count++;
186  }
187
188  render() {
189    return <button>{this.count}</button>;
190  }
191}
192"#,
193            ),
194            (
195                "src/Status.tsx",
196                r#"
197@component("x-status")
198class Status extends Component {
199  render() {
200    return <div>Status</div>;
201  }
202}
203"#,
204            ),
205        ]);
206
207        let symbols = build_symbol_table(&unit);
208        let counter = symbols.module("src/Counter.tsx").expect("counter module");
209
210        assert_eq!(symbols.modules.len(), 2);
211        assert!(symbols.diagnostics.is_empty());
212        assert_eq!(counter.symbols.len(), 4);
213        assert_eq!(counter.symbols["Counter"].kind, SymbolKind::Component);
214        assert_eq!(
215            counter.symbols["Counter"].id,
216            SemanticId::component_in_module("src/Counter.tsx", Some("x-counter"), "Counter")
217        );
218        assert_eq!(
219            counter.symbols["Counter.count"].kind,
220            SymbolKind::StateField
221        );
222        assert_eq!(
223            counter.symbols["Counter.count"].owner,
224            SemanticOwner::entity(SemanticId::component_in_module(
225                "src/Counter.tsx",
226                Some("x-counter"),
227                "Counter"
228            ))
229        );
230        assert_eq!(
231            counter.symbols["Counter.increment"].kind,
232            SymbolKind::Method
233        );
234        assert_eq!(counter.symbols["Counter.increment"].provenance.span.line, 8);
235        assert!(symbols.resolve("src/Counter.tsx", "shared").is_none());
236        assert!(symbols.resolve("src/Status.tsx", "Status.render").is_some());
237    }
238
239    #[test]
240    fn reports_duplicate_local_member_names() {
241        let unit = CompilationUnit::parse_sources([(
242            "src/Duplicate.tsx",
243            r#"
244@component("x-duplicate")
245class Duplicate extends Component {
246  first() {}
247  first() {}
248
249  render() {
250    return <div>Duplicate</div>;
251  }
252}
253"#,
254        )]);
255
256        let symbols = build_symbol_table(&unit);
257
258        assert_eq!(symbols.diagnostics.len(), 1);
259        assert_eq!(symbols.diagnostics[0].code, "PSSYM1001");
260        assert_eq!(symbols.diagnostics[0].name, "Duplicate.first");
261    }
262}