pub mod diagnostics;
pub mod document;
pub mod span;
pub use diagnostics::{DiagnosticKind, SyntaxDiagnostic};
pub use document::{
ParsedDocument, analyze, collect_error_nodes, collect_error_nodes_with_source, parse_document,
};
pub use span::{Point, Span};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_valid_source_without_diagnostics() {
let source = "2024-01-01 open Assets:Cash\n";
let doc = parse_document(source);
assert!(!doc.has_errors());
assert!(doc.diagnostics().is_empty());
assert_eq!(doc.source(), source);
}
#[test]
fn reports_positioned_diagnostic_for_invalid_source() {
let source = "2024-01-01 open\n";
let doc = parse_document(source);
assert!(doc.has_errors());
assert!(!doc.diagnostics().is_empty());
let diagnostic = &doc.diagnostics()[0];
assert!(diagnostic.span.start.byte <= source.len());
assert!(diagnostic.span.end.byte <= source.len());
assert!(diagnostic.span.start.row == 0);
}
#[test]
fn into_result_succeeds_for_valid_source() {
let source = "2024-01-01 open Assets:Cash\n";
let doc = parse_document(source).into_result();
assert!(doc.is_ok());
}
#[test]
fn into_result_fails_for_invalid_source() {
let source = "2024-01-01 open\n";
let result = parse_document(source).into_result();
let diagnostics = match result {
Ok(_) => panic!("expected an error"),
Err(diagnostics) => diagnostics,
};
assert!(!diagnostics.is_empty());
}
#[test]
fn analyze_is_an_alias_for_parse_document() {
let source = "2024-01-01 open Assets:Cash\n";
let doc = analyze(source);
assert!(!doc.has_errors());
}
#[test]
fn collect_error_nodes_matches_document_diagnostics_count() {
let source = "2024-01-01 open\n";
let doc = parse_document(source);
let diagnostics = collect_error_nodes(doc.tree());
assert_eq!(diagnostics.len(), doc.diagnostics().len());
}
#[test]
fn missing_closing_quote_is_reported_as_an_unterminated_string() {
let source = "2024-01-01 open Assets:Cash\n note: \"unterminated\n";
let doc = parse_document(source);
assert!(doc.has_errors());
let diagnostic = doc
.diagnostics()
.iter()
.find(|d| d.kind == DiagnosticKind::Missing)
.expect("expected a missing-node diagnostic");
assert_eq!(
diagnostic.message,
"unterminated string: missing closing `\"`"
);
}
#[test]
fn missing_closing_quote_inside_a_transaction_payee_is_contextual() {
let source = "2024-01-01 * \"unterminated\npayee\n";
let doc = parse_document(source);
assert!(doc.has_errors());
let diagnostic = doc
.diagnostics()
.iter()
.find(|d| d.kind == DiagnosticKind::Missing)
.expect("expected a missing-node diagnostic");
assert_eq!(
diagnostic.message,
"unterminated string: missing closing `\"`"
);
}
#[test]
fn unexpected_syntax_after_known_tokens_lists_them_and_quotes_the_snippet() {
let source = "2024-01-01 balance Assets:Cash 10\n";
let doc = parse_document(source);
assert!(doc.has_errors());
let diagnostic = doc
.diagnostics()
.iter()
.find(|d| d.kind == DiagnosticKind::Error)
.expect("expected an error-node diagnostic");
assert!(
diagnostic.message.contains("a date"),
"message should mention the recognized date: {}",
diagnostic.message
);
assert!(
diagnostic.message.contains("an account name"),
"message should mention the recognized account: {}",
diagnostic.message
);
assert!(
!diagnostic.message.contains('('),
"message should not be a raw tree-sitter S-expression: {}",
diagnostic.message
);
}
#[test]
fn unexpected_syntax_with_no_recognized_tokens_still_quotes_the_snippet() {
let source = "garbage line not a directive\n";
let doc = parse_document(source);
assert!(doc.has_errors());
let diagnostic = &doc.diagnostics()[0];
assert_eq!(diagnostic.kind, DiagnosticKind::Error);
assert!(
diagnostic.message.contains("garbage line not a directive"),
"message should quote the unexpected text: {}",
diagnostic.message
);
assert!(!diagnostic.message.starts_with('('));
}
#[test]
fn nested_error_nodes_are_not_reported_twice() {
let source = "@@@\n###\n$$$\n";
let doc = parse_document(source);
assert!(doc.has_errors());
assert!(
doc.diagnostics()
.iter()
.all(|d| d.kind == DiagnosticKind::Error)
);
}
#[test]
fn collect_error_nodes_without_source_still_gives_a_contextual_message() {
let source = "2024-01-01 balance Assets:Cash 10\n";
let doc = parse_document(source);
let diagnostics = collect_error_nodes(doc.tree());
let diagnostic = diagnostics
.iter()
.find(|d| d.kind == DiagnosticKind::Error)
.expect("expected an error-node diagnostic");
assert!(diagnostic.message.contains("a date"));
assert!(!diagnostic.message.contains('('));
}
#[test]
fn collect_error_nodes_with_source_matches_document_diagnostics() {
let source = "2024-01-01 balance Assets:Cash 10\n";
let doc = parse_document(source);
let diagnostics = collect_error_nodes_with_source(doc.tree(), source);
assert_eq!(diagnostics, doc.diagnostics());
}
}