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
//! RPM063 `setup-without-q-flag` — flag `%setup` invocations that
//! don't pass `-q` (quiet). The flag suppresses verbose tarball
//! extraction output; idiomatic spec files always use it.
//!
//! Matches:
//! - `%setup -q` — quiet, single flag (silent).
//! - `%setup -qn foo` / `%setup -nq foo` — combined short flags
//! containing `q` (silent).
//! - `%setup --quiet` — long form (silent).
//! - `%setup -n foo` — flagged: no quiet.
//!
//! Conservative fallback: if any arg is non-literal (contains a macro
//! expansion), we can't statically tell what flags expand to, so the
//! rule stays silent — better than a false positive on `%setup %{?my_opts}`.
use rpm_spec::ast::{BuildScriptKind, Section, Span, Text, TextSegment};
use crate::diagnostic::{Applicability, Diagnostic, LintCategory, Severity, Suggestion};
use crate::lint::{Lint, LintMetadata};
use crate::rules::util::MACRO_SETUP;
use crate::visit::{self, Visit};
pub static METADATA: LintMetadata = LintMetadata {
id: "RPM063",
name: "setup-without-q-flag",
description: "`%setup` should always be invoked with `-q` to silence tarball extraction noise.",
default_severity: Severity::Warn,
category: LintCategory::Style,
};
#[derive(Debug, Default)]
pub struct SetupWithoutQFlag {
diagnostics: Vec<Diagnostic>,
/// Span of the enclosing `%prep` section body. `MacroRef` doesn't
/// carry a span (the AST stores macros inside `TextSegment::Macro`
/// which lacks per-segment offsets), so we anchor diagnostics on
/// the section that holds the `%setup` call. Only set while inside
/// `%prep` — `%setup` outside `%prep` is a different category of
/// bug and not this rule's business.
current_prep_span: Option<Span>,
}
impl SetupWithoutQFlag {
pub fn new() -> Self {
Self::default()
}
}
impl<'ast> Visit<'ast> for SetupWithoutQFlag {
fn visit_section(&mut self, node: &'ast Section<Span>) {
let prev = self.current_prep_span.take();
if let Section::BuildScript {
kind: BuildScriptKind::Prep,
data,
..
} = node
{
self.current_prep_span = Some(*data);
}
visit::walk_section(self, node);
self.current_prep_span = prev;
}
fn visit_text(&mut self, node: &'ast Text) {
// `%setup` is a `Plain` macro: the rpm-spec parser produces
// `MacroRef { name: "setup", args: [] }` and leaves the
// arguments as a `Literal(" -q -n foo")` segment **immediately
// following** the macro in the same `Text` line. Scan
// post-`%setup` siblings rather than `MacroRef::args`.
let Some(anchor) = self.current_prep_span else {
return;
};
for (i, seg) in node.segments.iter().enumerate() {
let TextSegment::Macro(m) = seg else { continue };
if m.name != MACRO_SETUP {
continue;
}
if has_quiet_in_trailing_args(&node.segments[i + 1..]) {
continue;
}
self.diagnostics.push(
Diagnostic::new(
&METADATA,
Severity::Warn,
"`%setup` invoked without `-q`",
anchor,
)
.with_suggestion(Suggestion::new(
"add `-q` to silence tarball extraction output",
Vec::new(),
Applicability::Manual,
)),
);
}
visit::walk_text(self, node);
}
}
impl Lint for SetupWithoutQFlag {
fn metadata(&self) -> &'static LintMetadata {
&METADATA
}
fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
}
/// Scan the `TextSegment`s that follow `%setup` on the same line for a
/// quiet flag.
///
/// Returns `true` (suppresses the lint) when:
/// - a whitespace-delimited token like `-q`, `-qn`, `--quiet`, `-Tqc`,
/// … is found in the trailing literals, **or**
/// - a macro appears in the trailing segments (could expand to `-q`;
/// bail out conservatively rather than false-flag).
///
/// `--quiet` is matched as a whole token; `--quiet-mode` does not
/// match because the long-flag form has no `q` rule.
fn has_quiet_in_trailing_args(trailing: &[TextSegment]) -> bool {
let mut accumulated = String::new();
for seg in trailing {
match seg {
TextSegment::Literal(s) => accumulated.push_str(s),
TextSegment::Macro(_) => return true,
// `TextSegment` is `#[non_exhaustive]`; any future variant
// is treated like a macro — unknown content, bail out.
_ => return true,
}
}
accumulated.split_ascii_whitespace().any(is_quiet_flag)
}
fn is_quiet_flag(s: &str) -> bool {
if s == "--quiet" {
return true;
}
// Combined short flag: `-q`, `-qn`, `-nq`, `-Tcq`, ... anything
// that starts with a single `-` and contains a `q` after.
if let Some(rest) = s.strip_prefix('-')
&& !rest.starts_with('-')
&& rest.contains('q')
{
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::parse;
fn run(src: &str) -> Vec<Diagnostic> {
let outcome = parse(src);
let mut lint = SetupWithoutQFlag::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn flags_setup_without_q() {
let src = "Name: x\n%prep\n%setup -n foo\n";
let diags = run(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].lint_id, "RPM063");
}
#[test]
fn flags_bare_setup() {
let src = "Name: x\n%prep\n%setup\n";
let diags = run(src);
assert_eq!(diags.len(), 1, "{diags:?}");
}
#[test]
fn silent_for_setup_q() {
let src = "Name: x\n%prep\n%setup -q\n";
assert!(run(src).is_empty());
}
#[test]
fn silent_for_setup_q_with_name() {
let src = "Name: x\n%prep\n%setup -q -n foo\n";
assert!(run(src).is_empty());
}
#[test]
fn silent_for_setup_qn_combined() {
// `-qn foo` — combined short flags, quiet is included.
let src = "Name: x\n%prep\n%setup -qn foo\n";
assert!(run(src).is_empty());
}
#[test]
fn silent_for_setup_nq_combined() {
// Same flags in reverse order.
let src = "Name: x\n%prep\n%setup -nq foo\n";
assert!(run(src).is_empty());
}
#[test]
fn silent_for_long_quiet() {
let src = "Name: x\n%prep\n%setup --quiet -n foo\n";
assert!(run(src).is_empty());
}
#[test]
fn silent_for_autosetup() {
// `%autosetup` is a different macro — RPM063 only watches `%setup`.
let src = "Name: x\n%prep\n%autosetup -n foo -p1\n";
assert!(run(src).is_empty());
}
#[test]
fn silent_when_setup_outside_prep() {
// `%setup` in `%build` is a different category of bug (not
// ours to flag). The rule only watches the `%prep` body.
let src = "Name: x\n%build\n%setup -n foo\n";
assert!(run(src).is_empty());
}
#[test]
fn silent_when_arg_contains_macro() {
// Conservative bail-out: `%{my_opts}` could expand to `-q`,
// so we don't warn.
let src = "Name: x\n%prep\n%setup %{my_opts}\n";
assert!(run(src).is_empty());
}
}