use rpm_spec::ast::{BuildScriptKind, Section, Span, SpecFile, SpecItem};
use crate::diagnostic::{Diagnostic, LintCategory, Severity};
use crate::lint::{Lint, LintMetadata};
use crate::visit::Visit;
pub static METADATA: LintMetadata = LintMetadata {
id: "RPM023",
name: "duplicate-buildscript-section",
description: "Spec declares the same build-script section (%prep/%build/%install/...) more than once.",
default_severity: Severity::Deny,
category: LintCategory::Packaging,
};
#[derive(Debug, Default)]
pub struct DuplicateBuildscriptSection {
diagnostics: Vec<Diagnostic>,
}
impl DuplicateBuildscriptSection {
pub fn new() -> Self {
Self::default()
}
}
impl<'ast> Visit<'ast> for DuplicateBuildscriptSection {
fn visit_spec(&mut self, spec: &'ast SpecFile<Span>) {
let mut seen: Vec<(BuildScriptKind, Span)> = Vec::new();
for item in &spec.items {
let SpecItem::Section(boxed) = item else {
continue;
};
let Section::BuildScript { kind, data, .. } = boxed.as_ref() else {
continue;
};
if let Some(&(_, first)) = seen.iter().find(|(k, _)| k == kind) {
self.diagnostics.push(
Diagnostic::new(
&METADATA,
Severity::Deny,
format!(
"duplicate {} section; rpm honours only one body",
section_keyword(*kind)
),
*data,
)
.with_label(first, "first declaration here"),
);
} else {
seen.push((*kind, *data));
}
}
}
}
fn section_keyword(kind: BuildScriptKind) -> &'static str {
match kind {
BuildScriptKind::Prep => "%prep",
BuildScriptKind::Conf => "%conf",
BuildScriptKind::Build => "%build",
BuildScriptKind::Install => "%install",
BuildScriptKind::Check => "%check",
BuildScriptKind::Clean => "%clean",
BuildScriptKind::GenerateBuildRequires => "%generate_buildrequires",
_ => "%<unknown>",
}
}
impl Lint for DuplicateBuildscriptSection {
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 = DuplicateBuildscriptSection::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn flags_two_build_sections() {
let src = "Name: x\n%build\nmake\n%build\nmake clean\n";
let diags = run(src);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].lint_id, "RPM023");
assert!(diags[0].message.contains("%build"));
assert_eq!(diags[0].labels.len(), 1);
}
#[test]
fn flags_two_prep_sections() {
let src = "Name: x\n%prep\n%setup -q\n%prep\n%setup -q\n";
let diags = run(src);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("%prep"));
}
#[test]
fn silent_when_each_kind_appears_once() {
let src = "Name: x\n%prep\n%build\n%install\n%check\n";
assert!(run(src).is_empty());
}
#[test]
fn distinct_kinds_do_not_collide() {
let src = "Name: x\n%build\nmake\n%install\nmake install\n";
assert!(run(src).is_empty());
}
#[test]
fn flags_two_install_sections() {
let src = "Name: x\n%install\ncp a b\n%install\ncp c d\n";
let diags = run(src);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("%install"));
}
}