use super::*;
pub(crate) fn apply_exclusions_section(
exclude_value: &Value,
config: &mut Config,
) -> Result<(), String> {
let registry = rules::builtin_registry();
let entries = exclude_value
.as_array()
.ok_or_else(|| "config key `exclude` must be an array".to_string())?;
let mut exclusions = Vec::new();
for (index, entry_value) in entries.iter().enumerate() {
exclusions.push(parse_exclusion_rule(
index,
entry_value,
®istry,
&config.custom_rules,
)?);
}
config.exclusions = exclusions;
Ok(())
}
pub(crate) fn parse_exclusion_rule(
index: usize,
entry_value: &Value,
registry: &rules::RuleRegistry,
custom_rules: &[CustomRule],
) -> Result<ExclusionRule, String> {
let entry_path = format!("exclude[{index}]");
let entry = entry_value
.as_object()
.ok_or_else(|| format!("config key `{entry_path}` must be an object"))?;
reject_unknown_keys(
entry,
&["rule", "paths", "message_contains", "reason"],
&format!("config key `{entry_path}`"),
)?;
let (selector, rule_ids) =
parse_exclusion_selector(entry, &entry_path, registry, custom_rules)?;
let paths = parse_exclusion_paths(entry, &entry_path)?;
let message_contains = parse_exclusion_message_filter(entry, &entry_path)?;
let reason = parse_exclusion_reason(entry, &entry_path)?;
Ok(ExclusionRule {
selector,
rule_ids,
paths,
message_contains,
reason,
})
}
fn parse_exclusion_selector(
entry: &serde_json::Map<String, Value>,
entry_path: &str,
registry: &rules::RuleRegistry,
custom_rules: &[CustomRule],
) -> Result<(String, BTreeSet<String>), String> {
let selector = required_config_string(entry, "rule", &format!("{entry_path}.rule"))?;
let rule_ids = expand_rule_selector_with_custom(
&selector,
registry,
custom_rules,
&format!("{entry_path}.rule"),
)?;
Ok((selector, rule_ids))
}
fn parse_exclusion_paths(
entry: &serde_json::Map<String, Value>,
entry_path: &str,
) -> Result<Vec<String>, String> {
Ok(entry
.get("paths")
.map(|value| {
string_array(value, &format!("{entry_path}.paths")).map(|paths| {
paths
.into_iter()
.map(|path| normalize_report_path(&path))
.collect()
})
})
.transpose()?
.unwrap_or_default())
}
fn parse_exclusion_message_filter(
entry: &serde_json::Map<String, Value>,
entry_path: &str,
) -> Result<Option<String>, String> {
let message_contains = entry
.get("message_contains")
.map(|value| {
value.as_str().map(str::to_string).ok_or_else(|| {
format!("config key `{entry_path}.message_contains` must be a string")
})
})
.transpose()?;
if message_contains
.as_deref()
.is_some_and(|message| message.is_empty())
{
return Err(format!(
"config key `{entry_path}.message_contains` must be a non-empty string"
));
}
Ok(message_contains)
}
fn parse_exclusion_reason(
entry: &serde_json::Map<String, Value>,
entry_path: &str,
) -> Result<String, String> {
let reason = required_config_string(entry, "reason", &format!("{entry_path}.reason"))?;
if reason.trim().is_empty() {
return Err(format!(
"config key `{entry_path}.reason` must be a non-empty string"
));
}
Ok(reason)
}
pub(crate) fn required_non_empty_config_string(
object: &serde_json::Map<String, Value>,
key: &str,
entry_path: &str,
) -> Result<String, String> {
let path = format!("{entry_path}.{key}");
let value = required_config_string(object, key, &path)?;
if value.trim().is_empty() {
return Err(format!("config key `{path}` must be a non-empty string"));
}
Ok(value)
}
pub(crate) fn required_config_string(
object: &serde_json::Map<String, Value>,
key: &str,
path: &str,
) -> Result<String, String> {
object
.get(key)
.ok_or_else(|| format!("missing required config key `{path}`"))?
.as_str()
.map(str::to_string)
.ok_or_else(|| format!("config key `{path}` must be a string"))
}