Skip to main content

agnix_core/config/
schema.rs

1use super::*;
2
3impl LintConfig {
4    /// Validate the configuration and return any warnings.
5    ///
6    /// This performs semantic validation beyond what TOML parsing can check:
7    /// - Validates that disabled_rules match known rule ID patterns
8    /// - Validates that tools array contains known tool names
9    /// - Warns on deprecated fields
10    pub fn validate(&self) -> Vec<ConfigWarning> {
11        let mut warnings = Vec::new();
12
13        // Validate disabled_rules match known patterns
14        // Note: imports:: is a legacy prefix used in some internal diagnostics
15        let known_prefixes = [
16            "AS-",
17            "CC-SK-",
18            "CC-HK-",
19            "CC-AG-",
20            "CC-MEM-",
21            "CC-PL-",
22            "CDX-",
23            "XML-",
24            "MCP-",
25            "REF-",
26            "XP-",
27            "AGM-",
28            "COP-",
29            "CUR-",
30            "CLN-",
31            "OC-",
32            "GM-",
33            "PE-",
34            "VER-",
35            "ROO-",
36            "AMP-",
37            "WS-",
38            "WS-SK-",
39            "KIRO-",
40            "KR-SK-",
41            "imports::",
42        ];
43        for rule_id in &self.data.rules.disabled_rules {
44            let matches_known = known_prefixes
45                .iter()
46                .any(|prefix| rule_id.starts_with(prefix));
47            if !matches_known {
48                warnings.push(ConfigWarning {
49                    field: "rules.disabled_rules".to_string(),
50                    message: t!(
51                        "core.config.unknown_rule",
52                        rule = rule_id.as_str(),
53                        prefixes = known_prefixes.join(", ")
54                    )
55                    .to_string(),
56                    suggestion: Some(t!("core.config.unknown_rule_suggestion").to_string()),
57                });
58            }
59        }
60
61        // Validate tools array contains known tools
62        let known_tools = [
63            "claude-code",
64            "cursor",
65            "codex",
66            "kiro",
67            "copilot",
68            "github-copilot",
69            "cline",
70            "opencode",
71            "gemini-cli",
72            "amp",
73            "roo-code",
74            "windsurf",
75            "generic",
76        ];
77        for tool in &self.data.tools {
78            let tool_lower = tool.to_lowercase();
79            if !known_tools
80                .iter()
81                .any(|k| k.eq_ignore_ascii_case(&tool_lower))
82            {
83                warnings.push(ConfigWarning {
84                    field: "tools".to_string(),
85                    message: t!(
86                        "core.config.unknown_tool",
87                        tool = tool.as_str(),
88                        valid = known_tools.join(", ")
89                    )
90                    .to_string(),
91                    suggestion: Some(t!("core.config.unknown_tool_suggestion").to_string()),
92                });
93            }
94        }
95
96        // Warn on deprecated fields
97        if self.data.target != TargetTool::Generic && self.data.tools.is_empty() {
98            // Only warn if target is non-default and tools is empty
99            // (if both are set, tools takes precedence silently)
100            warnings.push(ConfigWarning {
101                field: "target".to_string(),
102                message: t!("core.config.deprecated_target").to_string(),
103                suggestion: Some(t!("core.config.deprecated_target_suggestion").to_string()),
104            });
105        }
106        if self.data.mcp_protocol_version.is_some() {
107            warnings.push(ConfigWarning {
108                field: "mcp_protocol_version".to_string(),
109                message: t!("core.config.deprecated_mcp_version").to_string(),
110                suggestion: Some(t!("core.config.deprecated_mcp_version_suggestion").to_string()),
111            });
112        }
113
114        // Validate files config glob patterns
115        let pattern_lists = [
116            (
117                "files.include_as_memory",
118                &self.data.files.include_as_memory,
119            ),
120            (
121                "files.include_as_generic",
122                &self.data.files.include_as_generic,
123            ),
124            ("files.exclude", &self.data.files.exclude),
125        ];
126        for (field, patterns) in &pattern_lists {
127            // Warn if pattern count exceeds recommended limit
128            if patterns.len() > MAX_FILE_PATTERNS {
129                warnings.push(ConfigWarning {
130                    field: field.to_string(),
131                    message: t!(
132                        "core.config.files_pattern_count_limit",
133                        field = *field,
134                        count = patterns.len(),
135                        limit = MAX_FILE_PATTERNS
136                    )
137                    .to_string(),
138                    suggestion: Some(
139                        t!("core.config.files_pattern_count_limit_suggestion").to_string(),
140                    ),
141                });
142            }
143            for pattern in *patterns {
144                let normalized = pattern.replace('\\', "/");
145                if let Err(e) = glob::Pattern::new(&normalized) {
146                    warnings.push(ConfigWarning {
147                        field: field.to_string(),
148                        message: t!(
149                            "core.config.invalid_files_pattern",
150                            pattern = pattern.as_str(),
151                            message = e.to_string()
152                        )
153                        .to_string(),
154                        suggestion: Some(
155                            t!("core.config.invalid_files_pattern_suggestion").to_string(),
156                        ),
157                    });
158                }
159                // Reject path traversal patterns
160                if has_path_traversal(&normalized) {
161                    warnings.push(ConfigWarning {
162                        field: field.to_string(),
163                        message: t!(
164                            "core.config.files_path_traversal",
165                            pattern = pattern.as_str()
166                        )
167                        .to_string(),
168                        suggestion: Some(
169                            t!("core.config.files_path_traversal_suggestion").to_string(),
170                        ),
171                    });
172                }
173                // Reject absolute paths (Unix-style leading slash or Windows drive letter)
174                if normalized.starts_with('/')
175                    || (normalized.len() >= 3
176                        && normalized.as_bytes()[0].is_ascii_alphabetic()
177                        && normalized.as_bytes().get(1..3) == Some(b":/"))
178                {
179                    warnings.push(ConfigWarning {
180                        field: field.to_string(),
181                        message: t!(
182                            "core.config.files_absolute_path",
183                            pattern = pattern.as_str()
184                        )
185                        .to_string(),
186                        suggestion: Some(
187                            t!("core.config.files_absolute_path_suggestion").to_string(),
188                        ),
189                    });
190                }
191            }
192        }
193
194        warnings
195    }
196}
197
198/// Warning from configuration validation.
199///
200/// These warnings indicate potential issues with the configuration that
201/// don't prevent validation from running but may indicate user mistakes.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct ConfigWarning {
204    /// The field path that has the issue (e.g., "rules.disabled_rules")
205    pub field: String,
206    /// Description of the issue
207    pub message: String,
208    /// Optional suggestion for how to fix the issue
209    pub suggestion: Option<String>,
210}
211
212/// Generate a JSON Schema for the LintConfig type.
213///
214/// This can be used to provide editor autocompletion and validation
215/// for `.agnix.toml` configuration files.
216///
217/// # Example
218///
219/// ```rust
220/// use agnix_core::config::generate_schema;
221///
222/// let schema = generate_schema();
223/// let json = serde_json::to_string_pretty(&schema).unwrap();
224/// println!("{}", json);
225/// ```
226pub fn generate_schema() -> schemars::Schema {
227    schemars::schema_for!(LintConfig)
228}