-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Add cargo_cfg_target_family_multivalued FCW
#147545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
madsmtm
wants to merge
1
commit into
rust-lang:main
Choose a base branch
from
madsmtm:cargo_cfg_target_family_lint
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| use rustc_ast::{BinOpKind, UnOp}; | ||
| use rustc_hir::{self as hir, Expr, ExprKind, HirIdSet, LangItem, QPath, Stmt, StmtKind}; | ||
| use rustc_macros::{LintDiagnostic, Subdiagnostic}; | ||
| use rustc_middle::ty; | ||
| use rustc_session::lint::FutureIncompatibilityReason; | ||
| use rustc_session::{declare_lint, impl_lint_pass}; | ||
| use rustc_span::{Span, sym}; | ||
|
|
||
| use crate::{LateContext, LateLintPass, LintContext}; | ||
|
|
||
| declare_lint! { | ||
| /// The `suspicious_cargo_cfg_target_family_comparisons` lint detects single-valued comparisons | ||
| /// of [the `CARGO_CFG_TARGET_FAMILY`][CARGO_CFG_TARGET_FAMILY] environment variable. | ||
| /// | ||
| /// This variable is set by Cargo in build scripts. | ||
| /// | ||
| /// ### Example | ||
| /// | ||
| /// ```rust,no_run | ||
| /// // build.rs | ||
| /// fn main() { | ||
| /// let target_family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap(); | ||
| /// | ||
| /// if target_family == "unix" { | ||
| /// // Do something specific to Unix platforms | ||
| /// } | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// {{produces}} | ||
| /// | ||
| /// ### Explanation | ||
| /// | ||
| /// `CARGO_CFG_TARGET_FAMILY` is taken from [the `target_family` cfg][cfg-target_family], which | ||
| /// may be set multiple times. This means that `CARGO_CFG_TARGET_FAMILY` can consist of multiple | ||
| /// values, separated by commas. Comparing against a single value is thus not cross-platform. | ||
| /// | ||
| /// Note that most targets currently only have a single `target_family`, so oftentimes you | ||
| /// wouldn't hit this. This is a [future-incompatible] lint, since the compiler may at some | ||
| /// point introduce further target families for existing targets, and then a simple comparison | ||
| /// would no longer work. | ||
| /// | ||
| /// [CARGO_CFG_TARGET_FAMILY]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#:~:text=CARGO_CFG_TARGET_FAMILY | ||
| /// [cfg-target_family]: https://doc.rust-lang.org/reference/conditional-compilation.html#target_family | ||
| SUSPICIOUS_CARGO_CFG_TARGET_FAMILY_COMPARISONS, | ||
| Warn, | ||
| "comparing `CARGO_CFG_TARGET_FAMILY` env var with a single value", | ||
| @future_incompatible = FutureIncompatibleInfo { | ||
| reason: FutureIncompatibilityReason::FutureReleaseSemanticsChange, | ||
| reference: "issue #100343 <https://github.com/rust-lang/rust/issues/100343>", | ||
| explain_reason: false, | ||
| }; | ||
| } | ||
|
|
||
| #[derive(Default)] | ||
| pub(crate) struct SuspiciousCargoCfgTargetFamilyComparisons { | ||
| /// A side table of locals that are initialized from | ||
| /// `std::env::var("CARGO_CFG_TARGET_FAMILY")` or similar. | ||
| target_family_locals: HirIdSet, | ||
| } | ||
|
|
||
| impl_lint_pass!(SuspiciousCargoCfgTargetFamilyComparisons => [SUSPICIOUS_CARGO_CFG_TARGET_FAMILY_COMPARISONS]); | ||
|
|
||
| #[derive(LintDiagnostic)] | ||
| #[diag(lint_suspicious_cargo_cfg_target_family_comparison)] | ||
| #[note] | ||
| struct SingleValuedComparison { | ||
| #[subdiagnostic] | ||
| sugg: Option<ReplaceWithSplitAny>, | ||
| } | ||
|
|
||
| #[derive(Subdiagnostic)] | ||
| #[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")] | ||
| struct ReplaceWithSplitAny { | ||
| #[suggestion_part(code = "!")] | ||
| negate: Option<Span>, | ||
| #[suggestion_part(code = ".split(',').any(|x| x == ")] | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could suggest |
||
| op: Span, | ||
| #[suggestion_part(code = ")")] | ||
| end: Span, | ||
| } | ||
|
|
||
| #[derive(LintDiagnostic)] | ||
| #[diag(lint_suspicious_cargo_cfg_target_family_match)] | ||
| #[note] | ||
| #[note(lint_suggestion)] | ||
| struct SingleValuedMatch; | ||
|
|
||
| // NOTE: We choose not to do a check for when in a build script, like: | ||
| // matches!(&sess.opts.crate_name, Some(crate_name) if crate_name == "build_script_build") | ||
| // Since we might be building a library that is used as a build script dependency (`cc-rs` etc). | ||
| impl<'tcx> LateLintPass<'tcx> for SuspiciousCargoCfgTargetFamilyComparisons { | ||
| fn check_stmt(&mut self, _cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) { | ||
| // Find locals that are initialized from `CARGO_CFG_TARGET_FAMILY`, and save them for later | ||
| // checking. | ||
| if let StmtKind::Let(stmt) = &stmt.kind { | ||
| if let Some(init) = stmt.init { | ||
| if self.accesses_target_family_env(init) { | ||
| stmt.pat.each_binding(|_, hir_id, _, _| { | ||
| self.target_family_locals.insert(hir_id); | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { | ||
| // Check expressions that do single-valued comparisons. | ||
| match &expr.kind { | ||
| ExprKind::Binary(op, a, b) if matches!(op.node, BinOpKind::Eq | BinOpKind::Ne) => { | ||
| if self.accesses_target_family_env(a) { | ||
| // If this is a &str or String, we can confidently give a `.split()` suggestion. | ||
| let a_ty = cx.typeck_results().expr_ty(a); | ||
| let is_str = matches!( | ||
| a_ty.kind(), | ||
| ty::Ref(_, r, _) if r.is_str(), | ||
| ) || matches!( | ||
| a_ty.ty_adt_def(), | ||
| Some(ty_def) if cx.tcx.is_lang_item(ty_def.did(), LangItem::String), | ||
| ); | ||
| let sugg = is_str.then(|| ReplaceWithSplitAny { | ||
| negate: (op.node == BinOpKind::Ne).then(|| expr.span.shrink_to_lo()), | ||
| op: a.span.between(b.span), | ||
| end: b.span.shrink_to_hi(), | ||
| }); | ||
|
|
||
| cx.emit_span_lint( | ||
| SUSPICIOUS_CARGO_CFG_TARGET_FAMILY_COMPARISONS, | ||
| expr.span, | ||
| SingleValuedComparison { sugg }, | ||
| ); | ||
| } else if self.accesses_target_family_env(b) { | ||
| cx.emit_span_lint( | ||
| SUSPICIOUS_CARGO_CFG_TARGET_FAMILY_COMPARISONS, | ||
| expr.span, | ||
| // Unsure how to emit a suggestion when we need to reorder `a` and `b`. | ||
| SingleValuedComparison { sugg: None }, | ||
| ); | ||
| } | ||
| } | ||
| ExprKind::Match(expr, _, _) if self.accesses_target_family_env(expr) => { | ||
| cx.emit_span_lint( | ||
| SUSPICIOUS_CARGO_CFG_TARGET_FAMILY_COMPARISONS, | ||
| expr.span, | ||
| SingleValuedMatch, | ||
| ); | ||
| } | ||
| // We don't handle method calls like `PartialEq::eq`, that's probably fine though, | ||
| // those are uncommon in real-world code. | ||
| _ => {} | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl SuspiciousCargoCfgTargetFamilyComparisons { | ||
| /// Check if an expression is likely derived from the `CARGO_CFG_TARGET_FAMILY` env var. | ||
| fn accesses_target_family_env(&self, expr: &Expr<'_>) -> bool { | ||
| match &expr.kind { | ||
| // A call to `std::env::var[_os]("CARGO_CFG_TARGET_FAMILY")`. | ||
| // | ||
| // NOTE: This actually matches all functions that take as a single value | ||
| // `"CARGO_CFG_TARGET_FAMILY"`. We could restrict this by matching only functions that | ||
| // match `"std::env::var"` or `"std::env::var_os"` by doing something like: | ||
| // | ||
| // && let Expr { kind: ExprKind::Path(QPath::Resolved(_, path)), .. } = func | ||
| // && let Some(fn_def_id) = path.res.opt_def_id() | ||
| // && cx.tcx.is_diagnostic_item(sym::std_env_var, fn_def_id) | ||
| // | ||
| // But users often define wrapper functions around these, and so we wouldn't catch it | ||
| // when they do. | ||
| // | ||
| // This is probably fine, `"CARGO_CFG_TARGET_FAMILY"` is unique enough of a name that | ||
| // it's unlikely that people will be using it for anything else. | ||
| ExprKind::Call(_, [arg]) | ||
| if let ExprKind::Lit(lit) = &arg.kind | ||
| && lit.node.str() == Some(sym::cargo_cfg_target_family) => | ||
| { | ||
| true | ||
| } | ||
| // On local variables, try to see if it was initialized from target family earlier. | ||
| ExprKind::Path(QPath::Resolved(_, path)) | ||
| if let hir::def::Res::Local(local_hir_id) = &path.res => | ||
| { | ||
| self.target_family_locals.contains(local_hir_id) | ||
| } | ||
| // Recurse through references and dereferences. | ||
| ExprKind::AddrOf(_, _, expr) | ExprKind::Unary(UnOp::Deref, expr) => { | ||
| self.accesses_target_family_env(expr) | ||
| } | ||
| // Recurse on every method call to allow `.unwrap()`, `.as_deref()` and similar. | ||
| // | ||
| // NOTE: We could consider only recursing on specific `Option`/`Result` methods, but the | ||
| // full list of the ones we'd want becomes unwieldy pretty quickly. | ||
| ExprKind::MethodCall(_, receiver, _, _) => self.accesses_target_family_env(receiver), | ||
| _ => false, | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
tests/ui/lint/suspicious_cargo_cfg_target_family_comparisons.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| // Test the `suspicious_cargo_cfg_target_family_comparisons` lint. | ||
|
|
||
| //@ check-pass | ||
| //@ exec-env:CARGO_CFG_TARGET_FAMILY=unix | ||
|
|
||
| use std::env; | ||
|
|
||
| fn main() { | ||
| // Check that direct comparisons warn. | ||
| let is_unix = env::var("CARGO_CFG_TARGET_FAMILY").unwrap() == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
|
|
||
| // But that later usage doesn't warn. | ||
| if is_unix {} | ||
|
|
||
| // Assigning to local variable is fine. | ||
| let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap(); | ||
|
|
||
| // Using local in an `==` comparison. | ||
| if target_family == "unix" { | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| } | ||
|
|
||
| // Using local in a match. | ||
| match &*target_family { | ||
| //~^ WARN matching on `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| "unix" => {} | ||
| _ => {} | ||
| } | ||
|
|
||
| // Correct handling doesn't warn. | ||
| if target_family.contains("unix") {} | ||
| if target_family.split(',').any(|x| x == "unix") {} | ||
|
|
||
| // Test supression. | ||
| #[allow(suspicious_cargo_cfg_target_family_comparisons)] | ||
| let _ = env::var("CARGO_CFG_TARGET_FAMILY").unwrap() == "unix"; | ||
|
|
||
| // Negative comparison. | ||
| let _ = target_family != "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
|
|
||
| // Local variable propagation. | ||
| let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap(); | ||
| let target_family: &str = target_family.as_ref(); | ||
| let _ = target_family == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
|
|
||
| // Custom wrapper. | ||
| fn get_and_track_env_var(env_var_name: &str) -> String { | ||
| // This is actually unnecessary, Cargo already tracks changes to the target family, but it's | ||
| // nonetheless a fairly common pattern. | ||
| println!("cargo:rerun-if-env-changed={env_var_name}"); | ||
| env::var(env_var_name).unwrap() | ||
| } | ||
| let _ = get_and_track_env_var("CARGO_CFG_TARGET_FAMILY") == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
|
|
||
| // Various. | ||
| let _ = ::std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap() == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| let _ = env::var("CARGO_CFG_TARGET_FAMILY").expect("should be set") == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| let _ = env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default() == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| let _ = env::var_os("CARGO_CFG_TARGET_FAMILY").unwrap() == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| let _ = env::var("CARGO_CFG_TARGET_FAMILY") == Ok("unix".to_string()); | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| let _ = env::var_os("CARGO_CFG_TARGET_FAMILY") == Some("unix".into()); | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| let _ = env::var("CARGO_CFG_TARGET_FAMILY").as_deref() == Ok("unix"); | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| let _ = env::var_os("CARGO_CFG_TARGET_FAMILY").as_deref() == Some("unix".as_ref()); | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| let _ = env::var("CARGO_CFG_TARGET_FAMILY").ok().as_deref() == Some("unix".as_ref()); | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
|
|
||
| false_negatives(); | ||
| false_positives(); | ||
| } | ||
|
|
||
| // This lint has many false negatives, the problem is intractable in the general case. | ||
| fn false_negatives() { | ||
| // Cannot detect if the env var is not specified inline (such as when dynamically generated). | ||
| let var = "CARGO_CFG_TARGET_FAMILY"; | ||
| let _ = env::var(var).unwrap() == "unix"; | ||
|
|
||
| // Cannot detect if env var value comes from somewhere more complex. | ||
| fn get_env_var() -> String { | ||
| env::var("CARGO_CFG_TARGET_FAMILY").unwrap() | ||
| } | ||
| let _ = get_env_var() == "unix"; | ||
|
|
||
| // Doesn't detect more complex expressions. | ||
| let _ = std::convert::identity(env::var_os("CARGO_CFG_TARGET_FAMILY").unwrap()) == "unix"; | ||
| let _ = *Box::new(env::var_os("CARGO_CFG_TARGET_FAMILY").unwrap()) == "unix"; | ||
|
|
||
| // Doesn't detect variables that are initialized later. | ||
| let later_init; | ||
| later_init = env::var("CARGO_CFG_TARGET_FAMILY").unwrap(); | ||
| if later_init == "unix" {} | ||
|
|
||
| // Doesn't detect if placed inside a struct. | ||
| struct Target { | ||
| family: String, | ||
| } | ||
| let target = Target { family: env::var("CARGO_CFG_TARGET_FAMILY").unwrap() }; | ||
| if target.family == "unix" {} | ||
| } | ||
|
|
||
| // This lint also has false positives, these are probably unlikely to be hit in practice. | ||
| fn false_positives() { | ||
| // Cannot detect later changes to assigned variable. | ||
| let mut overwritten = env::var("CARGO_CFG_TARGET_FAMILY").unwrap(); | ||
| if true { | ||
| overwritten = "unix".to_string(); | ||
| } | ||
| if overwritten == "unix" {} | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
|
|
||
| // Non-std::env::var usage. | ||
| let _ = std::convert::identity("CARGO_CFG_TARGET_FAMILY") == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
|
|
||
| // Call unusual `Option`/`Result` method, and then compare that result. | ||
| let _ = env::var_os("CARGO_CFG_TARGET_FAMILY").is_some() == true; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
|
|
||
| // Match with match arms that contains checks. | ||
| match env::var("CARGO_CFG_TARGET_FAMILY") { | ||
| //~^ WARN matching on `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| Ok(os) if os.contains("unix") => {} | ||
| _ => {} | ||
| } | ||
|
|
||
| // Unusual method call. | ||
| trait Foo { | ||
| fn returns_string(&self) -> &str { | ||
| "unix" | ||
| } | ||
| } | ||
| impl Foo for String {} | ||
| let _ = env::var("CARGO_CFG_TARGET_FAMILY").unwrap().returns_string() == "unix"; | ||
| //~^ WARN comparing against `CARGO_CFG_TARGET_FAMILY` directly may break in the future | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsure if this is the intended use of a FCW? I would like it to trigger in dependents some day, but there wasn't precedent for using
FutureReleaseSemanticsChangeanywhere.