use crate::linter::diagnostic::{Diagnostic, ViolationData};
use crate::linter::rules::packaging::scalar_field::{escape, value};
use crate::linter::rules::{DcfRule, DcfRuleContext, Example};
use crate::semantic::symbols::base_priority_packages;
pub struct DescriptionMalformedName;
const EXAMPLES: &[Example] = &[
Example {
caption: "A name R's `valid_package_name` rejects, since underscores are not \
name characters:",
source: "Package: my_pkg\nVersion: 0.1.0\n",
},
Example {
caption: "A name R already ships:",
source: "Package: stats\nVersion: 0.1.0\n",
},
];
impl DcfRule for DescriptionMalformedName {
fn id(&self) -> &'static str {
"description-malformed-name"
}
fn description(&self) -> &'static str {
"Flag a `Package` value R will not accept as a package name.\n\nR's \
`valid_package_name` is `[[:alpha:]][[:alnum:].]*[[:alnum:]]`: a \
letter, then letters, digits, and periods, ending in a letter or \
digit. So underscores, hyphens, and a leading period are all out, and \
a name is at least two characters long—except the literal `R`, which \
R's check spells out as an alternative.\n\nA `Package` naming one of \
the packages R itself ships (`stats`, `utils`, `methods`, …) is \
reported too, since that package could never be installed alongside \
the one R ships. A description declaring `Priority: base` is exempt, \
which is how the base packages name themselves.\n\nThe letter and \
digit classes are matched as Unicode, exactly as R matches them under \
a UTF-8 locale, so `café` is accepted—a stricter reading would report \
a defect `R CMD check` does not have.\n\nAn absent or empty `Package` \
is `description-missing-field`'s finding, not this one's.\n\nThere is \
no autofix: the name is also in the NAMESPACE, the file names, the \
tests, and every `pkg::` that reaches the package, so renaming is the \
author's."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn check_file(&self, ctx: &DcfRuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(field) = ctx.document.field("Package") else {
return;
};
let Some((name, range)) = value(&field) else {
return;
};
let (message, suggestion) = if !is_valid_package_name(&name) {
(
format!(
"`{}` is not a valid package name: R requires a letter, then letters, \
digits, and periods, ending in a letter or digit",
escape(&name),
),
"Rename the package: at least two characters, starting with a letter, \
ending in a letter or digit, and made of letters, digits, and periods.",
)
} else if names_a_base_package(&name, ctx) {
(
format!("`{name}` is the name of a base R package"),
"Rename the package to one R does not already ship.",
)
} else {
return;
};
sink.push(Diagnostic {
rule: "description-malformed-name",
severity: Default::default(),
path: Default::default(),
range,
message: ViolationData::new("description-malformed-name", message)
.with_suggestion(suggestion.to_string()),
fix: None,
});
}
}
fn is_valid_package_name(name: &str) -> bool {
if name == "R" {
return true;
}
let mut middle = name.chars();
let (Some(first), Some(last)) = (middle.next(), middle.next_back()) else {
return false;
};
first.is_alphabetic()
&& last.is_alphanumeric()
&& middle.all(|c| c.is_alphanumeric() || c == '.')
}
fn names_a_base_package(name: &str, ctx: &DcfRuleContext<'_>) -> bool {
let declares_base_priority = ctx
.document
.field("Priority")
.is_some_and(|field| field.folded_value().trim() == "base");
!declares_base_priority && base_priority_packages().contains(&name)
}