use crate::linguist_data::{get_aliases, is_valid_alias, resolve_canonical};
use crate::rule_config_serde::RuleConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LanguageStyle {
#[default]
Disabled,
Consistent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum UnknownLanguageAction {
#[default]
Ignore,
Warn,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub struct MD040Config {
#[serde(default)]
pub style: LanguageStyle,
#[serde(default, alias = "preferred_aliases")]
pub preferred_aliases: HashMap<String, String>,
#[serde(default, alias = "allowed_languages")]
pub allowed_languages: Vec<String>,
#[serde(default, alias = "disallowed_languages")]
pub disallowed_languages: Vec<String>,
#[serde(default, alias = "unknown_language_action")]
pub unknown_language_action: UnknownLanguageAction,
#[serde(default)]
pub custom_languages: Vec<String>,
}
impl MD040Config {
pub fn custom_language(&self, label: &str) -> Option<&str> {
self.custom_languages
.iter()
.find(|declared| declared.eq_ignore_ascii_case(label))
.map(String::as_str)
}
pub fn preferred_alias_problem(&self, language: &str, alias: &str) -> Option<String> {
if let Some(canonical) = resolve_canonical(language) {
if is_valid_alias(canonical, alias) {
return None;
}
let examples = get_aliases(canonical).map_or_else(String::new, |valid_aliases| {
let valid_str = valid_aliases
.iter()
.take(5)
.map(|s| format!("'{s}'"))
.collect::<Vec<_>>()
.join(", ");
let suffix = if valid_aliases.len() > 5 { ", ..." } else { "" };
format!(" Valid aliases include: {valid_str}{suffix}")
});
return Some(format!("Invalid alias '{alias}' for language '{canonical}'.{examples}"));
}
let Some(declared) = self.custom_language(language) else {
return Some(format!(
"Unknown language '{language}' in preferred-aliases. Use GitHub Linguist canonical names, or declare it in custom-languages."
));
};
if alias.eq_ignore_ascii_case(declared) {
return None;
}
Some(format!(
"Invalid alias '{alias}' for custom language '{declared}'. A custom language has no aliases, so only a spelling of '{declared}' can be preferred."
))
}
pub fn preferred_label(&self, language: &str) -> Option<&str> {
self.preferred_aliases
.iter()
.find(|(configured, _)| configured.eq_ignore_ascii_case(language))
.map(|(_, alias)| alias.as_str())
.filter(|alias| self.preferred_alias_problem(language, alias).is_none())
}
}
impl RuleConfig for MD040Config {
const RULE_NAME: &'static str = "MD040";
}