Skip to main content

agentlint_cursor/
lib.rs

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        // Frontmatter is optional — only validate when the opening fence is present.
18        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}