use rpm_spec::ast::{CondExpr, Conditional, FilesContent, PreambleContent, Span, SpecItem};
use crate::diagnostic::{Applicability, Diagnostic, LintCategory, Severity, Suggestion};
use crate::lint::{Lint, LintMetadata};
use crate::rules::path_cond::{
BranchAnalyser, PathConditions, analyse_conditional, conjoin, else_effective_dnf, is_unsat,
};
use crate::visit::Visit;
pub static EXHAUSTIVE_CHAIN_METADATA: LintMetadata = LintMetadata {
id: "RPM116",
name: "mutex-branches-spell-out-else",
description: "`%if`/`%elif` chain exhausts the path-condition space yet lacks an explicit \
`%else`; rewriting the last `%elif` as `%else` makes the chain clearer.",
default_severity: Severity::Warn,
category: LintCategory::Style,
};
const MIN_CHAIN_FOR_ELSE_COLLAPSE: usize = 2;
#[derive(Debug, Default)]
pub struct ExhaustiveChain {
diagnostics: Vec<Diagnostic>,
pc: PathConditions,
last_branch_anchor: Option<Span>,
}
impl ExhaustiveChain {
pub fn new() -> Self {
Self::default()
}
}
impl BranchAnalyser for ExhaustiveChain {
fn pc(&mut self) -> &mut PathConditions {
&mut self.pc
}
fn on_branch(&mut self, _idx: usize, _eff: &Option<Dnf>, anchor: Span) {
self.last_branch_anchor = Some(anchor);
}
fn on_post_chain(&mut self, _node_anchor: Span, has_else: bool, prior: &[&CondExpr<Span>]) {
if has_else || prior.len() < MIN_CHAIN_FOR_ELSE_COLLAPSE {
return;
}
let Some(else_eff) = else_effective_dnf(prior, &mut self.pc.atoms) else {
return;
};
let combined = match self.pc.current() {
Some(path) => match conjoin(path, &else_eff) {
Some(d) => d,
None => return,
},
None => else_eff,
};
if is_unsat(&combined) {
let anchor = self
.last_branch_anchor
.expect("on_branch records every branch's anchor before on_post_chain");
self.diagnostics.push(
Diagnostic::new(
&EXHAUSTIVE_CHAIN_METADATA,
Severity::Warn,
"branches exhaust the path-condition space; the final `%elif` \
is equivalent to `%else`",
anchor,
)
.with_suggestion(Suggestion::new(
"replace the final `%elif EXPR` with `%else`",
Vec::new(),
Applicability::Manual,
)),
);
}
}
}
use crate::rules::boolean_dnf::Dnf;
fn walk_top_body(slf: &mut ExhaustiveChain, body: &[SpecItem<Span>]) {
for item in body {
slf.visit_item(item);
}
}
fn walk_preamble_body(slf: &mut ExhaustiveChain, body: &[PreambleContent<Span>]) {
for c in body {
slf.visit_preamble_content(c);
}
}
fn walk_files_body(slf: &mut ExhaustiveChain, body: &[FilesContent<Span>]) {
for c in body {
slf.visit_files_content(c);
}
}
impl<'ast> Visit<'ast> for ExhaustiveChain {
fn visit_top_conditional(&mut self, node: &'ast Conditional<Span, SpecItem<Span>>) {
analyse_conditional(self, node, walk_top_body);
}
fn visit_preamble_conditional(&mut self, node: &'ast Conditional<Span, PreambleContent<Span>>) {
analyse_conditional(self, node, walk_preamble_body);
}
fn visit_files_conditional(&mut self, node: &'ast Conditional<Span, FilesContent<Span>>) {
analyse_conditional(self, node, walk_files_body);
}
}
impl Lint for ExhaustiveChain {
fn metadata(&self) -> &'static LintMetadata {
&EXHAUSTIVE_CHAIN_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 = ExhaustiveChain::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn rpm116_flags_chain_covers_space() {
let src = "Name: x\n%if A\n%global foo bar\n%elif !A\n%global baz qux\n%endif\n";
let diags = run(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].lint_id, "RPM116");
}
#[test]
fn rpm116_silent_when_else_present() {
let src = "Name: x\n%if A\n%global foo bar\n%elif !A\n%global baz qux\n%else\n%global w v\n%endif\n";
assert!(run(src).is_empty());
}
#[test]
fn rpm116_silent_for_non_exhaustive() {
let src = "Name: x\n%if A\n%global foo bar\n%elif B\n%global baz qux\n%endif\n";
assert!(run(src).is_empty());
}
#[test]
fn rpm116_silent_for_single_if() {
let src = "Name: x\n%if A\n%global foo bar\n%endif\n";
assert!(run(src).is_empty());
}
}