pub mod completion;
pub mod goto_definition;
pub mod hover;
pub mod semantic_tokens;
pub use completion::{completion, CompletionItem, CompletionKind};
pub use goto_definition::goto_definition;
pub use hover::{config_field_hover, hover, HoverInfo};
pub use semantic_tokens::{semantic_tokens, SemanticKind, SemanticToken};
use crate::ast::Schema;
use crate::diagnostic::Diagnostic;
use crate::ir::SchemaIr;
use crate::span::Span;
use crate::token::{Token, TokenKind};
use crate::validator::validate_all;
use crate::{Lexer, Parser};
#[derive(Debug, Clone)]
pub struct AnalysisResult {
pub ast: Option<Schema>,
pub ir: Option<SchemaIr>,
pub diagnostics: Vec<Diagnostic>,
pub tokens: Vec<Token>,
}
pub fn analyze(source: &str) -> AnalysisResult {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let mut lexer = Lexer::new(source);
let mut tokens: Vec<Token> = Vec::new();
let mut lex_ok = true;
loop {
match lexer.next_token() {
Ok(tok) => {
let is_eof = matches!(tok.kind, TokenKind::Eof);
tokens.push(tok);
if is_eof {
break;
}
}
Err(e) => {
let recoverable = matches!(e, crate::SchemaError::UnexpectedCharacter(..));
diagnostics.push(Diagnostic::from(e));
if !recoverable {
lex_ok = false;
break;
}
}
}
}
if !lex_ok {
return AnalysisResult {
ast: None,
ir: None,
diagnostics,
tokens,
};
}
let mut parser = Parser::new(&tokens, source);
let parse_result = parser.parse_schema();
for e in parser.take_errors() {
diagnostics.push(Diagnostic::from(e));
}
let ast = match parse_result {
Ok(schema) => schema,
Err(e) => {
diagnostics.push(Diagnostic::from(e));
return AnalysisResult {
ast: None,
ir: None,
diagnostics,
tokens,
};
}
};
let (ir, val_errors) = validate_all(ast.clone());
for e in val_errors {
diagnostics.push(Diagnostic::from(e));
}
AnalysisResult {
ast: Some(ast),
ir,
diagnostics,
tokens,
}
}
pub(super) fn span_contains(span: Span, offset: usize) -> bool {
offset >= span.start && offset <= span.end
}
#[cfg(test)]
mod tests {
use super::*;
const VALID_SCHEMA: &str = r#"
datasource db {
provider = "postgresql"
url = "postgresql://localhost/mydb"
}
model User {
id Int @id
email String @unique
name String
}
enum Role {
Admin
User
}
"#;
#[test]
fn analyze_valid_schema() {
let r = analyze(VALID_SCHEMA);
assert!(r.diagnostics.is_empty(), "unexpected: {:?}", r.diagnostics);
assert!(r.ast.is_some());
assert!(r.ir.is_some());
}
#[test]
fn analyze_validation_error_returns_diagnostic() {
let src = r#"
model Broken {
id Int
name String
}
"#;
let r = analyze(src);
assert!(r.ast.is_some());
let _ = r.diagnostics;
}
#[test]
fn analyze_lex_error() {
let src = "model User { id # Int }";
let r = analyze(src);
assert!(!r.diagnostics.is_empty());
}
#[test]
fn completion_top_level() {
let items = completion("", 0);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"model"));
assert!(labels.contains(&"enum"));
assert!(labels.contains(&"datasource"));
assert!(labels.contains(&"generator"));
}
#[test]
fn completion_inside_model_returns_scalar_types() {
let src = "model User {\n \n}";
let offset = src.find("\n \n").unwrap() + 3; let items = completion(src, offset);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"String"), "got: {:?}", labels);
assert!(labels.contains(&"Int"), "got: {:?}", labels);
}
#[test]
fn completion_after_at_returns_field_attrs() {
let src = "model User {\n id Int @\n}";
let offset = src.find('@').unwrap() + 1; let items = completion(src, offset);
assert!(
items
.iter()
.any(|i| i.kind == CompletionKind::FieldAttribute),
"got: {:?}",
items
);
}
#[test]
fn hover_on_field_returns_type_info() {
let src = VALID_SCHEMA;
let offset = src.find("email").unwrap() + 2;
let h = hover(src, offset);
assert!(h.is_some(), "hover returned None");
let info = h.unwrap();
assert!(info.content.contains("email") || info.content.contains("String"));
}
#[test]
fn goto_definition_resolves_user_type() {
let src = r#"
model Post {
id Int @id
authorId Int
author User @relation(fields: [authorId], references: [id])
}
model User {
id Int @id
email String @unique
}
"#;
let offset = src.find("author User").unwrap() + "author ".len() + 1;
let span = goto_definition(src, offset);
assert!(span.is_some(), "goto_definition returned None");
let target = span.unwrap();
assert!(
&src[target.start..target.end].contains("User"),
"span does not point to User: {:?}",
&src[target.start..target.end]
);
}
}