Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 103 additions & 2 deletions src/analysis/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -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<Diagnostic> {
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<Diagnostic> {
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<Diagnostic> {
errors
Expand Down Expand Up @@ -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<Diagnostic> {
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());
}
}
18 changes: 14 additions & 4 deletions src/backend.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
Loading