use crate::linter::diagnostic::{Diagnostic, ViolationData};
use crate::linter::rules::{DcfRule, DcfRuleContext, Example};
pub struct DescriptionMissingField;
const REQUIRED: [&str; 7] = [
"Package",
"Version",
"Title",
"Description",
"Author",
"Maintainer",
"License",
];
const DERIVED_FROM_AUTHORS_AT_R: [&str; 2] = ["Author", "Maintainer"];
const EXAMPLES: &[Example] = &[Example {
caption: "A `DESCRIPTION` that `R CMD build` would reject:",
source: "Package: mypkg\nVersion: 0.1.0\n",
}];
impl DcfRule for DescriptionMissingField {
fn id(&self) -> &'static str {
"description-missing-field"
}
fn description(&self) -> &'static str {
"Flag a `DESCRIPTION` missing a field R requires.\n\n`R CMD build` \
refuses a package whose `DESCRIPTION` omits `Package`, `Version`, \
`Title`, `Description`, `Author`, `Maintainer`, or `License`. \
`Authors@R` satisfies `Author` and `Maintainer`, since `R CMD build` \
derives both from it. A field that is present but empty declares \
nothing and counts as missing.\n\nEvery missing field is reported as \
one finding rather than one each: the defect is that the file is \
incomplete, and it takes one decision—and one suppression—to \
settle.\n\nA file with no fields at all is left alone; that is not an \
incomplete package description.\n\nThere is no autofix: each of these \
fields needs a value only the author has."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn check_file(&self, ctx: &DcfRuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let declared = |name: &str| {
ctx.document
.field(name)
.is_some_and(|field| !field.folded_value().trim().is_empty())
};
let Some(first) = ctx.document.fields().find(|f| !f.name().is_empty()) else {
return;
};
let has_authors_at_r = declared("Authors@R");
let missing: Vec<&str> = REQUIRED
.into_iter()
.filter(|name| {
if has_authors_at_r && DERIVED_FROM_AUTHORS_AT_R.contains(name) {
return false;
}
!declared(name)
})
.collect();
if missing.is_empty() {
return;
}
let list = missing
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<_>>()
.join(", ");
let plural = if missing.len() == 1 {
"field"
} else {
"fields"
};
sink.push(Diagnostic {
rule: "description-missing-field",
severity: Default::default(),
path: Default::default(),
range: first.name_range(),
message: ViolationData::new(
"description-missing-field",
format!("DESCRIPTION is missing the required {plural} {list}"),
)
.with_suggestion(if missing.iter().any(|name| {
DERIVED_FROM_AUTHORS_AT_R.contains(name)
}) {
format!("Add the {plural} {list}, or declare `Authors@R` in place of `Author` and `Maintainer`.")
} else {
format!("Add the {plural} {list}.")
}),
fix: None,
});
}
}