1pub mod diagnostics;
24pub mod document;
25pub mod span;
26
27pub use diagnostics::{DiagnosticKind, SyntaxDiagnostic};
28pub use document::{
29 ParsedDocument, analyze, collect_error_nodes, collect_error_nodes_with_source, parse_document,
30};
31pub use span::{Point, Span};
32
33#[cfg(test)]
34mod tests {
35 use super::*;
36
37 #[test]
38 fn parses_valid_source_without_diagnostics() {
39 let source = "2024-01-01 open Assets:Cash\n";
40 let doc = parse_document(source);
41
42 assert!(!doc.has_errors());
43 assert!(doc.diagnostics().is_empty());
44 assert_eq!(doc.source(), source);
45 }
46
47 #[test]
48 fn reports_positioned_diagnostic_for_invalid_source() {
49 let source = "2024-01-01 open\n";
50 let doc = parse_document(source);
51
52 assert!(doc.has_errors());
53 assert!(!doc.diagnostics().is_empty());
54
55 let diagnostic = &doc.diagnostics()[0];
56 assert!(diagnostic.span.start.byte <= source.len());
58 assert!(diagnostic.span.end.byte <= source.len());
59 assert!(diagnostic.span.start.row == 0);
60 }
61
62 #[test]
63 fn into_result_succeeds_for_valid_source() {
64 let source = "2024-01-01 open Assets:Cash\n";
65 let doc = parse_document(source).into_result();
66
67 assert!(doc.is_ok());
68 }
69
70 #[test]
71 fn into_result_fails_for_invalid_source() {
72 let source = "2024-01-01 open\n";
73 let result = parse_document(source).into_result();
74
75 let diagnostics = match result {
76 Ok(_) => panic!("expected an error"),
77 Err(diagnostics) => diagnostics,
78 };
79 assert!(!diagnostics.is_empty());
80 }
81
82 #[test]
83 fn analyze_is_an_alias_for_parse_document() {
84 let source = "2024-01-01 open Assets:Cash\n";
85 let doc = analyze(source);
86
87 assert!(!doc.has_errors());
88 }
89
90 #[test]
91 fn collect_error_nodes_matches_document_diagnostics_count() {
92 let source = "2024-01-01 open\n";
93 let doc = parse_document(source);
94
95 let diagnostics = collect_error_nodes(doc.tree());
96 assert_eq!(diagnostics.len(), doc.diagnostics().len());
97 }
98
99 #[test]
100 fn missing_closing_quote_is_reported_as_an_unterminated_string() {
101 let source = "2024-01-01 open Assets:Cash\n note: \"unterminated\n";
105 let doc = parse_document(source);
106
107 assert!(doc.has_errors());
108 let diagnostic = doc
109 .diagnostics()
110 .iter()
111 .find(|d| d.kind == DiagnosticKind::Missing)
112 .expect("expected a missing-node diagnostic");
113 assert_eq!(
114 diagnostic.message,
115 "unterminated string: missing closing `\"`"
116 );
117 }
118
119 #[test]
120 fn missing_closing_quote_inside_a_transaction_payee_is_contextual() {
121 let source = "2024-01-01 * \"unterminated\npayee\n";
127 let doc = parse_document(source);
128
129 assert!(doc.has_errors());
130 let diagnostic = doc
131 .diagnostics()
132 .iter()
133 .find(|d| d.kind == DiagnosticKind::Missing)
134 .expect("expected a missing-node diagnostic");
135 assert_eq!(
136 diagnostic.message,
137 "unterminated string: missing closing `\"`"
138 );
139 }
140
141 #[test]
142 fn unexpected_syntax_after_known_tokens_lists_them_and_quotes_the_snippet() {
143 let source = "2024-01-01 balance Assets:Cash 10\n";
147 let doc = parse_document(source);
148
149 assert!(doc.has_errors());
150 let diagnostic = doc
151 .diagnostics()
152 .iter()
153 .find(|d| d.kind == DiagnosticKind::Error)
154 .expect("expected an error-node diagnostic");
155 assert!(
156 diagnostic.message.contains("a date"),
157 "message should mention the recognized date: {}",
158 diagnostic.message
159 );
160 assert!(
161 diagnostic.message.contains("an account name"),
162 "message should mention the recognized account: {}",
163 diagnostic.message
164 );
165 assert!(
166 !diagnostic.message.contains('('),
167 "message should not be a raw tree-sitter S-expression: {}",
168 diagnostic.message
169 );
170 }
171
172 #[test]
173 fn unexpected_syntax_with_no_recognized_tokens_still_quotes_the_snippet() {
174 let source = "garbage line not a directive\n";
175 let doc = parse_document(source);
176
177 assert!(doc.has_errors());
178 let diagnostic = &doc.diagnostics()[0];
179 assert_eq!(diagnostic.kind, DiagnosticKind::Error);
180 assert!(
181 diagnostic.message.contains("garbage line not a directive"),
182 "message should quote the unexpected text: {}",
183 diagnostic.message
184 );
185 assert!(!diagnostic.message.starts_with('('));
186 }
187
188 #[test]
189 fn nested_error_nodes_are_not_reported_twice() {
190 let source = "@@@\n###\n$$$\n";
194 let doc = parse_document(source);
195
196 assert!(doc.has_errors());
197 assert!(
198 doc.diagnostics()
199 .iter()
200 .all(|d| d.kind == DiagnosticKind::Error)
201 );
202 }
203
204 #[test]
205 fn collect_error_nodes_without_source_still_gives_a_contextual_message() {
206 let source = "2024-01-01 balance Assets:Cash 10\n";
207 let doc = parse_document(source);
208
209 let diagnostics = collect_error_nodes(doc.tree());
210 let diagnostic = diagnostics
211 .iter()
212 .find(|d| d.kind == DiagnosticKind::Error)
213 .expect("expected an error-node diagnostic");
214 assert!(diagnostic.message.contains("a date"));
217 assert!(!diagnostic.message.contains('('));
218 }
219
220 #[test]
221 fn collect_error_nodes_with_source_matches_document_diagnostics() {
222 let source = "2024-01-01 balance Assets:Cash 10\n";
223 let doc = parse_document(source);
224
225 let diagnostics = collect_error_nodes_with_source(doc.tree(), source);
226 assert_eq!(diagnostics, doc.diagnostics());
227 }
228}