diff --git a/README.md b/README.md index 12e81b9..e992071 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Language Server Protocol (LSP) implementation for [Comline](https://github.com ### ✅ Fully Implemented -- **Diagnostics** - Real-time syntax error detection and reporting +- **Diagnostics** - Real-time syntax errors plus `comline-core`'s validation pass (undefined types, duplicate declarations, ...) — the same checks `comline build` runs - **Document Symbols** - Hierarchical outline view of structs, enums, protocols, and constants - **Hover Information** - Rich tooltips showing full type definitions and signatures - **Go to Definition** - Jump from type references to their declarations diff --git a/src/analysis/diagnostics.rs b/src/analysis/diagnostics.rs index f946034..275251e 100644 --- a/src/analysis/diagnostics.rs +++ b/src/analysis/diagnostics.rs @@ -1,8 +1,67 @@ -// Diagnostic generation from parse errors +// Diagnostic generation — parse errors, and `comline-core`'s validation pass use crate::util::byte_range_to_lsp_range; +use comline_core::schema::idl::grammar::Document; +use comline_core::schema::ir::compiler::interpreter::incremental::IncrementalInterpreter; +use comline_core::schema::ir::compiler::Compile; +use comline_core::schema::ir::validation; use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity}; +/// Semantic diagnostics from `comline-core`'s validation pass — undefined type +/// references, duplicate declarations, and the like: the same checks +/// `comline build` runs. Call only on a document that parsed cleanly. +pub fn validation_diagnostics(source: &str, document: &Document) -> Vec { + let units = IncrementalInterpreter::from_declarations(document.0.clone()); + let errors = match validation::validate(&units) { + Ok(()) => return vec![], + Err(errors) => errors, + }; + + errors + .into_iter() + .map(|error| { + let range = error + .span + .map(|(start, end)| byte_range_to_lsp_range(source, start, end)) + .unwrap_or_default(); + + let message = if error.context.is_empty() { + error.message + } else { + format!("{} — {}", error.message, error.context) + }; + + Diagnostic { + range, + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("comline".to_string()), + message, + related_information: None, + tags: None, + data: None, + } + }) + .collect() +} + +/// Parse-error + validation diagnostics for `source`. Validation is skipped +/// while the tree is malformed (parse errors present). +pub fn all_diagnostics( + source: &str, + errors: &[rust_sitter::errors::ParseError], + document: Option<&Document>, +) -> Vec { + let mut diagnostics = generate_diagnostics(source, errors); + if errors.is_empty() { + if let Some(doc) = document { + diagnostics.extend(validation_diagnostics(source, doc)); + } + } + diagnostics +} + /// Generate LSP diagnostics from parse errors pub fn generate_diagnostics(source: &str, errors: &[rust_sitter::errors::ParseError]) -> Vec { errors @@ -80,8 +139,50 @@ struct User { "#; let result = parser::parse(source).unwrap(); assert!(!result.has_errors()); - + let diagnostics = generate_diagnostics(source, &result.errors); assert!(diagnostics.is_empty()); } + + fn diags(source: &str) -> Vec { + let result = parser::parse(source).unwrap(); + all_diagnostics(source, &result.errors, result.document.as_ref()) + } + + #[test] + fn validation_flags_an_undefined_type_reference() { + let d = diags("struct Order {\n buyer: Customer\n}\n"); + assert!( + d.iter().any(|x| x.message.to_lowercase().contains("customer")), + "expected an undefined-type diagnostic mentioning `Customer`, got {d:?}" + ); + } + + #[test] + fn validation_flags_a_duplicate_declaration() { + let d = diags("struct User {\n a: string\n}\nstruct User {\n b: string\n}\n"); + assert!( + d.iter().any(|x| x.message.to_lowercase().contains("duplicate")), + "expected a duplicate-definition diagnostic, got {d:?}" + ); + } + + #[test] + fn a_well_formed_schema_has_no_diagnostics() { + let d = diags( + "struct Item {\n id: u64\n}\n\nstruct Cart {\n items: Item[]\n}\n", + ); + assert!(d.is_empty(), "expected no diagnostics, got {d:?}"); + } + + #[test] + fn validation_is_skipped_while_the_tree_is_malformed() { + // A parse error is present, so validation must not run (no panic, no + // spurious semantic errors) — only the parse diagnostic. + let source = "struct User {\n name string\n}\n"; + let result = parser::parse(source).unwrap(); + assert!(result.has_errors()); + let d = all_diagnostics(source, &result.errors, result.document.as_ref()); + assert!(!d.is_empty()); + } } diff --git a/src/backend.rs b/src/backend.rs index 0e8c957..127c909 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,5 +1,10 @@ -use crate::document::DocumentStore; +// use std::sync::Arc; + +// +use crate::document::DocumentStore; + +// use tower_lsp::jsonrpc::Result; use tower_lsp::lsp_types::*; use tower_lsp::{Client, LanguageServer}; @@ -287,9 +292,14 @@ impl Backend { // Parse the document match parser::parse(&document.text) { Ok(result) => { - // Generate LSP diagnostics from parse errors - let lsp_diagnostics = diagnostics::generate_diagnostics(&document.text, &result.errors); - + // Parse-error diagnostics, plus `comline-core`'s validation + // pass once the tree is well-formed. + let lsp_diagnostics = diagnostics::all_diagnostics( + &document.text, + &result.errors, + result.document.as_ref(), + ); + // Log parse results if result.is_ok() { if let Some(doc) = &result.document {