use std::collections::BTreeMap;
use rpm_spec::ast::{
BoolDep, BuildCondStyle, BuildCondition, CondExpr, Conditional, DepExpr, ExprAst, MacroRef,
PreambleContent, PreambleItem, Section, Span, SpecFile, SpecItem, TagValue, Text, TextSegment,
};
use crate::diagnostic::{Diagnostic, LintCategory, Severity};
use crate::lint::{Lint, LintMetadata};
use crate::visit::Visit;
const WITH_NAMES: &[&str] = &["with", "without"];
pub static DEFINED_BUT_UNUSED_METADATA: LintMetadata = LintMetadata {
id: "RPM401",
name: "bcond-defined-but-unused",
description: "A `%bcond` / `%bcond_with` / `%bcond_without` declaration has no matching \
`%{with name}` / `%{without name}` reference anywhere in the spec. The toggle \
has no effect; remove it or wire it up.",
default_severity: Severity::Warn,
category: LintCategory::Style,
};
#[derive(Debug, Default)]
pub struct BcondDefinedButUnused {
diagnostics: Vec<Diagnostic>,
}
impl BcondDefinedButUnused {
pub fn new() -> Self {
Self::default()
}
}
impl<'ast> Visit<'ast> for BcondDefinedButUnused {
fn visit_spec(&mut self, spec: &'ast SpecFile<Span>) {
let usage = BcondUsage::collect(spec);
for (name, decl) in &usage.declared {
if usage.referenced.contains_key(name.as_str()) {
continue;
}
self.diagnostics.push(Diagnostic::new(
&DEFINED_BUT_UNUSED_METADATA,
Severity::Warn,
format!(
"bcond `{name}` is declared but never referenced via `%{{with {name}}}` or \
`%{{without {name}}}`; the toggle has no effect"
),
decl.span,
));
}
}
}
impl Lint for BcondDefinedButUnused {
fn metadata(&self) -> &'static LintMetadata {
&DEFINED_BUT_UNUSED_METADATA
}
fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
}
pub static REF_WITHOUT_BCOND_METADATA: LintMetadata = LintMetadata {
id: "RPM402",
name: "with-condition-without-bcond",
description: "A `%{with name}` or `%{without name}` reference has no matching `%bcond` \
declaration. RPM expands the reference to nothing, so the conditional silently \
never fires — declare the bcond or fix the typo.",
default_severity: Severity::Warn,
category: LintCategory::Correctness,
};
#[derive(Debug, Default)]
pub struct WithConditionWithoutBcond {
diagnostics: Vec<Diagnostic>,
}
impl WithConditionWithoutBcond {
pub fn new() -> Self {
Self::default()
}
}
impl<'ast> Visit<'ast> for WithConditionWithoutBcond {
fn visit_spec(&mut self, spec: &'ast SpecFile<Span>) {
let usage = BcondUsage::collect(spec);
for (name, reference) in &usage.referenced {
if usage.declared.contains_key(name) {
continue;
}
self.diagnostics.push(Diagnostic::new(
&REF_WITHOUT_BCOND_METADATA,
Severity::Warn,
format!(
"reference to `%{{with {name}}}` / `%{{without {name}}}` without a matching \
`%bcond_with {name}` / `%bcond_without {name}` declaration; RPM expands \
it to nothing — the branch silently never fires"
),
reference.span,
));
}
}
}
impl Lint for WithConditionWithoutBcond {
fn metadata(&self) -> &'static LintMetadata {
&REF_WITHOUT_BCOND_METADATA
}
fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BcondPolarity {
With,
Without,
Modern,
}
impl BcondPolarity {
fn from_style(style: BuildCondStyle) -> Self {
match style {
BuildCondStyle::BcondWith => Self::With,
BuildCondStyle::BcondWithout => Self::Without,
BuildCondStyle::Bcond => Self::Modern,
_ => Self::Modern,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WithKind {
With,
Without,
}
impl WithKind {
fn from_head(head: &str) -> Option<Self> {
match head {
"with" => Some(Self::With),
"without" => Some(Self::Without),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
struct BcondDecl {
#[allow(dead_code)] polarity: BcondPolarity,
span: Span,
}
#[derive(Debug, Clone, Copy)]
struct BcondRef {
#[allow(dead_code)] kind: WithKind,
span: Span,
}
#[derive(Debug, Default)]
struct BcondUsage {
declared: BTreeMap<String, BcondDecl>,
referenced: BTreeMap<String, BcondRef>,
}
impl BcondUsage {
fn collect(spec: &SpecFile<Span>) -> Self {
let mut out = BcondUsage::default();
walk_items(&spec.items, &mut out);
out
}
fn record_ref(&mut self, name: String, kind: WithKind, span: Span) {
self.referenced
.entry(name)
.or_insert(BcondRef { kind, span });
}
}
fn walk_items(items: &[SpecItem<Span>], out: &mut BcondUsage) {
for item in items {
match item {
SpecItem::BuildCondition(b) => record_declaration(out, b),
SpecItem::Preamble(p) => walk_preamble_item(p, out),
SpecItem::Conditional(c) => walk_top_cond(c, out),
SpecItem::Section(boxed) => walk_section(boxed.as_ref(), out),
SpecItem::MacroDef(m) => walk_text_for_refs(&m.body, m.data, out),
SpecItem::Statement(m) => {
walk_macro_ref(m, out, items_anchor(items).unwrap_or_default())
}
_ => {}
}
}
}
fn walk_top_cond(cond: &Conditional<Span, SpecItem<Span>>, out: &mut BcondUsage) {
for branch in &cond.branches {
walk_cond_expr(&branch.expr, cond.data, out);
walk_items(&branch.body, out);
}
if let Some(els) = &cond.otherwise {
walk_items(els, out);
}
}
fn walk_cond_expr(expr: &CondExpr<Span>, anchor: Span, out: &mut BcondUsage) {
match expr {
CondExpr::Raw(t) => walk_text_for_refs(t, anchor, out),
CondExpr::Parsed(ast) => walk_expr_ast(ast, out),
CondExpr::ArchList(items) => {
for t in items {
walk_text_for_refs(t, anchor, out);
}
}
_ => {}
}
}
fn walk_expr_ast(ast: &ExprAst<Span>, out: &mut BcondUsage) {
match ast {
ExprAst::Macro { text, data } => {
if let Some((kind, name)) = parse_with_without_from_text(text) {
out.record_ref(name.to_owned(), kind, *data);
}
}
ExprAst::Paren { inner, .. } | ExprAst::Not { inner, .. } => {
walk_expr_ast(inner, out);
}
ExprAst::Binary { lhs, rhs, .. } => {
walk_expr_ast(lhs, out);
walk_expr_ast(rhs, out);
}
_ => {}
}
}
fn parse_with_without_from_text(s: &str) -> Option<(WithKind, &str)> {
let body = s.strip_prefix("%{")?.strip_suffix('}')?;
let body = body.trim_start_matches(['?', '!']);
let mut parts = body.split_whitespace();
let head = parts.next()?;
if !WITH_NAMES.contains(&head) {
return None;
}
let kind = WithKind::from_head(head)?;
let name = parts.next()?;
if name.is_empty() {
return None;
}
Some((kind, name))
}
fn walk_section(section: &Section<Span>, out: &mut BcondUsage) {
if let Section::Package { content, .. } = section {
walk_preamble_content(content, out);
}
}
fn walk_preamble_content(items: &[PreambleContent<Span>], out: &mut BcondUsage) {
for item in items {
match item {
PreambleContent::Item(p) => walk_preamble_item(p, out),
PreambleContent::Conditional(c) => {
for branch in &c.branches {
walk_cond_expr(&branch.expr, c.data, out);
walk_preamble_content(&branch.body, out);
}
if let Some(els) = &c.otherwise {
walk_preamble_content(els, out);
}
}
_ => {}
}
}
}
fn walk_preamble_item(p: &PreambleItem<Span>, out: &mut BcondUsage) {
let anchor = p.data;
match &p.value {
TagValue::Text(t) => walk_text_for_refs(t, anchor, out),
TagValue::ArchList(items) => {
for t in items {
walk_text_for_refs(t, anchor, out);
}
}
TagValue::Dep(dep) => walk_dep_expr(dep, anchor, out),
_ => {}
}
}
fn walk_dep_expr(dep: &DepExpr, anchor: Span, out: &mut BcondUsage) {
match dep {
DepExpr::Atom(atom) => {
walk_text_for_refs(&atom.name, anchor, out);
if let Some(arch) = &atom.arch {
walk_text_for_refs(arch, anchor, out);
}
if let Some(c) = &atom.constraint {
walk_text_for_refs(&c.evr.version, anchor, out);
if let Some(rel) = &c.evr.release {
walk_text_for_refs(rel, anchor, out);
}
}
}
DepExpr::Rich(bool_dep) => walk_bool_dep(bool_dep, anchor, out),
_ => {}
}
}
fn walk_bool_dep(node: &BoolDep, anchor: Span, out: &mut BcondUsage) {
match node {
BoolDep::And(children) | BoolDep::Or(children) | BoolDep::With(children) => {
for d in children {
walk_dep_expr(d, anchor, out);
}
}
BoolDep::If {
cond,
then,
otherwise,
}
| BoolDep::Unless {
cond,
then,
otherwise,
} => {
walk_dep_expr(cond, anchor, out);
walk_dep_expr(then, anchor, out);
if let Some(o) = otherwise {
walk_dep_expr(o, anchor, out);
}
}
BoolDep::Without { left, right } => {
walk_dep_expr(left, anchor, out);
walk_dep_expr(right, anchor, out);
}
_ => {}
}
}
fn walk_text_for_refs(text: &Text, anchor: Span, out: &mut BcondUsage) {
for seg in &text.segments {
if let TextSegment::Macro(m) = seg {
walk_macro_ref(m, out, anchor);
}
}
}
fn walk_macro_ref(m: &MacroRef, out: &mut BcondUsage, anchor: Span) {
if let Some((kind, name)) = with_or_without_target(m) {
out.record_ref(name, kind, anchor);
}
for arg in &m.args {
walk_text_for_refs(arg, anchor, out);
}
if let Some(t) = &m.with_value {
walk_text_for_refs(t, anchor, out);
}
}
fn items_anchor(items: &[SpecItem<Span>]) -> Option<Span> {
for item in items {
if let SpecItem::Preamble(p) = item {
return Some(p.data);
}
}
None
}
fn record_declaration(out: &mut BcondUsage, node: &BuildCondition<Span>) {
let polarity = BcondPolarity::from_style(node.style);
out.declared.insert(
node.name.clone(),
BcondDecl {
polarity,
span: node.data,
},
);
if let Some(default) = &node.default {
walk_text_for_refs(default, node.data, out);
}
}
fn with_or_without_target(m: &MacroRef) -> Option<(WithKind, String)> {
if !WITH_NAMES.contains(&m.name.as_str()) {
return None;
}
let kind = WithKind::from_head(m.name.as_str())?;
let arg = m.args.first()?;
let lit = arg.literal_str()?;
let trimmed = lit.trim();
if trimmed.is_empty() {
return None;
}
Some((kind, trimmed.to_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::parse;
fn run_401(src: &str) -> Vec<Diagnostic> {
let outcome = parse(src);
let mut lint = BcondDefinedButUnused::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
fn run_402(src: &str) -> Vec<Diagnostic> {
let outcome = parse(src);
let mut lint = WithConditionWithoutBcond::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn rpm401_flags_unused_bcond_with() {
let src = "%bcond_with foo\nName: x\n";
let diags = run_401(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].lint_id, "RPM401");
assert!(diags[0].message.contains("foo"));
}
#[test]
fn rpm401_flags_unused_bcond_without() {
let src = "%bcond_without bar\nName: x\n";
assert_eq!(run_401(src).len(), 1);
}
#[test]
fn rpm401_silent_when_bcond_used_with_macro() {
let src = "%bcond_with foo\nName: x\n\
%if %{with foo}\nBuildRequires: extra\n%endif\n";
assert!(run_401(src).is_empty());
}
#[test]
fn rpm401_silent_when_bcond_used_without_macro() {
let src = "%bcond_without bar\nName: x\n\
%if %{without bar}\nBuildRequires: alt\n%endif\n";
assert!(run_401(src).is_empty());
}
#[test]
fn rpm401_flags_one_of_two_when_only_one_referenced() {
let src = "%bcond_with used\n%bcond_with unused\nName: x\n\
%if %{with used}\nBuildRequires: a\n%endif\n";
let diags = run_401(src);
assert_eq!(diags.len(), 1);
assert!(diags[0].message.contains("unused"));
}
#[test]
fn rpm401_silent_for_modern_bcond_form() {
let src = "%bcond newcond 1\nName: x\n\
%if %{with newcond}\nRequires: a\n%endif\n";
assert!(run_401(src).is_empty());
}
#[test]
fn rpm401_silent_when_bcond_used_via_optional_with() {
let src = "%bcond_with foo\nName: x\n\
%if %{?with foo}\nBuildRequires: extra\n%endif\n";
assert!(run_401(src).is_empty(), "{:?}", run_401(src));
}
#[test]
fn rpm401_silent_when_bcond_declared_in_subpackage_used_in_main() {
let src = "%bcond_with foo\n\
Name: x\nVersion: 1\nRelease: 1\nSummary: s\nLicense: MIT\n\
%description\nmain\n\
%package devel\nSummary: d\n\
%if %{with foo}\nRequires: foo-extras\n%endif\n\
%description devel\ndevel\n";
assert!(run_401(src).is_empty(), "{:?}", run_401(src));
}
#[test]
fn rpm402_flags_with_without_bcond() {
let src = "Name: x\n%if %{with foo}\nBuildRequires: a\n%endif\n";
let diags = run_402(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].lint_id, "RPM402");
assert!(diags[0].message.contains("foo"));
}
#[test]
fn rpm402_flags_without_without_bcond() {
let src = "Name: x\n%if %{without bar}\nBuildRequires: a\n%endif\n";
assert_eq!(run_402(src).len(), 1);
}
#[test]
fn rpm402_silent_when_bcond_declared() {
let src = "%bcond_with foo\nName: x\n%if %{with foo}\nBuildRequires: a\n%endif\n";
assert!(run_402(src).is_empty());
}
#[test]
fn rpm402_deduplicates_repeated_references() {
let src = "Name: x\n\
%if %{with foo}\nBuildRequires: a\n%endif\n\
%if %{with foo}\nBuildRequires: b\n%endif\n";
assert_eq!(run_402(src).len(), 1);
}
#[test]
fn rpm402_silent_when_bcond_modern_form_declared() {
let src = "%bcond foo 1\nName: x\n%if %{with foo}\nBuildRequires: a\n%endif\n";
assert!(run_402(src).is_empty());
}
#[test]
fn rpm402_flags_each_distinct_missing_name() {
let src = "Name: x\n%if %{with foo}\nBuildRequires: a\n%endif\n\
%if %{without bar}\nBuildRequires: b\n%endif\n";
let diags = run_402(src);
assert_eq!(diags.len(), 2, "{diags:?}");
}
#[test]
fn rpm402_silent_on_clean_spec() {
let src = "Name: x\nVersion: 1\n";
assert!(run_402(src).is_empty());
}
#[test]
fn rpm402_flags_optional_with_without_bcond() {
let src = "Name: x\n%if %{?with foo}\nBuildRequires: a\n%endif\n";
let diags = run_402(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert!(diags[0].message.contains("foo"));
}
#[test]
fn rpm402_handles_negated_optional() {
let src = "Name: x\n%if %{!?with foo}\nBuildRequires: a\n%endif\n";
let diags = run_402(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert!(diags[0].message.contains("foo"));
}
}