nautilus_schema/analysis/
mod.rs1pub mod completion;
21pub mod goto_definition;
22pub mod hover;
23pub mod semantic_tokens;
24
25pub use completion::{completion, completion_with_analysis, CompletionItem, CompletionKind};
26pub use goto_definition::{goto_definition, goto_definition_with_analysis};
27pub use hover::{config_field_hover, hover, hover_with_analysis, 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() {
106 diagnostics.push(Diagnostic::from(e));
107 }
108
109 let ast = match parse_result {
110 Ok(schema) => schema,
111 Err(e) => {
112 diagnostics.push(Diagnostic::from(e));
113 return AnalysisResult {
114 ast: None,
115 ir: None,
116 diagnostics,
117 tokens,
118 };
119 }
120 };
121
122 let (ir, val_errors) = validate_all(ast.clone());
123 for e in val_errors {
124 diagnostics.push(Diagnostic::from(e));
125 }
126
127 AnalysisResult {
128 ast: Some(ast),
129 ir,
130 diagnostics,
131 tokens,
132 }
133}
134
135pub(super) fn span_contains(span: Span, offset: usize) -> bool {
137 offset >= span.start && offset <= span.end
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 const VALID_SCHEMA: &str = r#"
145datasource db {
146 provider = "postgresql"
147 url = "postgresql://localhost/mydb"
148}
149
150model User {
151 id Int @id
152 email String @unique
153 name String
154}
155
156enum Role {
157 Admin
158 User
159}
160"#;
161
162 #[test]
163 fn analyze_valid_schema() {
164 let r = analyze(VALID_SCHEMA);
165 assert!(r.diagnostics.is_empty(), "unexpected: {:?}", r.diagnostics);
166 assert!(r.ast.is_some());
167 assert!(r.ir.is_some());
168 }
169
170 #[test]
171 fn analyze_validation_error_returns_diagnostic() {
172 let src = r#"
173model Broken {
174 id Int
175 name String
176}
177"#;
178 let r = analyze(src);
179 assert!(r.ast.is_some());
180 let _ = r.diagnostics;
181 }
182
183 #[test]
184 fn analyze_lex_error() {
185 let src = "model User { id # Int }";
186 let r = analyze(src);
187 assert!(!r.diagnostics.is_empty());
188 }
189
190 #[test]
191 fn completion_top_level() {
192 let items = completion("", 0);
193 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
194 assert!(labels.contains(&"model"));
195 assert!(labels.contains(&"enum"));
196 assert!(labels.contains(&"datasource"));
197 assert!(labels.contains(&"generator"));
198 }
199
200 #[test]
201 fn completion_inside_model_returns_scalar_types() {
202 let src = "model User {\n \n}";
203 let offset = src.find("\n \n").unwrap() + 3;
204 let items = completion(src, offset);
205 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
206 assert!(labels.contains(&"String"), "got: {:?}", labels);
207 assert!(labels.contains(&"Int"), "got: {:?}", labels);
208 }
209
210 #[test]
211 fn completion_after_at_returns_field_attrs() {
212 let src = "model User {\n id Int @\n}";
213 let offset = src.find('@').unwrap() + 1;
214 let items = completion(src, offset);
215 assert!(
216 items
217 .iter()
218 .any(|i| i.kind == CompletionKind::FieldAttribute),
219 "got: {:?}",
220 items
221 );
222 }
223
224 #[test]
225 fn hover_on_field_returns_type_info() {
226 let src = VALID_SCHEMA;
227 let offset = src.find("email").unwrap() + 2;
228 let h = hover(src, offset);
229 assert!(h.is_some(), "hover returned None");
230 let info = h.unwrap();
231 assert!(info.content.contains("email") || info.content.contains("String"));
232 }
233
234 #[test]
235 fn goto_definition_resolves_user_type() {
236 let src = r#"
237model Post {
238 id Int @id
239 authorId Int
240 author User @relation(fields: [authorId], references: [id])
241}
242
243model User {
244 id Int @id
245 email String @unique
246}
247"#;
248 let offset = src.find("author User").unwrap() + "author ".len() + 1;
249 let span = goto_definition(src, offset);
250 assert!(span.is_some(), "goto_definition returned None");
251 let target = span.unwrap();
252 assert!(
253 &src[target.start..target.end].contains("User"),
254 "span does not point to User: {:?}",
255 &src[target.start..target.end]
256 );
257 }
258}