1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
//! RPM032 `macro-redefinition` — defining the same `%global` /
//! `%define` macro twice at the **same scope** in one spec is almost
//! always a copy-paste mistake: rpm honours the last one and the
//! earlier definition is dead code.
//!
//! ### Conditional branches are alternatives, not redefinitions
//!
//! ```rpm
//! %if 0%{?fedora}
//! %global flavor fedora
//! %else
//! %global flavor centos
//! %endif
//! ```
//!
//! is **not** flagged: only one branch executes at build time, so the
//! two `%global` lines never collide. We track macro definitions per
//! conditional branch independently and merge them back as a union
//! after `%endif`, never as a sequence.
//!
//! `%undefine` is treated as a deliberate stack pop and clears the
//! seen-set, so a subsequent `%define` is a fresh definition.
use std::collections::HashMap;
use rpm_spec::ast::{Conditional, MacroDef, MacroDefKind, Span, SpecFile, SpecItem};
use crate::diagnostic::{Diagnostic, LintCategory, Severity};
use crate::lint::{Lint, LintMetadata};
use crate::visit::Visit;
pub static METADATA: LintMetadata = LintMetadata {
id: "RPM032",
name: "macro-redefinition",
description: "A macro is redefined at the same scope; the earlier definition is dead code. \
Definitions in alternative %if/%else branches are not redefinitions and are ignored.",
default_severity: Severity::Warn,
category: LintCategory::Correctness,
};
#[derive(Debug, Default)]
pub struct MacroRedefinition {
diagnostics: Vec<Diagnostic>,
}
impl MacroRedefinition {
pub fn new() -> Self {
Self::default()
}
}
impl<'ast> Visit<'ast> for MacroRedefinition {
fn visit_spec(&mut self, spec: &'ast SpecFile<Span>) {
let mut seen: HashMap<String, Span> = HashMap::new();
walk_items(&spec.items, &mut seen, &mut self.diagnostics);
}
}
fn walk_items(
items: &[SpecItem<Span>],
seen: &mut HashMap<String, Span>,
out: &mut Vec<Diagnostic>,
) {
for item in items {
match item {
SpecItem::MacroDef(m) => check_macro_def(m, seen, out),
SpecItem::Conditional(c) => walk_conditional(c, out),
_ => {}
}
}
}
fn walk_conditional(cond: &Conditional<Span, SpecItem<Span>>, out: &mut Vec<Diagnostic>) {
// Each branch is treated as its own isolated scope: it does *not*
// inherit the parent's seen-set. This avoids two common false
// positives:
//
// 1. `%global foo 1` at top level, then `%if cond %global foo 2 %endif`
// — the second line is a conditional override, not a bug. Only
// one of the lines wins at build time in any given configuration.
//
// 2. `%if A %global foo 1 %endif %if B %global foo 2 %endif`
// — the two `%if` blocks are mutually exclusive alternatives.
//
// Trade-off: a true bug like
// `%if outer %global foo 1 %if inner %global foo 2 %endif %endif`
// where both `%global` lines execute on the `outer && inner` path
// is treated as silent. We accept that — the false-positive rate
// of the alternative ("inherit parent") is far worse on real specs.
//
// A redefinition *within* one branch (same `%if`, two `%global foo`)
// is still caught.
for branch in &cond.branches {
let mut branch_seen: HashMap<String, Span> = HashMap::new();
walk_items(&branch.body, &mut branch_seen, out);
}
if let Some(els) = &cond.otherwise {
let mut branch_seen: HashMap<String, Span> = HashMap::new();
walk_items(els, &mut branch_seen, out);
}
}
fn check_macro_def(
m: &MacroDef<Span>,
seen: &mut HashMap<String, Span>,
out: &mut Vec<Diagnostic>,
) {
// `%undefine` pops the macro stack — subsequent `%define` is a
// fresh definition, not a redefinition.
if matches!(m.kind, MacroDefKind::Undefine) {
seen.remove(&m.name);
return;
}
match seen.get(&m.name) {
None => {
seen.insert(m.name.clone(), m.data);
}
Some(&first) => {
out.push(
Diagnostic::new(
&METADATA,
Severity::Warn,
format!("macro `{}` is redefined", m.name),
m.data,
)
.with_label(first, "previously defined here"),
);
}
}
}
impl Lint for MacroRedefinition {
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 = MacroRedefinition::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn flags_double_define_at_top_level() {
let diags = run("%define foo 1\n%define foo 2\nName: x\n");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].lint_id, "RPM032");
assert_eq!(diags[0].labels.len(), 1);
}
#[test]
fn flags_global_after_define_at_top_level() {
let diags = run("%define foo 1\n%global foo 2\nName: x\n");
assert_eq!(diags.len(), 1);
}
#[test]
fn silent_for_undefine_redefine_pair() {
assert!(run("%define foo 1\n%undefine foo\n%define foo 2\nName: x\n").is_empty(),);
}
#[test]
fn silent_for_distinct_names() {
assert!(run("%define foo 1\n%define bar 2\nName: x\n").is_empty());
}
#[test]
fn silent_for_if_else_alternatives() {
// The two branches define the same macro but only one executes.
// This is the canonical "conditional %global" pattern.
let src = "%if 0%{?fedora}\n\
%global flavor fedora\n\
%else\n\
%global flavor centos\n\
%endif\n\
Name: x\n";
assert!(run(src).is_empty(), "alternatives must not flag");
}
#[test]
fn silent_for_multiple_separate_if_blocks() {
// Each `%if ... %endif` introduces an alternative tree; the
// macro is `defined-in-some-branch` after the first block, but
// the subsequent block also redefines only inside an
// alternative, so still no collision.
let src = "%if 1\n%define jit 1\n%else\n%define jit 0\n%endif\n\
%if 2\n%define jit 1\n%else\n%define jit 0\n%endif\n\
Name: x\n";
assert!(
run(src).is_empty(),
"consecutive %if blocks are alternatives at top level"
);
}
#[test]
fn flags_redefinition_inside_same_branch() {
// Two `%define foo` inside the *same* %if branch is a real bug.
let src = "%if 1\n%define foo 1\n%define foo 2\n%endif\nName: x\n";
let diags = run(src);
assert_eq!(diags.len(), 1);
}
#[test]
fn silent_for_nested_conditionals_documented_tradeoff() {
// Regression lock for the trade-off documented in `walk_conditional`:
// `%if outer %define foo 1 %if inner %define foo 2 %endif %endif`
// would actually redefine `foo` at build time when `outer && inner`,
// but flagging it would require global scope tracking and would
// trigger many false positives on idiomatic specs. We accept the
// silent false-negative and pin it here.
let src = "%if 1\n\
%define foo 1\n\
%if 1\n\
%define foo 2\n\
%endif\n\
%endif\n\
Name: x\n";
assert!(
run(src).is_empty(),
"nested if/define pattern is silently accepted by design"
);
}
#[test]
fn silent_for_conditional_default_plus_top_level_override() {
// Common idiom: set a default inside %if, optionally override
// unconditionally afterwards. The post-%if line always runs
// and "wins", which is exactly what the maintainer intended.
let src = "%if 1\n%define foo 1\n%endif\n%define foo 2\nName: x\n";
assert!(
run(src).is_empty(),
"conditional + top-level override is idiomatic"
);
}
}