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#[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}