nautilus_schema/analysis/
mod.rs1pub mod completion;
21pub mod goto_definition;
22pub mod hover;
23pub mod semantic_tokens;
24
25pub use completion::{completion, CompletionItem, CompletionKind};
26pub use goto_definition::goto_definition;
27pub use hover::{config_field_hover, hover, HoverInfo};
28pub use semantic_tokens::{semantic_tokens, SemanticKind, SemanticToken};
29
30use crate::ast::Schema;
31use crate::diagnostic::Diagnostic;
32use crate::ir::SchemaIr;
33use crate::span::Span;
34use crate::token::{Token, TokenKind};
35use crate::validator::validate_all;
36use crate::{Lexer, Parser};
37
38#[derive(Debug, Clone)]
45pub struct AnalysisResult {
46 pub ast: Option<Schema>,
48 pub ir: Option<SchemaIr>,
50 pub diagnostics: Vec<Diagnostic>,
52 pub tokens: Vec<Token>,
54}
55
56pub fn analyze(source: &str) -> AnalysisResult {
63 let mut diagnostics: Vec<Diagnostic> = Vec::new();
64
65 let mut lexer = Lexer::new(source);
66 let mut tokens: Vec<Token> = Vec::new();
67 let mut lex_ok = true;
68
69 loop {
70 match lexer.next_token() {
71 Ok(tok) => {
72 let is_eof = matches!(tok.kind, TokenKind::Eof);
73 tokens.push(tok);
74 if is_eof {
75 break;
76 }
77 }
78 Err(e) => {
79 let recoverable = matches!(e, crate::SchemaError::UnexpectedCharacter(..));
84 diagnostics.push(Diagnostic::from(e));
85 if !recoverable {
86 lex_ok = false;
87 break;
88 }
89 }
90 }
91 }
92
93 if !lex_ok {
94 return AnalysisResult {
95 ast: None,
96 ir: None,
97 diagnostics,
98 tokens,
99 };
100 }
101
102 let mut parser = Parser::new(&tokens, source);
103 let parse_result = parser.parse_schema();
104
105 for e in parser.take_errors() {
107 diagnostics.push(Diagnostic::from(e));
108 }
109
110 let ast = match parse_result {
111 Ok(schema) => schema,
112 Err(e) => {
113 diagnostics.push(Diagnostic::from(e));
114 return AnalysisResult {
115 ast: None,
116 ir: None,
117 diagnostics,
118 tokens,
119 };
120 }
121 };
122
123 let (ir, val_errors) = validate_all(ast.clone());
124 for e in val_errors {
125 diagnostics.push(Diagnostic::from(e));
126 }
127
128 AnalysisResult {
129 ast: Some(ast),
130 ir,
131 diagnostics,
132 tokens,
133 }
134}
135
136pub(super) fn span_contains(span: Span, offset: usize) -> bool {
138 offset >= span.start && offset <= span.end
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 const VALID_SCHEMA: &str = r#"
146datasource db {
147 provider = "postgresql"
148 url = "postgresql://localhost/mydb"
149}
150
151model User {
152 id Int @id
153 email String @unique
154 name String
155}
156
157enum Role {
158 Admin
159 User
160}
161"#;
162
163 #[test]
164 fn analyze_valid_schema() {
165 let r = analyze(VALID_SCHEMA);
166 assert!(r.diagnostics.is_empty(), "unexpected: {:?}", r.diagnostics);
167 assert!(r.ast.is_some());
168 assert!(r.ir.is_some());
169 }
170
171 #[test]
172 fn analyze_validation_error_returns_diagnostic() {
173 let src = r#"
175model Broken {
176 id Int
177 name String
178}
179"#;
180 let r = analyze(src);
181 assert!(r.ast.is_some());
184 let _ = r.diagnostics;
186 }
187
188 #[test]
189 fn analyze_lex_error() {
190 let src = "model User { id # Int }";
191 let r = analyze(src);
192 assert!(!r.diagnostics.is_empty());
193 }
194
195 #[test]
196 fn completion_top_level() {
197 let items = completion("", 0);
198 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
199 assert!(labels.contains(&"model"));
200 assert!(labels.contains(&"enum"));
201 assert!(labels.contains(&"datasource"));
202 assert!(labels.contains(&"generator"));
203 }
204
205 #[test]
206 fn completion_inside_model_returns_scalar_types() {
207 let src = "model User {\n \n}";
209 let offset = src.find("\n \n").unwrap() + 3; let items = completion(src, offset);
211 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
212 assert!(labels.contains(&"String"), "got: {:?}", labels);
213 assert!(labels.contains(&"Int"), "got: {:?}", labels);
214 }
215
216 #[test]
217 fn completion_after_at_returns_field_attrs() {
218 let src = "model User {\n id Int @\n}";
219 let offset = src.find('@').unwrap() + 1; let items = completion(src, offset);
221 assert!(
222 items
223 .iter()
224 .any(|i| i.kind == CompletionKind::FieldAttribute),
225 "got: {:?}",
226 items
227 );
228 }
229
230 #[test]
231 fn hover_on_field_returns_type_info() {
232 let src = VALID_SCHEMA;
233 let offset = src.find("email").unwrap() + 2;
235 let h = hover(src, offset);
236 assert!(h.is_some(), "hover returned None");
237 let info = h.unwrap();
238 assert!(info.content.contains("email") || info.content.contains("String"));
239 }
240
241 #[test]
242 fn goto_definition_resolves_user_type() {
243 let src = r#"
244model Post {
245 id Int @id
246 authorId Int
247 author User @relation(fields: [authorId], references: [id])
248}
249
250model User {
251 id Int @id
252 email String @unique
253}
254"#;
255 let offset = src.find("author User").unwrap() + "author ".len() + 1;
257 let span = goto_definition(src, offset);
258 assert!(span.is_some(), "goto_definition returned None");
259 let target = span.unwrap();
261 assert!(
262 &src[target.start..target.end].contains("User"),
263 "span does not point to User: {:?}",
264 &src[target.start..target.end]
265 );
266 }
267}