use rpm_spec::ast::{PreambleItem, Span, Tag};
use crate::diagnostic::{Applicability, Diagnostic, LintCategory, Severity, Suggestion};
use crate::lint::{Lint, LintMetadata};
use crate::rules::util::drop_span;
use crate::visit::{self, Visit};
pub static METADATA: LintMetadata = LintMetadata {
id: "RPM020",
name: "obsolete-tag",
description: "Preamble uses a tag that's deprecated or forbidden by modern packaging guidelines.",
default_severity: Severity::Warn,
category: LintCategory::Packaging,
};
const OBSOLETE_OTHER_TAGS: &[(&str, &str)] = &[
(
"Copyright",
"Copyright was renamed to License in rpm 4.0 (2000-09)",
),
(
"Serial",
"Serial was replaced by Epoch in rpm 3.0 (1999-04)",
),
(
"PreReq",
"PreReq is deprecated since rpm 4.4 (2005-09); use Requires",
),
(
"BuildPreReq",
"BuildPreReq is deprecated since rpm 4.4 (2005-09); use BuildRequires",
),
];
#[derive(Debug, Default)]
pub struct ObsoleteTag {
diagnostics: Vec<Diagnostic>,
}
impl ObsoleteTag {
pub fn new() -> Self {
Self::default()
}
}
fn obsolete_reason(tag: &Tag) -> Option<&'static str> {
match tag {
Tag::BuildRoot => Some(
"BuildRoot is set automatically by rpm ≥ 4.6 (released 2009-02); \
every supported distribution ships a newer rpm",
),
Tag::Packager => Some(
"Packager is forbidden by the Fedora Packaging Guidelines \
(the field is set by the build system, not the spec)",
),
Tag::Vendor => Some(
"Vendor is forbidden by the Fedora Packaging Guidelines \
(the field is set by the build system, not the spec)",
),
Tag::Other(name) => OBSOLETE_OTHER_TAGS
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(name))
.map(|(_, msg)| *msg),
_ => None,
}
}
impl<'ast> Visit<'ast> for ObsoleteTag {
fn visit_preamble(&mut self, node: &'ast PreambleItem<Span>) {
if let Some(reason) = obsolete_reason(&node.tag) {
let diag = Diagnostic::new(
&METADATA,
Severity::Warn,
format!("{reason}; remove this line"),
node.data,
)
.with_suggestion(Suggestion::new(
"drop the obsolete tag",
vec![drop_span(node.data)],
Applicability::MachineApplicable,
));
self.diagnostics.push(diag);
}
visit::walk_preamble(self, node);
}
}
impl Lint for ObsoleteTag {
fn metadata(&self) -> &'static LintMetadata {
&METADATA
}
fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::parse;
fn run(src: &str) -> Vec<Diagnostic> {
let outcome = parse(src);
let mut lint = ObsoleteTag::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn flags_buildroot_tag() {
let diags = run("Name: x\nBuildRoot: %{_tmppath}/foo\n");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].lint_id, "RPM020");
assert!(diags[0].message.contains("BuildRoot"));
assert!(!diags[0].suggestions.is_empty());
}
#[test]
fn flags_packager_and_vendor() {
let diags = run("Name: x\nPackager: who\nVendor: us\n");
assert_eq!(diags.len(), 2);
assert!(diags.iter().all(|d| d.lint_id == "RPM020"));
}
#[test]
fn flags_legacy_copyright() {
let diags = run("Name: x\nCopyright: MIT\n");
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Copyright"));
}
#[test]
fn flags_legacy_serial() {
let diags = run("Name: x\nSerial: 1\n");
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("Serial"));
}
#[test]
fn flags_legacy_prereq() {
let diags = run("Name: x\nPreReq: bash\n");
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("PreReq"));
}
#[test]
fn flags_legacy_buildprereq() {
let diags = run("Name: x\nBuildPreReq: gcc\n");
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("BuildPreReq"));
}
#[test]
fn matches_case_insensitively() {
let diags = run("Name: x\ncopyright: MIT\n");
assert_eq!(diags.len(), 1, "lowercase 'copyright' should be flagged");
}
#[test]
fn silent_when_modern_tags_only() {
let diags = run("Name: x\nVersion: 1\nLicense: MIT\n");
assert!(diags.is_empty(), "{diags:?}");
}
}