ailint_core/rules/structural/
malformed_yaml.rs1use crate::file_type::FileType;
6use crate::parser::{DocumentContent, ParsedDocument};
7use crate::rules::structural::AIL041;
8use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
9
10#[derive(Debug, Default)]
12pub struct MalformedYamlRule;
13
14impl Rule for MalformedYamlRule {
15 fn id(&self) -> RuleId {
16 AIL041
17 }
18
19 fn default_severity(&self) -> Severity {
20 Severity::Error
21 }
22
23 fn description(&self) -> &'static str {
24 "YAML file failed to parse."
25 }
26
27 fn fix_hint(&self) -> &'static str {
28 "Fix the YAML syntax error at the reported location."
29 }
30
31 fn applies_to(&self, file_type: FileType) -> bool {
33 matches!(
34 file_type,
35 FileType::GenericYaml
36 | FileType::GenericSystemPrompt
37 | FileType::CursorRules
38 | FileType::WindsurfRules
39 | FileType::ClineRules
40 | FileType::ContinueRules
41 )
42 }
43
44 fn run(&self, doc: &ParsedDocument, _ctx: &RuleContext<'_>) -> Vec<Violation> {
45 let msg = match &doc.content {
46 DocumentContent::ParseError(m) => m,
47 _ => return Vec::new(),
48 };
49 let (line, column) = extract_location(msg).unwrap_or((1, 1));
50 let v = Violation::new(
51 AIL041,
52 self.default_severity(),
53 doc.path.clone(),
54 "YAML/JSON parse error",
55 )
56 .at(line, column)
57 .with_detail(msg.clone());
58 vec![v]
59 }
60}
61
62fn extract_location(msg: &str) -> Option<(usize, usize)> {
64 let lower = msg.to_ascii_lowercase();
65 let line = extract_after(&lower, "line ").or_else(|| extract_after(&lower, "line: "))?;
66 let col = extract_after(&lower, "column ")
67 .or_else(|| extract_after(&lower, "column: "))
68 .unwrap_or(1);
69 Some((line, col))
70}
71
72fn extract_after(haystack: &str, needle: &str) -> Option<usize> {
73 let idx = haystack.find(needle)?;
74 let rest = &haystack[idx + needle.len()..];
75 let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
76 digits.parse().ok()
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn extracts_serde_yaml_style_location() {
85 assert_eq!(
86 extract_location("mapping values are not allowed in this context at line 3 column 5"),
87 Some((3, 5))
88 );
89 }
90
91 #[test]
92 fn falls_back_when_no_location() {
93 assert_eq!(extract_location("some other error"), None);
94 }
95}