use crate::includes::IncludedWord;
use seqc::{Parser, TypeChecker};
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
use tracing::{debug, warn};
#[cfg(test)]
pub fn check_document(source: &str) -> Vec<Diagnostic> {
check_document_with_includes(source, &[])
}
pub fn check_document_with_includes(
source: &str,
included_words: &[IncludedWord],
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let mut parser = Parser::new(source);
let program = match parser.parse() {
Ok(prog) => prog,
Err(err) => {
debug!("Parse error: {}", err);
diagnostics.push(error_to_diagnostic(&err, source));
return diagnostics;
}
};
let included_word_names: Vec<&str> = included_words.iter().map(|w| w.name.as_str()).collect();
if let Err(err) = program.validate_word_calls_with_externals(&included_word_names) {
debug!("Validation error: {}", err);
diagnostics.push(error_to_diagnostic(&err, source));
}
let mut typechecker = TypeChecker::new();
let external_words: Vec<(&str, Option<&seqc::Effect>)> = included_words
.iter()
.map(|w| (w.name.as_str(), w.effect.as_ref()))
.collect();
typechecker.register_external_words(&external_words);
if let Err(err) = typechecker.check_program(&program) {
debug!("Type error: {}", err);
diagnostics.push(error_to_diagnostic(&err, source));
}
diagnostics
}
fn error_to_diagnostic(error: &str, source: &str) -> Diagnostic {
let (line, message) = extract_line_info(error, source);
let line_length = source
.lines()
.nth(line)
.map(|l| l.len() as u32)
.unwrap_or(0);
Diagnostic {
range: Range {
start: Position {
line: line as u32,
character: 0,
},
end: Position {
line: line as u32,
character: line_length,
},
},
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some("seq".to_string()),
message: message.to_string(),
related_information: None,
tags: None,
data: None,
}
}
fn extract_line_info<'a>(error: &'a str, source: &str) -> (usize, &'a str) {
if let Some(idx) = error.find("at line ") {
let after = &error[idx + 8..];
if let Some(end) = after.find(|c: char| !c.is_ascii_digit())
&& let Ok(line) = after[..end].parse::<usize>()
{
return (line.saturating_sub(1), error); }
}
if let Some(idx) = error.find("line ") {
let after = &error[idx + 5..];
if let Some(end) = after.find(|c: char| !c.is_ascii_digit())
&& let Ok(line) = after[..end].parse::<usize>()
{
return (line.saturating_sub(1), error);
}
}
if let Some(rest) = error.strip_prefix("Unknown word: '")
&& let Some(end) = rest.find('\'')
&& let Some(line) = find_word_line(source, &rest[..end])
{
return (line, error);
}
if let Some(rest) = error.strip_prefix("Undefined word '")
&& let Some(end) = rest.find('\'')
&& let Some(line) = find_word_line(source, &rest[..end])
{
return (line, error);
}
warn!("Could not extract line info from error: {}", error);
(0, error)
}
fn find_word_line(source: &str, word: &str) -> Option<usize> {
for (line_num, line) in source.lines().enumerate() {
if !line.contains(word) {
continue;
}
let trimmed = line.trim();
if trimmed.starts_with('#') {
continue;
}
for token in trimmed.split_whitespace() {
if token == word {
return Some(line_num);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_error() {
let source = ": foo 1 2 +";
let diagnostics = check_document(source);
assert!(!diagnostics.is_empty());
}
#[test]
fn test_type_error() {
let source = ": foo ( -- Int ) \"hello\" ;";
let diagnostics = check_document(source);
assert!(!diagnostics.is_empty());
}
#[test]
fn test_undefined_word() {
let source = ": main ( -- Int ) undefined-word 0 ;";
let diagnostics = check_document(source);
assert!(!diagnostics.is_empty(), "Expected diagnostics but got none");
assert!(
diagnostics[0].message.contains("Undefined word"),
"Expected 'Undefined word' in message, got: {}",
diagnostics[0].message
);
}
#[test]
fn test_valid_program() {
let source = ": main ( -- Int ) 0 ;";
let diagnostics = check_document(source);
assert!(diagnostics.is_empty());
}
#[test]
fn test_find_word_with_special_chars() {
let source = "string->float\nfile-exists?\nsome-word";
assert_eq!(find_word_line(source, "string->float"), Some(0));
assert_eq!(find_word_line(source, "file-exists?"), Some(1));
assert_eq!(find_word_line(source, "some-word"), Some(2));
}
#[test]
fn test_find_word_skips_comments() {
let source = "# string->float comment\nstring->float";
assert_eq!(find_word_line(source, "string->float"), Some(1));
}
}