1use agentlint_core::{Diagnostic, Validator};
2use agentlint_frontmatter::{ParseError, parse};
3use std::path::Path;
4
5pub struct CursorValidator;
6
7impl Validator for CursorValidator {
8 fn patterns(&self) -> &[&str] {
9 &[
10 ".cursor/rules/**/*.mdc",
11 ".cursor/rules/**/*.md",
12 ".cursorrules",
13 ]
14 }
15
16 fn validate(&self, path: &Path, src: &str) -> Vec<Diagnostic> {
17 if !src.starts_with("---\n") && !src.starts_with("---\r\n") {
19 return vec![];
20 }
21 match parse(src) {
22 Ok(_) => vec![],
23 Err(ParseError::UnclosedFence) => vec![Diagnostic::error(
24 path,
25 1,
26 1,
27 "unclosed frontmatter fence: missing closing '---'",
28 )],
29 Err(ParseError::NoFence) => vec![],
30 }
31 }
32}
33
34#[cfg(test)]
35mod tests {
36 use super::*;
37 use std::path::Path;
38
39 fn v() -> CursorValidator {
40 CursorValidator
41 }
42
43 #[test]
44 fn no_frontmatter_is_clean() {
45 let diags = v().validate(Path::new("rule.md"), "# Hello\nsome content\n");
46 assert!(diags.is_empty());
47 }
48
49 #[test]
50 fn well_formed_frontmatter_is_clean() {
51 let src = "---\ntitle: test\ndescription: lint files\n---\n# Body\n";
52 let diags = v().validate(Path::new("rule.mdc"), src);
53 assert!(diags.is_empty());
54 }
55
56 #[test]
57 fn unclosed_fence_is_error() {
58 let src = "---\ntitle: test\n# no closing fence\n";
59 let diags = v().validate(Path::new("rule.mdc"), src);
60 assert_eq!(diags.len(), 1);
61 assert!(
62 diags[0].message.contains("unclosed"),
63 "unexpected message: {}",
64 diags[0].message
65 );
66 }
67}