Skip to content

Commit bed778e

Browse files
committed
Lint against some unexpected target cfgs
with the help of the Cargo lints infra
1 parent d8d70f4 commit bed778e

File tree

5 files changed

+260
-23
lines changed

5 files changed

+260
-23
lines changed

src/cargo/core/workspace.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::util::context::FeatureUnification;
2525
use crate::util::edit_distance;
2626
use crate::util::errors::{CargoResult, ManifestError};
2727
use crate::util::interning::InternedString;
28-
use crate::util::lints::{analyze_cargo_lints_table, check_im_a_teapot};
28+
use crate::util::lints::{analyze_cargo_lints_table, check_im_a_teapot, unexpected_target_cfgs};
2929
use crate::util::toml::{read_manifest, InheritableFields};
3030
use crate::util::{
3131
context::CargoResolverConfig, context::ConfigRelativePath, context::IncompatibleRustVersions,
@@ -1273,6 +1273,7 @@ impl<'gctx> Workspace<'gctx> {
12731273
self.gctx,
12741274
)?;
12751275
check_im_a_teapot(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?;
1276+
unexpected_target_cfgs(self, pkg, &path, &cargo_lints, &mut error_count, self.gctx)?;
12761277
if error_count > 0 {
12771278
Err(crate::util::errors::AlreadyPrintedError::new(anyhow!(
12781279
"encountered {error_count} errors(s) while running lints"

src/cargo/util/lints.rs

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
use crate::core::{Edition, Feature, Features, Manifest, Package};
1+
use crate::core::compiler::{CompileKind, TargetInfo};
2+
use crate::core::{Edition, Feature, Features, Manifest, Package, Workspace};
23
use crate::{CargoResult, GlobalContext};
34
use annotate_snippets::{Level, Snippet};
5+
use cargo_platform::{Cfg, ExpectedValues, Platform};
46
use cargo_util_schemas::manifest::{TomlLintLevel, TomlToolLints};
57
use pathdiff::diff_paths;
8+
use std::borrow::Cow;
69
use std::fmt::Display;
710
use std::ops::Range;
811
use std::path::Path;
912
use toml_edit::ImDocument;
1013

1114
const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE];
12-
pub const LINTS: &[Lint] = &[IM_A_TEAPOT, UNKNOWN_LINTS];
15+
pub const LINTS: &[Lint] = &[IM_A_TEAPOT, UNEXPECTED_CFGS, UNKNOWN_LINTS];
1316

1417
pub fn analyze_cargo_lints_table(
1518
pkg: &Package,
@@ -605,6 +608,164 @@ fn output_unknown_lints(
605608
Ok(())
606609
}
607610

611+
const UNEXPECTED_CFGS: Lint = Lint {
612+
name: "unexpected_cfgs",
613+
desc: "lint on unexpected target cfgs",
614+
groups: &[],
615+
default_level: LintLevel::Warn,
616+
edition_lint_opts: None,
617+
feature_gate: None,
618+
docs: Some(
619+
r#"
620+
### What it does
621+
Checks for unexpected cfgs in `[target.'cfg(...)']`
622+
623+
### Why it is bad
624+
The lint helps with verifying that the crate is correctly handling conditional
625+
compilation for different target platforms. It ensures that the cfg settings are
626+
consistent between what is intended and what is used, helping to
627+
catch potential bugs or errors early in the development process.
628+
629+
### Example
630+
```toml
631+
[lints.cargo]
632+
unexpected_cfgs = "warn"
633+
```
634+
"#,
635+
),
636+
};
637+
638+
pub fn unexpected_target_cfgs(
639+
ws: &Workspace<'_>,
640+
pkg: &Package,
641+
path: &Path,
642+
pkg_lints: &TomlToolLints,
643+
error_count: &mut usize,
644+
gctx: &GlobalContext,
645+
) -> CargoResult<()> {
646+
let manifest = pkg.manifest();
647+
648+
let (lint_level, _lint_reason) =
649+
UNEXPECTED_CFGS.level(pkg_lints, manifest.edition(), manifest.unstable_features());
650+
651+
if lint_level == LintLevel::Allow {
652+
return Ok(());
653+
}
654+
655+
let rustc = gctx.load_global_rustc(Some(ws))?;
656+
// FIXME: While it doesn't doesn't really matter for `--print=check-cfg`, we should
657+
// still pass the actual requested targets instead of an empty array so that the
658+
// target info can always be de-duplicated.
659+
// FIXME: Move the target info creation before any linting is done and re-use it for
660+
// all the packages.
661+
let compile_kinds = CompileKind::from_requested_targets(gctx, &[])?;
662+
let target_info = TargetInfo::new(gctx, &compile_kinds, &rustc, CompileKind::Host)?;
663+
664+
let Some(global_check_cfg) = target_info.check_cfg() else {
665+
return Ok(());
666+
};
667+
668+
if !global_check_cfg.exhaustive {
669+
return Ok(());
670+
}
671+
672+
// FIXME: If the `[lints.rust.unexpected_cfgs.check-cfg]` config is set we should
673+
// re-fetch the check-cfg informations with those extra args
674+
675+
for dep in pkg.summary().dependencies() {
676+
let Some(platform) = dep.platform() else {
677+
continue;
678+
};
679+
let Platform::Cfg(cfg_expr) = platform else {
680+
continue;
681+
};
682+
683+
cfg_expr.walk(|cfg| -> CargoResult<()> {
684+
let (name, value) = match cfg {
685+
Cfg::Name(name) => (name, None),
686+
Cfg::KeyPair(name, value) => (name, Some(value.to_string())),
687+
};
688+
689+
match global_check_cfg.expecteds.get(name.as_str()) {
690+
Some(ExpectedValues::Some(values)) if !values.contains(&value) => {
691+
let level = lint_level.to_diagnostic_level();
692+
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
693+
*error_count += 1;
694+
}
695+
696+
let value = match value {
697+
Some(value) => Cow::from(format!("`{value}`")),
698+
None => Cow::from("(none)"),
699+
};
700+
701+
// Retrieving the span can fail if our pretty-priting doesn't match what the
702+
// user wrote: `unix="foo"` and `unix = "foo"`, for that reason we don't fail
703+
// and just produce a slightly worth diagnostic.
704+
if let Some(span) = get_span(manifest.document(), &["target", &*format!("cfg({cfg_expr})")], false) {
705+
let manifest_path = rel_cwd_manifest_path(path, gctx);
706+
let title = format!(
707+
"unexpected `cfg` condition value: {value} for `{cfg}`"
708+
);
709+
let diag = level.title(&*title).snippet(
710+
Snippet::source(manifest.contents())
711+
.origin(&manifest_path)
712+
.annotation(level.span(span))
713+
.fold(true)
714+
);
715+
gctx.shell().print_message(diag)?;
716+
} else {
717+
let title = format!(
718+
"unexpected `cfg` condition value: {value} for `{cfg}` in `[target.'cfg({cfg_expr})']`"
719+
);
720+
let help_where = format!("occurred in `{path}`", path = path.display());
721+
let diag = level.title(&*title).footer(Level::Help.title(&*help_where));
722+
gctx.shell().print_message(diag)?;
723+
}
724+
}
725+
None => {
726+
let level = lint_level.to_diagnostic_level();
727+
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
728+
*error_count += 1;
729+
}
730+
731+
let for_cfg = match value {
732+
Some(_value) => Cow::from(format!(" for `{cfg}`")),
733+
None => Cow::from(""),
734+
};
735+
736+
// Retrieving the span can fail if our pretty-priting doesn't match what the
737+
// user wrote: `unix="foo"` and `unix = "foo"`, for that reason we don't fail
738+
// and just produce a slightly worth diagnostic.
739+
if let Some(span) = get_span(manifest.document(), &["target", &*format!("cfg({cfg_expr})")], false) {
740+
let manifest_path = rel_cwd_manifest_path(path, gctx);
741+
let title = format!(
742+
"unexpected `cfg` condition name: {name}{for_cfg}"
743+
);
744+
let diag = level.title(&*title).snippet(
745+
Snippet::source(manifest.contents())
746+
.origin(&manifest_path)
747+
.annotation(level.span(span))
748+
.fold(true)
749+
);
750+
gctx.shell().print_message(diag)?;
751+
} else {
752+
let title = format!(
753+
"unexpected `cfg` condition name: {name}{for_cfg} in `[target.'cfg({cfg_expr})']`"
754+
);
755+
let help_where = format!("occurred in `{path}`", path = path.display());
756+
let diag = level.title(&*title).footer(Level::Help.title(&*help_where));
757+
gctx.shell().print_message(diag)?;
758+
}
759+
}
760+
_ => { /* not unexpected */ }
761+
}
762+
Ok(())
763+
})?;
764+
}
765+
766+
Ok(())
767+
}
768+
608769
#[cfg(test)]
609770
mod tests {
610771
use itertools::Itertools;

src/doc/src/reference/lints.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,28 @@ Note: [Cargo's linting system is unstable](unstable.md#lintscargo) and can only
55
## Warn-by-default
66

77
These lints are all set to the 'warn' level by default.
8+
- [`unexpected_cfgs`](#unexpected_cfgs)
89
- [`unknown_lints`](#unknown_lints)
910

11+
## `unexpected_cfgs`
12+
Set to `warn` by default
13+
14+
### What it does
15+
Checks for unexpected cfgs in `[target.'cfg(...)']`
16+
17+
### Why it is bad
18+
The lint helps with verifying that the crate is correctly handling conditional
19+
compilation for different target platforms. It ensures that the cfg settings are
20+
consistent between what is intended and what is used, helping to
21+
catch potential bugs or errors early in the development process.
22+
23+
### Example
24+
```toml
25+
[lints.cargo]
26+
unexpected_cfgs = "warn"
27+
```
28+
29+
1030
## `unknown_lints`
1131
Set to `warn` by default
1232

src/doc/src/reference/unstable.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1938,7 +1938,7 @@ This feature checks for unexpected cfgs in `[target.'cfg(...)']` entries, based
19381938
on `rustc --print=check-cfg`.
19391939

19401940
```sh
1941-
cargo check -Zcheck-target-cfgs
1941+
cargo check -Zcargo-lints -Zcheck-target-cfgs
19421942
```
19431943

19441944
It follows the lint Rust `unexpected_cfgs` lint configuration:

0 commit comments

Comments
 (0)