dbml_language_server/analysis/
semantic.rs

1// src/analysis/semantic.rs
2use crate::ast::*;
3use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
7#[allow(dead_code)]
8pub struct SymbolInfo {
9    pub name: String,
10    pub kind: SymbolKind,
11    pub span: Span,
12}
13
14#[derive(Debug, Clone, PartialEq)]
15#[allow(dead_code)]
16pub enum SymbolKind {
17    Table,
18    Column,
19    Enum,
20    EnumMember,
21}
22
23#[derive(Debug, Clone)]
24pub struct SemanticError {
25    pub message: String,
26    pub span: Span,
27}
28
29#[derive(Debug)]
30pub struct SemanticModel {
31    pub tables: HashMap<String, SymbolInfo>,
32    pub columns: HashMap<String, Vec<SymbolInfo>>, // table_name -> columns
33    pub enums: HashMap<String, SymbolInfo>,
34    pub errors: Vec<SemanticError>,
35}
36
37impl SemanticModel {
38    pub fn new() -> Self {
39        Self {
40            tables: HashMap::new(),
41            columns: HashMap::new(),
42            enums: HashMap::new(),
43            errors: Vec::new(),
44        }
45    }
46
47    pub fn analyze(document: &Document) -> Self {
48        let mut model = Self::new();
49
50        // Pass 1: Collect all definitions
51        for item in &document.items {
52            match item {
53                DocumentItem::Table(table) => {
54                    model.analyze_table(table);
55                }
56                DocumentItem::Enum(enum_def) => {
57                    model.analyze_enum(enum_def);
58                }
59                DocumentItem::Ref(ref_def) => {
60                    model.validate_ref(ref_def);
61                }
62                DocumentItem::Project(_) => {}
63            }
64        }
65
66        model
67    }
68
69    fn analyze_table(&mut self, table: &Table) {
70        let table_name = table.name.name.clone();
71
72        // Check for duplicate table
73        if self.tables.contains_key(&table_name) {
74            self.errors.push(SemanticError {
75                message: format!("Duplicate table definition: '{}'", table_name),
76                span: table.name.span.clone(),
77            });
78        } else {
79            self.tables.insert(
80                table_name.clone(),
81                SymbolInfo {
82                    name: table_name.clone(),
83                    kind: SymbolKind::Table,
84                    span: table.name.span.clone(),
85                },
86            );
87        }
88
89        // Analyze columns
90        let mut column_names = Vec::new();
91        for item in &table.items {
92            if let TableItem::Column(col) = item {
93                let col_name = col.name.name.clone();
94
95                // Check for duplicate column
96                if column_names.iter().any(|c: &SymbolInfo| c.name == col_name) {
97                    self.errors.push(SemanticError {
98                        message: format!(
99                            "Duplicate column '{}' in table '{}'",
100                            col_name, table_name
101                        ),
102                        span: col.name.span.clone(),
103                    });
104                } else {
105                    column_names.push(SymbolInfo {
106                        name: col_name.clone(),
107                        kind: SymbolKind::Column,
108                        span: col.name.span.clone(),
109                    });
110                }
111
112                // Validate column settings
113                for setting in &col.settings {
114                    if let ColumnSetting::Ref(inline_ref) = setting {
115                        self.validate_inline_ref(inline_ref, &table_name);
116                    }
117                }
118            }
119        }
120
121        self.columns.insert(table_name, column_names);
122    }
123
124    fn analyze_enum(&mut self, enum_def: &Enum) {
125        let enum_name = enum_def.name.name.clone();
126
127        if self.enums.contains_key(&enum_name) {
128            self.errors.push(SemanticError {
129                message: format!("Duplicate enum definition: '{}'", enum_name),
130                span: enum_def.name.span.clone(),
131            });
132        } else {
133            self.enums.insert(
134                enum_name.clone(),
135                SymbolInfo {
136                    name: enum_name,
137                    kind: SymbolKind::Enum,
138                    span: enum_def.name.span.clone(),
139                },
140            );
141        }
142    }
143
144    fn validate_ref(&mut self, ref_def: &Ref) {
145        let from_table = &ref_def.from_table.name;
146        let to_table = &ref_def.to_table.name;
147
148        // Check if tables exist
149        if !self.tables.contains_key(from_table) {
150            self.errors.push(SemanticError {
151                message: format!("Table '{}' not found", from_table),
152                span: ref_def.from_table.span.clone(),
153            });
154        }
155
156        if !self.tables.contains_key(to_table) {
157            self.errors.push(SemanticError {
158                message: format!("Table '{}' not found", to_table),
159                span: ref_def.to_table.span.clone(),
160            });
161        }
162
163        // Validate columns exist in their respective tables
164        for col in &ref_def.from_columns {
165            if let Some(columns) = self.columns.get(from_table) {
166                if !columns.iter().any(|c| c.name == col.name) {
167                    self.errors.push(SemanticError {
168                        message: format!(
169                            "Column '{}' not found in table '{}'",
170                            col.name, from_table
171                        ),
172                        span: col.span.clone(),
173                    });
174                }
175            }
176        }
177
178        for col in &ref_def.to_columns {
179            if let Some(columns) = self.columns.get(to_table) {
180                if !columns.iter().any(|c| c.name == col.name) {
181                    self.errors.push(SemanticError {
182                        message: format!("Column '{}' not found in table '{}'", col.name, to_table),
183                        span: col.span.clone(),
184                    });
185                }
186            }
187        }
188    }
189
190    fn validate_inline_ref(&mut self, inline_ref: &InlineRef, _current_table: &str) {
191        let target_table = &inline_ref.target_table.name;
192
193        if !self.tables.contains_key(target_table) {
194            self.errors.push(SemanticError {
195                message: format!("Referenced table '{}' not found", target_table),
196                span: inline_ref.target_table.span.clone(),
197            });
198        }
199    }
200
201    pub fn find_symbol_at_position(&self, pos: usize) -> Option<&SymbolInfo> {
202        // Search in tables
203        for symbol in self.tables.values() {
204            if symbol.span.contains(&pos) {
205                return Some(symbol);
206            }
207        }
208
209        // Search in columns
210        for columns in self.columns.values() {
211            for symbol in columns {
212                if symbol.span.contains(&pos) {
213                    return Some(symbol);
214                }
215            }
216        }
217
218        // Search in enums
219        for symbol in self.enums.values() {
220            if symbol.span.contains(&pos) {
221                return Some(symbol);
222            }
223        }
224
225        None
226    }
227
228    /// Find the definition location for a symbol at the given position
229    /// Returns the target symbol info (where to jump to)
230    pub fn find_definition_at_position(&self, pos: usize, ast: &Document) -> Option<SymbolInfo> {
231        // First, try to find what's under the cursor in the AST
232        for item in &ast.items {
233            match item {
234                DocumentItem::Table(table) => {
235                    // Check if cursor is on table name (this is the definition itself)
236                    if pos >= table.name.span.start && pos <= table.name.span.end {
237                        // Already at definition
238                        return Some(SymbolInfo {
239                            name: table.name.name.clone(),
240                            kind: SymbolKind::Table,
241                            span: table.name.span.clone(),
242                        });
243                    }
244
245                    // Check columns
246                    for table_item in &table.items {
247                        if let TableItem::Column(column) = table_item {
248                            // Check if cursor is on column name (this is the definition itself)
249                            if pos >= column.name.span.start && pos <= column.name.span.end {
250                                return Some(SymbolInfo {
251                                    name: column.name.name.clone(),
252                                    kind: SymbolKind::Column,
253                                    span: column.name.span.clone(),
254                                });
255                            }
256
257                            // Check if cursor is on column type (might be an enum reference)
258                            if pos >= column.col_type_span.start && pos <= column.col_type_span.end {
259                                // Try to find enum with this name
260                                if let Some(enum_symbol) = self.enums.get(&column.col_type) {
261                                    return Some(enum_symbol.clone());
262                                }
263                            }
264
265                            // Check inline references in column settings
266                            for setting in &column.settings {
267                                if let ColumnSetting::Ref(inline_ref) = setting {
268                                    // Check if cursor is on target table
269                                    if pos >= inline_ref.target_table.span.start
270                                        && pos <= inline_ref.target_table.span.end
271                                    {
272                                        if let Some(table_symbol) =
273                                            self.tables.get(&inline_ref.target_table.name)
274                                        {
275                                            return Some(table_symbol.clone());
276                                        }
277                                    }
278
279                                    // Check if cursor is on target column
280                                    if pos >= inline_ref.target_column.span.start
281                                        && pos <= inline_ref.target_column.span.end
282                                    {
283                                        // Find the column in the target table
284                                        if let Some(columns) =
285                                            self.columns.get(&inline_ref.target_table.name)
286                                        {
287                                            for col in columns {
288                                                if col.name == inline_ref.target_column.name {
289                                                    return Some(col.clone());
290                                                }
291                                            }
292                                        }
293                                    }
294                                }
295                            }
296                        }
297                    }
298                }
299                DocumentItem::Enum(enum_def) => {
300                    // Check if cursor is on enum name (this is the definition itself)
301                    if pos >= enum_def.name.span.start && pos <= enum_def.name.span.end {
302                        return Some(SymbolInfo {
303                            name: enum_def.name.name.clone(),
304                            kind: SymbolKind::Enum,
305                            span: enum_def.name.span.clone(),
306                        });
307                    }
308
309                    // Check if cursor is on enum member (this is the definition itself)
310                    for member in &enum_def.members {
311                        if pos >= member.name.span.start && pos <= member.name.span.end {
312                            return Some(SymbolInfo {
313                                name: member.name.name.clone(),
314                                kind: SymbolKind::EnumMember,
315                                span: member.name.span.clone(),
316                            });
317                        }
318                    }
319                }
320                DocumentItem::Ref(ref_def) => {
321                    // Check if cursor is on from_table
322                    if pos >= ref_def.from_table.span.start && pos <= ref_def.from_table.span.end
323                    {
324                        if let Some(table_symbol) = self.tables.get(&ref_def.from_table.name) {
325                            return Some(table_symbol.clone());
326                        }
327                    }
328
329                    // Check if cursor is on to_table
330                    if pos >= ref_def.to_table.span.start && pos <= ref_def.to_table.span.end {
331                        if let Some(table_symbol) = self.tables.get(&ref_def.to_table.name) {
332                            return Some(table_symbol.clone());
333                        }
334                    }
335
336                    // Check if cursor is on from_columns
337                    for from_col in &ref_def.from_columns {
338                        if pos >= from_col.span.start && pos <= from_col.span.end {
339                            // Find the column in the from_table
340                            if let Some(columns) = self.columns.get(&ref_def.from_table.name) {
341                                for col in columns {
342                                    if col.name == from_col.name {
343                                        return Some(col.clone());
344                                    }
345                                }
346                            }
347                        }
348                    }
349
350                    // Check if cursor is on to_columns
351                    for to_col in &ref_def.to_columns {
352                        if pos >= to_col.span.start && pos <= to_col.span.end {
353                            // Find the column in the to_table
354                            if let Some(columns) = self.columns.get(&ref_def.to_table.name) {
355                                for col in columns {
356                                    if col.name == to_col.name {
357                                        return Some(col.clone());
358                                    }
359                                }
360                            }
361                        }
362                    }
363                }
364                DocumentItem::Project(_) => {
365                    // No references to resolve in project items
366                }
367            }
368        }
369
370        None
371    }
372
373    pub fn to_diagnostics(&self, content: &str) -> Vec<Diagnostic> {
374        self.errors
375            .iter()
376            .map(|err| {
377                let range = span_to_range(&err.span, content);
378                Diagnostic {
379                    range,
380                    severity: Some(DiagnosticSeverity::ERROR),
381                    code: None,
382                    code_description: None,
383                    source: Some("dbml-lsp".to_string()),
384                    message: err.message.clone(),
385                    related_information: None,
386                    tags: None,
387                    data: None,
388                }
389            })
390            .collect()
391    }
392}
393
394fn span_to_range(span: &Span, content: &str) -> Range {
395    let start_pos = offset_to_position(span.start, content);
396    let end_pos = offset_to_position(span.end, content);
397    Range::new(start_pos, end_pos)
398}
399
400fn offset_to_position(offset: usize, content: &str) -> Position {
401    let mut line = 0;
402    let mut col = 0;
403
404    for (i, ch) in content.chars().enumerate() {
405        if i >= offset {
406            break;
407        }
408        if ch == '\n' {
409            line += 1;
410            col = 0;
411        } else {
412            col += 1;
413        }
414    }
415
416    Position::new(line, col)
417}