use super::*;
impl LintConfig {
pub fn validate(&self) -> Vec<ConfigWarning> {
let mut warnings = Vec::new();
let known_prefixes = [
"AS-",
"CC-SK-",
"CC-HK-",
"CC-AG-",
"CC-MEM-",
"CC-PL-",
"CDX-",
"XML-",
"MCP-",
"REF-",
"XP-",
"AGM-",
"COP-",
"CUR-",
"CLN-",
"OC-",
"GM-",
"PE-",
"VER-",
"ROO-",
"AMP-",
"WS-",
"WS-SK-",
"KIRO-",
"KR-SK-",
"imports::",
];
for rule_id in &self.data.rules.disabled_rules {
let matches_known = known_prefixes
.iter()
.any(|prefix| rule_id.starts_with(prefix));
if !matches_known {
warnings.push(ConfigWarning {
field: "rules.disabled_rules".to_string(),
message: t!(
"core.config.unknown_rule",
rule = rule_id.as_str(),
prefixes = known_prefixes.join(", ")
)
.to_string(),
suggestion: Some(t!("core.config.unknown_rule_suggestion").to_string()),
});
}
}
let known_tools = [
"claude-code",
"cursor",
"codex",
"kiro",
"copilot",
"github-copilot",
"cline",
"opencode",
"gemini-cli",
"amp",
"roo-code",
"windsurf",
"generic",
];
for tool in &self.data.tools {
let tool_lower = tool.to_lowercase();
if !known_tools
.iter()
.any(|k| k.eq_ignore_ascii_case(&tool_lower))
{
warnings.push(ConfigWarning {
field: "tools".to_string(),
message: t!(
"core.config.unknown_tool",
tool = tool.as_str(),
valid = known_tools.join(", ")
)
.to_string(),
suggestion: Some(t!("core.config.unknown_tool_suggestion").to_string()),
});
}
}
if self.data.target != TargetTool::Generic && self.data.tools.is_empty() {
warnings.push(ConfigWarning {
field: "target".to_string(),
message: t!("core.config.deprecated_target").to_string(),
suggestion: Some(t!("core.config.deprecated_target_suggestion").to_string()),
});
}
if self.data.mcp_protocol_version.is_some() {
warnings.push(ConfigWarning {
field: "mcp_protocol_version".to_string(),
message: t!("core.config.deprecated_mcp_version").to_string(),
suggestion: Some(t!("core.config.deprecated_mcp_version_suggestion").to_string()),
});
}
let pattern_lists = [
(
"files.include_as_memory",
&self.data.files.include_as_memory,
),
(
"files.include_as_generic",
&self.data.files.include_as_generic,
),
("files.exclude", &self.data.files.exclude),
];
for (field, patterns) in &pattern_lists {
if patterns.len() > MAX_FILE_PATTERNS {
warnings.push(ConfigWarning {
field: field.to_string(),
message: t!(
"core.config.files_pattern_count_limit",
field = *field,
count = patterns.len(),
limit = MAX_FILE_PATTERNS
)
.to_string(),
suggestion: Some(
t!("core.config.files_pattern_count_limit_suggestion").to_string(),
),
});
}
for pattern in *patterns {
let normalized = pattern.replace('\\', "/");
if let Err(e) = glob::Pattern::new(&normalized) {
warnings.push(ConfigWarning {
field: field.to_string(),
message: t!(
"core.config.invalid_files_pattern",
pattern = pattern.as_str(),
message = e.to_string()
)
.to_string(),
suggestion: Some(
t!("core.config.invalid_files_pattern_suggestion").to_string(),
),
});
}
if has_path_traversal(&normalized) {
warnings.push(ConfigWarning {
field: field.to_string(),
message: t!(
"core.config.files_path_traversal",
pattern = pattern.as_str()
)
.to_string(),
suggestion: Some(
t!("core.config.files_path_traversal_suggestion").to_string(),
),
});
}
if normalized.starts_with('/')
|| (normalized.len() >= 3
&& normalized.as_bytes()[0].is_ascii_alphabetic()
&& normalized.as_bytes().get(1..3) == Some(b":/"))
{
warnings.push(ConfigWarning {
field: field.to_string(),
message: t!(
"core.config.files_absolute_path",
pattern = pattern.as_str()
)
.to_string(),
suggestion: Some(
t!("core.config.files_absolute_path_suggestion").to_string(),
),
});
}
}
}
warnings
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigWarning {
pub field: String,
pub message: String,
pub suggestion: Option<String>,
}
pub fn generate_schema() -> schemars::Schema {
schemars::schema_for!(LintConfig)
}