Skip to content

Commit 6583dcd

Browse files
Add new misleading_cfg_in_build_script rustc lint
1 parent c525b8a commit 6583dcd

File tree

3 files changed

+294
-1
lines changed

3 files changed

+294
-1
lines changed

compiler/rustc_lint/src/early/diagnostics/check_cfg.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ fn cargo_help_sub(
6666
inst: &impl Fn(EscapeQuotes) -> String,
6767
) -> lints::UnexpectedCfgCargoHelp {
6868
// We don't want to suggest the `build.rs` way to expected cfgs if we are already in a
69-
// `build.rs`. We therefor do a best effort check (looking if the `--crate-name` is
69+
// `build.rs`. We therefore do a best effort check (looking if the `--crate-name` is
7070
// `build_script_build`) to try to figure out if we are building a Cargo build script
7171

7272
let unescaped = &inst(EscapeQuotes::No);

compiler/rustc_lint/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub mod lifetime_syntax;
5757
mod lints;
5858
mod macro_expr_fragment_specifier_2024_migration;
5959
mod map_unit_fn;
60+
mod misleading_cfg_in_build_script;
6061
mod multiple_supertrait_upcastable;
6162
mod non_ascii_idents;
6263
mod non_fmt_panic;
@@ -101,6 +102,7 @@ use let_underscore::*;
101102
use lifetime_syntax::*;
102103
use macro_expr_fragment_specifier_2024_migration::*;
103104
use map_unit_fn::*;
105+
use misleading_cfg_in_build_script::MisleadingCfgInBuildScript;
104106
use multiple_supertrait_upcastable::*;
105107
use non_ascii_idents::*;
106108
use non_fmt_panic::NonPanicFmt;
@@ -155,6 +157,7 @@ early_lint_methods!(
155157
pub BuiltinCombinedPreExpansionLintPass,
156158
[
157159
KeywordIdents: KeywordIdents,
160+
MisleadingCfgInBuildScript: MisleadingCfgInBuildScript,
158161
]
159162
]
160163
);
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
use rustc_ast::ast::{Attribute, MacCall};
2+
use rustc_ast::tokenstream::TokenStream;
3+
use rustc_ast::{MetaItem, MetaItemInner, MetaItemKind};
4+
use rustc_errors::{Applicability, DiagDecorator};
5+
use rustc_session::{declare_lint, declare_lint_pass};
6+
use rustc_span::{Span, Symbol, sym};
7+
8+
use crate::{EarlyContext, EarlyLintPass, LintContext};
9+
10+
declare_lint! {
11+
/// Checks for usage of `#[cfg]`/`#[cfg_attr]`/`cfg!()` in `build.rs` scripts.
12+
///
13+
/// ### Explanation
14+
///
15+
/// It checks the `cfg` values for the *host*, not the target. For example, `cfg!(windows)` is
16+
/// true when compiling on Windows, so it will give the wrong answer if you are cross compiling.
17+
/// This is because build scripts run on the machine performing compilation, rather than on the
18+
/// target.
19+
///
20+
/// ### Example
21+
///
22+
/// ```no_run
23+
/// if cfg!(windows) {}
24+
/// ```
25+
///
26+
/// Use instead:
27+
///
28+
/// ```no_run
29+
/// if std::env::var("CARGO_CFG_WINDOWS").is_ok() {}
30+
/// ```
31+
pub MISLEADING_CFG_IN_BUILD_SCRIPT,
32+
Warn,
33+
"use of host configs in `build.rs` scripts"
34+
}
35+
36+
declare_lint_pass!(MisleadingCfgInBuildScript => [MISLEADING_CFG_IN_BUILD_SCRIPT]);
37+
38+
/// Represents the AST of `cfg` attribute and `cfg!` macro.
39+
#[derive(Debug)]
40+
enum CfgAst {
41+
/// Represents an OS family such as "unix" or "windows".
42+
OsFamily(Symbol),
43+
/// The `any()` attribute.
44+
Any(Vec<CfgAst>),
45+
/// The `all()` attribute.
46+
All(Vec<CfgAst>),
47+
/// The `not()` attribute.
48+
Not(Box<CfgAst>),
49+
/// Any `target_* = ""` key/value attribute.
50+
TargetKeyValue(Symbol, Symbol),
51+
/// the `feature = ""` attribute with its associated value.
52+
Feature(Symbol),
53+
}
54+
55+
impl CfgAst {
56+
fn has_only_features(&self) -> bool {
57+
match self {
58+
Self::OsFamily(_) | Self::TargetKeyValue(_, _) => false,
59+
Self::Any(v) | Self::All(v) => v.is_empty() || v.iter().all(CfgAst::has_only_features),
60+
Self::Not(v) => v.has_only_features(),
61+
Self::Feature(_) => true,
62+
}
63+
}
64+
65+
fn generate_replacement(&self) -> String {
66+
self.generate_replacement_inner(true, false)
67+
}
68+
69+
fn generate_replacement_inner(&self, is_top_level: bool, parent_is_not: bool) -> String {
70+
match self {
71+
Self::OsFamily(os) => format!(
72+
"std::env::var(\"CARGO_CFG_{}\"){}",
73+
os.as_str().to_uppercase(),
74+
if parent_is_not { ".is_err()" } else { ".is_ok()" },
75+
),
76+
Self::TargetKeyValue(cfg_target, s) => format!(
77+
"{}std::env::var(\"CARGO_CFG_{}\").unwrap_or_default() == \"{s}\"",
78+
if parent_is_not { "!" } else { "" },
79+
cfg_target.as_str().to_uppercase(),
80+
),
81+
Self::Any(v) => {
82+
if v.is_empty() {
83+
if parent_is_not { "true" } else { "false" }.to_string()
84+
} else if v.len() == 1 {
85+
v[0].generate_replacement_inner(is_top_level, parent_is_not)
86+
} else {
87+
format!(
88+
"{not}{open_paren}{cond}{closing_paren}",
89+
not = if parent_is_not { "!" } else { "" },
90+
open_paren = if !parent_is_not && is_top_level { "" } else { "(" },
91+
cond = v
92+
.iter()
93+
.map(|i| i.generate_replacement_inner(false, false))
94+
.collect::<Vec<_>>()
95+
.join(" || "),
96+
closing_paren = if !parent_is_not && is_top_level { "" } else { ")" },
97+
)
98+
}
99+
}
100+
Self::All(v) => {
101+
if v.is_empty() {
102+
if parent_is_not { "false" } else { "true" }.to_string()
103+
} else if v.len() == 1 {
104+
v[0].generate_replacement_inner(is_top_level, parent_is_not)
105+
} else {
106+
format!(
107+
"{not}{open_paren}{cond}{closing_paren}",
108+
not = if parent_is_not { "!" } else { "" },
109+
open_paren = if !parent_is_not && is_top_level { "" } else { "(" },
110+
cond = v
111+
.iter()
112+
.map(|i| i.generate_replacement_inner(false, false))
113+
.collect::<Vec<_>>()
114+
.join(" && "),
115+
closing_paren = if !parent_is_not && is_top_level { "" } else { ")" },
116+
)
117+
}
118+
}
119+
Self::Not(i) => i.generate_replacement_inner(is_top_level, true),
120+
Self::Feature(s) => format!(
121+
"cfg!({}feature = {s}{})",
122+
if parent_is_not { "not(" } else { "" },
123+
if parent_is_not { ")" } else { "" },
124+
),
125+
}
126+
}
127+
}
128+
129+
fn parse_meta_item(meta: MetaItem, has_unknown: &mut bool, out: &mut Vec<CfgAst>) {
130+
let Some(name) = meta.name() else {
131+
*has_unknown = true;
132+
return;
133+
};
134+
match meta.kind {
135+
MetaItemKind::Word => {
136+
if [sym::windows, sym::unix].contains(&name) {
137+
out.push(CfgAst::OsFamily(name));
138+
return;
139+
}
140+
}
141+
MetaItemKind::NameValue(item) => {
142+
if name == sym::feature {
143+
out.push(CfgAst::Feature(item.symbol));
144+
return;
145+
} else if name.as_str().starts_with("target_") {
146+
out.push(CfgAst::TargetKeyValue(name, item.symbol));
147+
return;
148+
}
149+
}
150+
MetaItemKind::List(item) => {
151+
if !*has_unknown && [sym::any, sym::not, sym::all].contains(&name) {
152+
let mut sub_out = Vec::new();
153+
154+
for sub in item {
155+
if let MetaItemInner::MetaItem(item) = sub {
156+
parse_meta_item(item, has_unknown, &mut sub_out);
157+
if *has_unknown {
158+
return;
159+
}
160+
}
161+
}
162+
if name == sym::any {
163+
out.push(CfgAst::Any(sub_out));
164+
return;
165+
} else if name == sym::all {
166+
out.push(CfgAst::All(sub_out));
167+
return;
168+
// `not()` can only have one argument.
169+
} else if sub_out.len() == 1 {
170+
out.push(CfgAst::Not(Box::new(sub_out.pop().unwrap())));
171+
return;
172+
}
173+
}
174+
}
175+
}
176+
*has_unknown = true;
177+
}
178+
179+
fn parse_macro_args(tokens: &TokenStream, has_unknown: &mut bool, out: &mut Vec<CfgAst>) {
180+
if let Some(meta) = MetaItem::from_tokens(&mut tokens.iter()) {
181+
parse_meta_item(meta, has_unknown, out);
182+
}
183+
}
184+
185+
fn get_invalid_cfg_attrs(attr: &MetaItem, spans: &mut Vec<Span>, has_unknown: &mut bool) {
186+
let Some(ident) = attr.ident() else { return };
187+
if [sym::unix, sym::windows].contains(&ident.name) {
188+
spans.push(attr.span);
189+
} else if attr.value_str().is_some() && ident.name.as_str().starts_with("target_") {
190+
spans.push(attr.span);
191+
} else if let Some(sub_attrs) = attr.meta_item_list() {
192+
for sub_attr in sub_attrs {
193+
if let Some(meta) = sub_attr.meta_item() {
194+
get_invalid_cfg_attrs(meta, spans, has_unknown);
195+
}
196+
}
197+
} else {
198+
*has_unknown = true;
199+
}
200+
}
201+
202+
fn is_build_script(cx: &EarlyContext<'_>) -> bool {
203+
rustc_session::utils::was_invoked_from_cargo()
204+
&& cx.sess().opts.crate_name.as_deref() == Some("build_script_build")
205+
}
206+
207+
const ERROR_MESSAGE: &str = "target-based cfg should be avoided in build scripts";
208+
209+
impl EarlyLintPass for MisleadingCfgInBuildScript {
210+
fn check_attribute(&mut self, cx: &EarlyContext<'_>, attr: &Attribute) {
211+
if !is_build_script(cx) {
212+
return;
213+
}
214+
215+
let mut spans = Vec::new();
216+
let mut has_unknown = false;
217+
match attr.name() {
218+
Some(sym::cfg) if let Some(meta) = attr.meta() => {
219+
get_invalid_cfg_attrs(&meta, &mut spans, &mut has_unknown);
220+
}
221+
Some(sym::cfg_attr)
222+
if let Some(sub_attrs) = attr.meta_item_list()
223+
&& let Some(meta) = sub_attrs.first().and_then(|a| a.meta_item()) =>
224+
{
225+
get_invalid_cfg_attrs(meta, &mut spans, &mut has_unknown);
226+
}
227+
_ => return,
228+
}
229+
if !spans.is_empty() {
230+
if has_unknown {
231+
// If the `cfg`/`cfg_attr` attribute contains not only invalid items, we display
232+
// spans of all invalid items.
233+
cx.emit_span_lint(
234+
MISLEADING_CFG_IN_BUILD_SCRIPT,
235+
spans,
236+
DiagDecorator(|diag| {
237+
diag.primary_message(ERROR_MESSAGE);
238+
}),
239+
);
240+
} else {
241+
// No "good" item in the `cfg`/`cfg_attr` attribute so we can use the span of the
242+
// whole attribute directly.
243+
cx.emit_span_lint(
244+
MISLEADING_CFG_IN_BUILD_SCRIPT,
245+
attr.span,
246+
DiagDecorator(|diag| {
247+
diag.primary_message(ERROR_MESSAGE);
248+
}),
249+
);
250+
}
251+
}
252+
}
253+
254+
fn check_mac(&mut self, cx: &EarlyContext<'_>, call: &MacCall) {
255+
if !is_build_script(cx) {
256+
return;
257+
}
258+
259+
if call.path.segments.len() == 1 && call.path.segments[0].ident.name == sym::cfg {
260+
let mut ast = Vec::new();
261+
let mut has_unknown = false;
262+
parse_macro_args(&call.args.tokens, &mut has_unknown, &mut ast);
263+
if !has_unknown && ast.len() > 1 {
264+
cx.emit_span_lint(
265+
MISLEADING_CFG_IN_BUILD_SCRIPT,
266+
call.span(),
267+
DiagDecorator(|diag| {
268+
diag.primary_message(ERROR_MESSAGE);
269+
}),
270+
);
271+
} else if let Some(ast) = ast.get(0)
272+
&& !ast.has_only_features()
273+
{
274+
let span = call.span();
275+
cx.emit_span_lint(
276+
MISLEADING_CFG_IN_BUILD_SCRIPT,
277+
span,
278+
DiagDecorator(|diag| {
279+
diag.primary_message(ERROR_MESSAGE).span_suggestion(
280+
span,
281+
"use cargo environment variables if possible",
282+
ast.generate_replacement(),
283+
Applicability::MaybeIncorrect,
284+
);
285+
}),
286+
);
287+
}
288+
}
289+
}
290+
}

0 commit comments

Comments
 (0)