1#![cfg_attr(not(test), warn(missing_docs))]
108mod diagnostic;
109pub mod fix;
111mod rules;
112mod runner;
113mod visitor;
114
115pub use diagnostic::{Diagnostic, DiagnosticKind, Severity};
116pub use fix::{
117 Fix, FixApplicator, FixConfig, FixContext, FixError, FixId, FixProvider, FixResult,
118 FixStatistics, SourcePosition, SourceRange,
119};
120pub use rules::{LintRule, RuleConfig};
121pub use runner::{LintConfig, LintContext, LintRunner};
122
123use hedl_core::Document;
124
125#[must_use]
127pub fn lint(doc: &Document) -> Vec<Diagnostic> {
128 let runner = LintRunner::new(LintConfig::default());
129 runner.run(doc)
130}
131
132#[must_use]
134pub fn lint_with_config(doc: &Document, config: LintConfig) -> Vec<Diagnostic> {
135 let runner = LintRunner::new(config);
136 runner.run(doc)
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use hedl_core::{Item, MatrixList, Node, Reference, Value};
143
144 #[test]
145 fn test_lint_empty_document() {
146 let doc = Document::new((2, 0));
147 let diagnostics = lint(&doc);
148 assert_eq!(diagnostics.len(), 0);
149 }
150
151 #[test]
152 fn test_lint_short_ids() {
153 let mut doc = Document::new((2, 0));
154
155 let mut list = MatrixList::new("User", vec!["id".to_string(), "name".to_string()]);
157 let node1 = Node::new("User", "a", vec![Value::String("Alice".to_string().into())]);
158 let node2 = Node::new("User", "b", vec![Value::String("Bob".to_string().into())]);
159 list.add_row(node1);
160 list.add_row(node2);
161
162 doc.root.insert("users".to_string(), Item::List(list));
163
164 let diagnostics = lint(&doc);
165
166 let short_id_hints: Vec<_> = diagnostics
168 .iter()
169 .filter(|d| matches!(d.kind(), DiagnosticKind::IdNaming))
170 .collect();
171 assert_eq!(short_id_hints.len(), 2);
172 assert!(short_id_hints
173 .iter()
174 .all(|d| d.severity() == Severity::Hint));
175 }
176
177 #[test]
178 fn test_lint_numeric_ids() {
179 let mut doc = Document::new((2, 0));
180
181 let mut list = MatrixList::new("Item", vec!["id".to_string(), "value".to_string()]);
182 let node = Node::new("Item", "123", vec![Value::Int(100)]);
183 list.add_row(node);
184
185 doc.root.insert("items".to_string(), Item::List(list));
186
187 let diagnostics = lint(&doc);
188
189 let numeric_id_hints: Vec<_> = diagnostics
190 .iter()
191 .filter(|d| matches!(d.kind(), DiagnosticKind::IdNaming))
192 .collect();
193 assert!(!numeric_id_hints.is_empty());
194 }
195
196 #[test]
197 fn test_lint_unused_schema() {
198 let mut doc = Document::new((2, 0));
199
200 doc.structs.insert(
202 "UnusedType".to_string(),
203 vec!["id".to_string(), "name".to_string()],
204 );
205
206 doc.structs
208 .insert("UsedType".to_string(), vec!["id".to_string()]);
209 let mut list = MatrixList::new("UsedType", vec!["id".to_string()]);
210 list.add_row(Node::new("UsedType", "test", vec![]));
211 doc.root.insert("used".to_string(), Item::List(list));
212
213 let diagnostics = lint(&doc);
214
215 let unused_schema_warnings: Vec<_> = diagnostics
216 .iter()
217 .filter(|d| matches!(d.kind(), DiagnosticKind::UnusedSchema))
218 .collect();
219 assert_eq!(unused_schema_warnings.len(), 1);
220 assert_eq!(unused_schema_warnings[0].severity(), Severity::Warning);
221 }
222
223 #[test]
224 fn test_lint_empty_list() {
225 let mut doc = Document::new((2, 0));
226
227 let list = MatrixList::new("EmptyType", vec!["id".to_string()]);
228 doc.root.insert("empty_list".to_string(), Item::List(list));
229
230 let diagnostics = lint(&doc);
231
232 let empty_list_hints: Vec<_> = diagnostics
233 .iter()
234 .filter(|d| matches!(d.kind(), DiagnosticKind::EmptyList))
235 .collect();
236 assert_eq!(empty_list_hints.len(), 1);
237 assert_eq!(empty_list_hints[0].severity(), Severity::Hint);
238 }
239
240 #[test]
241 fn test_lint_unqualified_reference() {
242 let mut doc = Document::new((2, 0));
243
244 let ref_value = Value::Reference(Reference::local("some_id"));
246 doc.root
247 .insert("ref_field".to_string(), Item::Scalar(ref_value));
248
249 let diagnostics = lint(&doc);
250
251 let unqualified_ref_warnings: Vec<_> = diagnostics
252 .iter()
253 .filter(|d| matches!(d.kind(), DiagnosticKind::UnqualifiedKvReference))
254 .collect();
255 assert_eq!(unqualified_ref_warnings.len(), 1);
256 assert_eq!(unqualified_ref_warnings[0].severity(), Severity::Warning);
257 assert!(unqualified_ref_warnings[0].suggestion().is_some());
258 }
259
260 #[test]
261 fn test_lint_config_disable_rule() {
262 let mut doc = Document::new((2, 0));
263
264 let list = MatrixList::new("EmptyType", vec!["id".to_string()]);
265 doc.root.insert("empty_list".to_string(), Item::List(list));
266
267 let mut config = LintConfig::default();
269 config.disable_rule("empty-list");
270
271 let diagnostics = lint_with_config(&doc, config);
272
273 let empty_list_hints: Vec<_> = diagnostics
274 .iter()
275 .filter(|d| matches!(d.kind(), DiagnosticKind::EmptyList))
276 .collect();
277 assert_eq!(empty_list_hints.len(), 0);
278 }
279
280 #[test]
281 fn test_diagnostic_display() {
282 let diag = Diagnostic::warning(DiagnosticKind::UnusedSchema, "Test message", "test-rule");
283 let display = format!("{diag}");
284 assert!(display.contains("warning"));
285 assert!(display.contains("Test message"));
286 assert!(display.contains("test-rule"));
287 }
288
289 #[test]
290 fn test_diagnostic_with_line() {
291 let diag = Diagnostic::error(
292 DiagnosticKind::Custom("dup-key".to_string()),
293 "Duplicate key found",
294 "dup-key",
295 )
296 .with_line(42);
297
298 let display = format!("{diag}");
299 assert!(display.contains("line 42"));
300 }
301}