use std::collections::HashMap;
use crate::linter::diagnostic::{Diagnostic, ViolationData};
use crate::linter::rules::{DcfRule, DcfRuleContext, Example};
pub struct DescriptionDuplicateField;
const EXAMPLES: &[Example] = &[Example {
caption: "A field declared twice, which arity and `read.dcf` read differently:",
source: "Package: mypkg\nVersion: 0.1.0\nLicense: MIT + file LICENSE\nVersion: 0.2.0\n",
}];
impl DcfRule for DescriptionDuplicateField {
fn id(&self) -> &'static str {
"description-duplicate-field"
}
fn description(&self) -> &'static str {
"Flag a `DESCRIPTION` field declared more than once.\n\nA repeated \
field is a silent mistake: nothing errors, and the file keeps \
whichever value the reader picks—except the readers disagree. R's \
`read.dcf` takes the **last** occurrence; arity takes the **first**. A \
duplicated `Version` therefore means R and arity are describing two \
different packages, and every tool downstream of either inherits the \
split.\n\nThe finding is reported on the *later* occurrence, since the \
earlier one is what arity already read. Duplicates are detected across \
DCF records, so a stray blank line does not hide one.\n\nThere is no \
autofix: removing a duplicate means choosing a value, and this rule \
exists precisely because it is not settled which value is already in \
effect."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn check_file(&self, ctx: &DcfRuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let text = ctx.root.text().to_string();
let mut first_line: HashMap<String, u32> = HashMap::new();
for field in ctx.document.fields() {
let name = field.name();
if name.is_empty() {
continue;
}
let Some(&first) = first_line.get(name.as_str()) else {
first_line.insert(name.to_string(), line_of(&text, field.name_range().start()));
continue;
};
sink.push(Diagnostic {
rule: "description-duplicate-field",
severity: Default::default(),
path: Default::default(),
range: field.name_range(),
message: ViolationData::new(
"description-duplicate-field",
format!(
"`{name}` is already declared on line {first}; arity reads the \
first occurrence and R's `read.dcf` reads the last, so the two \
disagree about this file"
),
)
.with_suggestion(format!("Keep one `{name}` field and delete the other.")),
fix: None,
});
}
}
}
fn line_of(text: &str, offset: rowan::TextSize) -> u32 {
let at: usize = offset.into();
1 + text[..at.min(text.len())].matches('\n').count() as u32
}