Skip to content

Commit 9a0ef74

Browse files
committed
Add cargo_cfg_target_family_multivalued lint
1 parent 4b57d81 commit 9a0ef74

9 files changed

+673
-0
lines changed

compiler/rustc_lint/messages.ftl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ lint_builtin_unused_doc_comment = unused doc comment
181181
lint_builtin_while_true = denote infinite loops with `loop {"{"} ... {"}"}`
182182
.suggestion = use `loop`
183183
184+
lint_cargo_cfg_target_family_multivalued_comparison =
185+
comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
186+
.note = `CARGO_CFG_TARGET_FAMILY` can contain multiple comma-separated values
187+
.suggestion = compare against each family instead
188+
189+
lint_cargo_cfg_target_family_multivalued_match =
190+
matching on `CARGO_CFG_TARGET_FAMILY` directly may break in the future
191+
.note = `CARGO_CFG_TARGET_FAMILY` can contain multiple comma-separated values
192+
.suggestion = compare against each family instead
193+
184194
lint_check_name_unknown_tool = unknown lint tool: `{$tool_name}`
185195
186196
lint_closure_returning_async_block = closure returning async block can be made into an async closure
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use rustc_ast::{BinOpKind, UnOp};
2+
use rustc_hir::{self as hir, Expr, ExprKind, HirIdSet, LangItem, QPath, Stmt, StmtKind};
3+
use rustc_macros::{LintDiagnostic, Subdiagnostic};
4+
use rustc_middle::ty;
5+
use rustc_session::lint::FutureIncompatibilityReason;
6+
use rustc_session::{declare_lint, impl_lint_pass};
7+
use rustc_span::{Span, sym};
8+
9+
use crate::{LateContext, LateLintPass, LintContext};
10+
11+
declare_lint! {
12+
/// The `cargo_cfg_target_family_multivalued` lint detects single-valued comparisons of [the
13+
/// `CARGO_CFG_TARGET_FAMILY`][CARGO_CFG_TARGET_FAMILY] environment variable.
14+
///
15+
/// This variable is set by Cargo in build scripts.
16+
///
17+
/// ### Example
18+
///
19+
/// ```rust,no_run
20+
/// // build.rs
21+
/// fn main() {
22+
/// let target_family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
23+
///
24+
/// if target_family == "unix" {
25+
/// // Do something specific to Unix platforms
26+
/// }
27+
/// }
28+
/// ```
29+
///
30+
/// {{produces}}
31+
///
32+
/// ### Explanation
33+
///
34+
/// `CARGO_CFG_TARGET_FAMILY` is taken from [the `target_family` cfg][cfg-target_family], which
35+
/// may be set multiple times. This means that `CARGO_CFG_TARGET_FAMILY` can consist of multiple
36+
/// values, separated by commas. Comparing against a single value is thus not cross-platform.
37+
///
38+
/// Note that most targets currently only have a single `target_family`, so oftentimes you
39+
/// wouldn't hit this. This is a [future-incompatible] lint, since the compiler may at some
40+
/// point introduce further target families for existing targets, and then a simple comparison
41+
/// would no longer work.
42+
///
43+
/// [CARGO_CFG_TARGET_FAMILY]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#:~:text=CARGO_CFG_TARGET_FAMILY
44+
/// [cfg-target_family]: https://doc.rust-lang.org/reference/conditional-compilation.html#target_family
45+
CARGO_CFG_TARGET_FAMILY_MULTIVALUED,
46+
Warn,
47+
"comparing `CARGO_CFG_TARGET_FAMILY` env var with a single value",
48+
@future_incompatible = FutureIncompatibleInfo {
49+
reason: FutureIncompatibilityReason::FutureReleaseSemanticsChange,
50+
reference: "issue #100343 <https://github.com/rust-lang/rust/issues/100343>",
51+
explain_reason: false,
52+
};
53+
}
54+
55+
#[derive(Default)]
56+
pub(crate) struct CargoCfgTargetFamilyMultivalued {
57+
/// A side table of locals that are initialized from
58+
/// `std::env::var("CARGO_CFG_TARGET_FAMILY")` or similar.
59+
target_family_locals: HirIdSet,
60+
}
61+
62+
impl_lint_pass!(CargoCfgTargetFamilyMultivalued => [CARGO_CFG_TARGET_FAMILY_MULTIVALUED]);
63+
64+
#[derive(LintDiagnostic)]
65+
#[diag(lint_cargo_cfg_target_family_multivalued_comparison)]
66+
#[note]
67+
struct SingleValuedComparison {
68+
#[subdiagnostic]
69+
sugg: Option<ReplaceWithSplitAny>,
70+
}
71+
72+
#[derive(Subdiagnostic)]
73+
#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")]
74+
struct ReplaceWithSplitAny {
75+
#[suggestion_part(code = "!")]
76+
negate: Option<Span>,
77+
#[suggestion_part(code = ".split(',').any(|x| x == ")]
78+
op: Span,
79+
#[suggestion_part(code = ")")]
80+
end: Span,
81+
}
82+
83+
#[derive(LintDiagnostic)]
84+
#[diag(lint_cargo_cfg_target_family_multivalued_match)]
85+
#[note]
86+
#[note(lint_suggestion)]
87+
struct SingleValuedMatch;
88+
89+
// NOTE: We choose not to do a check for when in a build script, like:
90+
// matches!(&sess.opts.crate_name, Some(crate_name) if crate_name == "build_script_build")
91+
// Since we might be building a library that is used as a build script dependency (`cc-rs` etc).
92+
impl<'tcx> LateLintPass<'tcx> for CargoCfgTargetFamilyMultivalued {
93+
fn check_stmt(&mut self, _cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) {
94+
// Find locals that are initialized from `CARGO_CFG_TARGET_FAMILY`, and save them for later
95+
// checking.
96+
if let StmtKind::Let(stmt) = &stmt.kind {
97+
if let Some(init) = stmt.init {
98+
if self.accesses_target_family_env(init) {
99+
stmt.pat.each_binding(|_, hir_id, _, _| {
100+
self.target_family_locals.insert(hir_id);
101+
});
102+
}
103+
}
104+
}
105+
}
106+
107+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
108+
// Check expressions that do single-valued comparisons.
109+
match &expr.kind {
110+
ExprKind::Binary(op, a, b) if matches!(op.node, BinOpKind::Eq | BinOpKind::Ne) => {
111+
if self.accesses_target_family_env(a) {
112+
// If this is a &str or String, we can confidently give a `.split()` suggestion.
113+
let a_ty = cx.typeck_results().expr_ty(a);
114+
let is_str = matches!(
115+
a_ty.kind(),
116+
ty::Ref(_, r, _) if r.is_str(),
117+
) || matches!(
118+
a_ty.ty_adt_def(),
119+
Some(ty_def) if cx.tcx.is_lang_item(ty_def.did(), LangItem::String),
120+
);
121+
let sugg = is_str.then(|| ReplaceWithSplitAny {
122+
negate: (op.node == BinOpKind::Ne).then(|| expr.span.shrink_to_lo()),
123+
op: a.span.between(b.span),
124+
end: b.span.shrink_to_hi(),
125+
});
126+
127+
cx.emit_span_lint(
128+
CARGO_CFG_TARGET_FAMILY_MULTIVALUED,
129+
expr.span,
130+
SingleValuedComparison { sugg },
131+
);
132+
} else if self.accesses_target_family_env(b) {
133+
cx.emit_span_lint(
134+
CARGO_CFG_TARGET_FAMILY_MULTIVALUED,
135+
expr.span,
136+
// Unsure how to emit a suggestion when we need to reorder `a` and `b`.
137+
SingleValuedComparison { sugg: None },
138+
);
139+
}
140+
}
141+
ExprKind::Match(expr, _, _) if self.accesses_target_family_env(expr) => {
142+
cx.emit_span_lint(
143+
CARGO_CFG_TARGET_FAMILY_MULTIVALUED,
144+
expr.span,
145+
SingleValuedMatch,
146+
);
147+
}
148+
// We don't handle method calls like `PartialEq::eq`, that's probably fine though,
149+
// those are uncommon in real-world code.
150+
_ => {}
151+
}
152+
}
153+
}
154+
155+
impl CargoCfgTargetFamilyMultivalued {
156+
/// Check if an expression is likely derived from the `CARGO_CFG_TARGET_FAMILY` env var.
157+
fn accesses_target_family_env(&self, expr: &Expr<'_>) -> bool {
158+
match &expr.kind {
159+
// A call to `std::env::var[_os]("CARGO_CFG_TARGET_FAMILY")`.
160+
//
161+
// NOTE: This actually matches all functions that take as a single value
162+
// `"CARGO_CFG_TARGET_FAMILY"`. We could restrict this by matching only functions that
163+
// match `"std::env::var"` or `"std::env::var_os"` by doing something like:
164+
//
165+
// && let Expr { kind: ExprKind::Path(QPath::Resolved(_, path)), .. } = func
166+
// && let Some(fn_def_id) = path.res.opt_def_id()
167+
// && cx.tcx.is_diagnostic_item(sym::std_env_var, fn_def_id)
168+
//
169+
// But users often define wrapper functions around these, and so we wouldn't catch it
170+
// when they do.
171+
//
172+
// This is probably fine, `"CARGO_CFG_TARGET_FAMILY"` is unique enough of a name that
173+
// it's unlikely that people will be using it for anything else.
174+
ExprKind::Call(_, [arg])
175+
if let ExprKind::Lit(lit) = &arg.kind
176+
&& lit.node.str() == Some(sym::cargo_cfg_target_family) =>
177+
{
178+
true
179+
}
180+
// On local variables, try to see if it was initialized from target family earlier.
181+
ExprKind::Path(QPath::Resolved(_, path))
182+
if let hir::def::Res::Local(local_hir_id) = &path.res =>
183+
{
184+
self.target_family_locals.contains(local_hir_id)
185+
}
186+
// Recurse through references and dereferences.
187+
ExprKind::AddrOf(_, _, expr) | ExprKind::Unary(UnOp::Deref, expr) => {
188+
self.accesses_target_family_env(expr)
189+
}
190+
// Recurse on every method call to allow `.unwrap()`, `.as_deref()` and similar.
191+
//
192+
// NOTE: We could consider only recursing on specific `Option`/`Result` methods, but the
193+
// full list of the ones we'd want becomes unwieldy pretty quickly.
194+
ExprKind::MethodCall(_, receiver, _, _) => self.accesses_target_family_env(receiver),
195+
_ => false,
196+
}
197+
}
198+
}

compiler/rustc_lint/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ mod async_closures;
3737
mod async_fn_in_trait;
3838
mod autorefs;
3939
pub mod builtin;
40+
mod cargo_cfg;
4041
mod context;
4142
mod dangling;
4243
mod default_could_be_derived;
@@ -86,6 +87,7 @@ use async_closures::AsyncClosureUsage;
8687
use async_fn_in_trait::AsyncFnInTrait;
8788
use autorefs::*;
8889
use builtin::*;
90+
use cargo_cfg::*;
8991
use dangling::*;
9092
use default_could_be_derived::DefaultCouldBeDerived;
9193
use deref_into_dyn_supertrait::*;
@@ -246,6 +248,7 @@ late_lint_methods!(
246248
UnqualifiedLocalImports: UnqualifiedLocalImports,
247249
CheckTransmutes: CheckTransmutes,
248250
LifetimeSyntax: LifetimeSyntax,
251+
CargoCfgTargetFamilyMultivalued: CargoCfgTargetFamilyMultivalued::default(),
249252
]
250253
]
251254
);

compiler/rustc_span/src/symbol.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ symbols! {
612612
call_ref_future,
613613
caller_location,
614614
capture_disjoint_fields,
615+
cargo_cfg_target_family: "CARGO_CFG_TARGET_FAMILY",
615616
carrying_mul_add,
616617
catch_unwind,
617618
cause,
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Test the `cargo_cfg_target_family_multivalued` lint.
2+
3+
//@ check-pass
4+
//@ exec-env:CARGO_CFG_TARGET_FAMILY=unix
5+
6+
use std::env;
7+
8+
fn main() {
9+
// Check that direct comparisons warn.
10+
let is_unix = env::var("CARGO_CFG_TARGET_FAMILY").unwrap() == "unix";
11+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
12+
13+
// But that later usage doesn't warn.
14+
if is_unix {}
15+
16+
// Assigning to local variable is fine.
17+
let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
18+
19+
// Using local in an `==` comparison.
20+
if target_family == "unix" {
21+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
22+
}
23+
24+
// Using local in a match.
25+
match &*target_family {
26+
//~^ WARN matching on `CARGO_CFG_TARGET_FAMILY` directly may break in the future
27+
"unix" => {}
28+
_ => {}
29+
}
30+
31+
// Correct handling doesn't warn.
32+
if target_family.contains("unix") {}
33+
if target_family.split(',').any(|x| x == "unix") {}
34+
35+
// Test supression.
36+
#[allow(cargo_cfg_target_family_multivalued)]
37+
let _ = env::var("CARGO_CFG_TARGET_FAMILY").unwrap() == "unix";
38+
39+
// Negative comparison.
40+
let _ = target_family != "unix";
41+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
42+
43+
// Local variable propagation.
44+
let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
45+
let target_family: &str = target_family.as_ref();
46+
let _ = target_family == "unix";
47+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
48+
49+
// Custom wrapper.
50+
fn get_and_track_env_var(env_var_name: &str) -> String {
51+
// This is actually unnecessary, Cargo already tracks changes to the target family, but it's
52+
// nonetheless a fairly common pattern.
53+
println!("cargo:rerun-if-env-changed={env_var_name}");
54+
env::var(env_var_name).unwrap()
55+
}
56+
let _ = get_and_track_env_var("CARGO_CFG_TARGET_FAMILY") == "unix";
57+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
58+
59+
// Various.
60+
let _ = ::std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap() == "unix";
61+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
62+
let _ = env::var("CARGO_CFG_TARGET_FAMILY").expect("should be set") == "unix";
63+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
64+
let _ = env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default() == "unix";
65+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
66+
let _ = env::var_os("CARGO_CFG_TARGET_FAMILY").unwrap() == "unix";
67+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
68+
let _ = env::var("CARGO_CFG_TARGET_FAMILY") == Ok("unix".to_string());
69+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
70+
let _ = env::var_os("CARGO_CFG_TARGET_FAMILY") == Some("unix".into());
71+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
72+
let _ = env::var("CARGO_CFG_TARGET_FAMILY").as_deref() == Ok("unix");
73+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
74+
let _ = env::var_os("CARGO_CFG_TARGET_FAMILY").as_deref() == Some("unix".as_ref());
75+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
76+
let _ = env::var("CARGO_CFG_TARGET_FAMILY").ok().as_deref() == Some("unix".as_ref());
77+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
78+
79+
false_negatives();
80+
false_positives();
81+
}
82+
83+
// This lint has many false negatives, the problem is intractable in the general case.
84+
fn false_negatives() {
85+
// Cannot detect if the env var is not specified inline (such as when dynamically generated).
86+
let var = "CARGO_CFG_TARGET_FAMILY";
87+
let _ = env::var(var).unwrap() == "unix";
88+
89+
// Cannot detect if env var value comes from somewhere more complex.
90+
fn get_env_var() -> String {
91+
env::var("CARGO_CFG_TARGET_FAMILY").unwrap()
92+
}
93+
let _ = get_env_var() == "unix";
94+
95+
// Doesn't detect more complex expressions.
96+
let _ = std::convert::identity(env::var_os("CARGO_CFG_TARGET_FAMILY").unwrap()) == "unix";
97+
let _ = *Box::new(env::var_os("CARGO_CFG_TARGET_FAMILY").unwrap()) == "unix";
98+
99+
// Doesn't detect variables that are initialized later.
100+
let later_init;
101+
later_init = env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
102+
if later_init == "unix" {}
103+
104+
// Doesn't detect if placed inside a struct.
105+
struct Target {
106+
family: String,
107+
}
108+
let target = Target { family: env::var("CARGO_CFG_TARGET_FAMILY").unwrap() };
109+
if target.family == "unix" {}
110+
}
111+
112+
// This lint also has false positives, these are probably unlikely to be hit in practice.
113+
fn false_positives() {
114+
// Cannot detect later changes to assigned variable.
115+
let mut overwritten = env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
116+
if true {
117+
overwritten = "unix".to_string();
118+
}
119+
if overwritten == "unix" {}
120+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
121+
122+
// Non-std::env::var usage.
123+
let _ = std::convert::identity("CARGO_CFG_TARGET_FAMILY") == "unix";
124+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
125+
126+
// Call unusual `Option`/`Result` method, and then compare that result.
127+
let _ = env::var_os("CARGO_CFG_TARGET_FAMILY").is_some() == true;
128+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
129+
130+
// Match with match arms that contains checks.
131+
match env::var("CARGO_CFG_TARGET_FAMILY") {
132+
//~^ WARN matching on `CARGO_CFG_TARGET_FAMILY` directly may break in the future
133+
Ok(os) if os.contains("unix") => {}
134+
_ => {}
135+
}
136+
137+
// Unusual method call.
138+
trait Foo {
139+
fn returns_string(&self) -> &str {
140+
"unix"
141+
}
142+
}
143+
impl Foo for String {}
144+
let _ = env::var("CARGO_CFG_TARGET_FAMILY").unwrap().returns_string() == "unix";
145+
//~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future
146+
}

0 commit comments

Comments
 (0)