Skip to content

Commit 7d7b7c2

Browse files
committed
feat: Add an xtask to generate lint documentation
1 parent 30a7eb5 commit 7d7b7c2

File tree

9 files changed

+259
-1
lines changed

9 files changed

+259
-1
lines changed

Diff for: .cargo/config.toml

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
build-man = "run --package xtask-build-man --"
33
stale-label = "run --package xtask-stale-label --"
44
bump-check = "run --package xtask-bump-check --"
5+
lint-docs = "run --package xtask-lint-docs --"
56

67
[env]
78
# HACK: Until this is stabilized, `snapbox`s polyfill could get confused

Diff for: .github/workflows/main.yml

+7
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ jobs:
8383
- run: rustup update stable && rustup default stable
8484
- run: cargo stale-label
8585

86+
lint-docs:
87+
runs-on: ubuntu-latest
88+
steps:
89+
- uses: actions/checkout@v4
90+
- run: rustup update stable && rustup default stable
91+
- run: cargo lint-docs --check
92+
8693
# Ensure Cargo.lock is up-to-date
8794
lockfile:
8895
runs-on: ubuntu-latest

Diff for: Cargo.lock

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: crates/xtask-lint-docs/Cargo.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "xtask-lint-docs"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
publish = false
6+
7+
[dependencies]
8+
anyhow.workspace = true
9+
cargo.workspace = true
10+
clap.workspace = true
11+
itertools.workspace = true
12+
13+
[lints]
14+
workspace = true

Diff for: crates/xtask-lint-docs/src/main.rs

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use cargo::util::command_prelude::{flag, ArgMatchesExt};
2+
use cargo::util::lints::{Lint, LintLevel};
3+
use itertools::Itertools;
4+
use std::fmt::Write;
5+
use std::path::PathBuf;
6+
7+
fn cli() -> clap::Command {
8+
clap::Command::new("xtask-lint-docs").arg(flag("check", "Check that the docs are up-to-date"))
9+
}
10+
11+
fn main() -> anyhow::Result<()> {
12+
let args = cli().get_matches();
13+
let check = args.flag("check");
14+
15+
let mut allow = Vec::new();
16+
let mut warn = Vec::new();
17+
let mut deny = Vec::new();
18+
let mut forbid = Vec::new();
19+
20+
let mut lint_docs = String::new();
21+
for lint in cargo::util::lints::LINTS
22+
.iter()
23+
.sorted_by_key(|lint| lint.name)
24+
{
25+
if lint.docs.is_some() {
26+
let sectipn = match lint.default_level {
27+
LintLevel::Allow => &mut allow,
28+
LintLevel::Warn => &mut warn,
29+
LintLevel::Deny => &mut deny,
30+
LintLevel::Forbid => &mut forbid,
31+
};
32+
sectipn.push(lint.name);
33+
add_lint(lint, &mut lint_docs)?;
34+
}
35+
}
36+
37+
let mut buf = String::new();
38+
writeln!(buf, "# Lints\n")?;
39+
writeln!(
40+
buf,
41+
"Note: [Cargo's linting system is unstable](unstable.md#lintscargo) and can only be used on nightly toolchains"
42+
)?;
43+
writeln!(buf)?;
44+
45+
if !allow.is_empty() {
46+
add_level_section(LintLevel::Allow, &allow, &mut buf)?;
47+
}
48+
if !warn.is_empty() {
49+
add_level_section(LintLevel::Warn, &warn, &mut buf)?;
50+
}
51+
if !deny.is_empty() {
52+
add_level_section(LintLevel::Deny, &deny, &mut buf)?;
53+
}
54+
if !forbid.is_empty() {
55+
add_level_section(LintLevel::Forbid, &forbid, &mut buf)?;
56+
}
57+
58+
buf.push_str(&lint_docs);
59+
60+
if check {
61+
let old = std::fs::read_to_string(lint_docs_path())?;
62+
if old != buf {
63+
anyhow::bail!(
64+
"The lints documentation is out-of-date. Run `cargo lint-docs` to update it."
65+
);
66+
}
67+
} else {
68+
std::fs::write(lint_docs_path(), buf)?;
69+
}
70+
Ok(())
71+
}
72+
73+
fn add_lint(lint: &Lint, buf: &mut String) -> std::fmt::Result {
74+
writeln!(buf, "## `{}`", lint.name)?;
75+
writeln!(buf, "Set to `{}` by default", lint.default_level)?;
76+
writeln!(buf, "{}\n", lint.docs.as_ref().unwrap())
77+
}
78+
79+
fn add_level_section(level: LintLevel, lint_names: &[&str], buf: &mut String) -> std::fmt::Result {
80+
let title = match level {
81+
LintLevel::Allow => "Allowed-by-default",
82+
LintLevel::Warn => "Warn-by-default",
83+
LintLevel::Deny => "Deny-by-default",
84+
LintLevel::Forbid => "Forbid-by-default",
85+
};
86+
writeln!(buf, "## {title}\n")?;
87+
writeln!(
88+
buf,
89+
"These lints are all set to the '{}' level by default.",
90+
level
91+
)?;
92+
93+
for name in lint_names {
94+
writeln!(buf, "- [`{}`](#{})", name, name)?;
95+
}
96+
writeln!(buf)?;
97+
Ok(())
98+
}
99+
100+
fn lint_docs_path() -> PathBuf {
101+
let pkg_root = env!("CARGO_MANIFEST_DIR");
102+
let ws_root = PathBuf::from(format!("{pkg_root}/../.."));
103+
let path = {
104+
let path = ws_root.join("src/doc/src/reference/lints.md");
105+
path.canonicalize().unwrap_or(path)
106+
};
107+
path
108+
}

Diff for: src/cargo/util/lints.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::path::Path;
1313
use toml_edit::ImDocument;
1414

1515
const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE];
16-
const LINTS: &[Lint] = &[
16+
pub const LINTS: &[Lint] = &[
1717
IM_A_TEAPOT,
1818
IMPLICIT_FEATURES,
1919
UNKNOWN_LINTS,

Diff for: src/doc/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
* [SemVer Compatibility](reference/semver.md)
4646
* [Future incompat report](reference/future-incompat-report.md)
4747
* [Reporting build timings](reference/timings.md)
48+
* [Lints](reference/lints.md)
4849
* [Unstable Features](reference/unstable.md)
4950

5051
* [Cargo Commands](commands/index.md)

Diff for: src/doc/src/reference/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ The reference covers the details of various areas of Cargo.
2323
* [SemVer Compatibility](semver.md)
2424
* [Future incompat report](future-incompat-report.md)
2525
* [Reporting build timings](timings.md)
26+
* [Lints](lints.md)
2627
* [Unstable Features](unstable.md)

Diff for: src/doc/src/reference/lints.md

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Lints
2+
3+
Note: [Cargo's linting system is unstable](unstable.md#lintscargo) and can only be used on nightly toolchains
4+
5+
## Allowed-by-default
6+
7+
These lints are all set to the 'allow' level by default.
8+
- [`implicit_features`](#implicit_features)
9+
10+
## Warn-by-default
11+
12+
These lints are all set to the 'warn' level by default.
13+
- [`unknown_lints`](#unknown_lints)
14+
- [`unused_optional_dependency`](#unused_optional_dependency)
15+
16+
## `implicit_features`
17+
Set to `allow` by default
18+
19+
### What it does
20+
Checks for implicit features for optional dependencies
21+
22+
### Why it is bad
23+
By default, cargo will treat any optional dependency as a [feature]. As of
24+
cargo 1.60, these can be disabled by declaring a feature that activates the
25+
optional dependency as `dep:<name>` (see [RFC #3143]).
26+
27+
In the 2024 edition, `cargo` will stop exposing optional dependencies as
28+
features implicitly, requiring users to add `foo = ["dep:foo"]` if they
29+
still want it exposed.
30+
31+
For more information, see [RFC #3491]
32+
33+
### Example
34+
```toml
35+
edition = "2021"
36+
37+
[dependencies]
38+
bar = { version = "0.1.0", optional = true }
39+
40+
[features]
41+
# No explicit feature activation for `bar`
42+
```
43+
44+
Instead, the dependency should have an explicit feature:
45+
```toml
46+
edition = "2021"
47+
48+
[dependencies]
49+
bar = { version = "0.1.0", optional = true }
50+
51+
[features]
52+
bar = ["dep:bar"]
53+
```
54+
55+
[feature]: https://doc.rust-lang.org/cargo/reference/features.html
56+
[RFC #3143]: https://rust-lang.github.io/rfcs/3143-cargo-weak-namespaced-features.html
57+
[RFC #3491]: https://rust-lang.github.io/rfcs/3491-remove-implicit-features.html
58+
59+
60+
## `unknown_lints`
61+
Set to `warn` by default
62+
63+
### What it does
64+
Checks for unknown lints in the `[lints.cargo]` table
65+
66+
### Why it is bad
67+
- The lint name could be misspelled, leading to confusion as to why it is
68+
not working as expected
69+
- The unknown lint could end up causing an error if `cargo` decides to make
70+
a lint with the same name in the future
71+
72+
### Example
73+
```toml
74+
[lints.cargo]
75+
this-lint-does-not-exist = "warn"
76+
```
77+
78+
79+
## `unused_optional_dependency`
80+
Set to `warn` by default
81+
82+
### What it does
83+
Checks for optional dependencies that are not activated by any feature
84+
85+
### Why it is bad
86+
Starting in the 2024 edition, `cargo` no longer implicitly creates features
87+
for optional dependencies (see [RFC #3491]). This means that any optional
88+
dependency not specified with `"dep:<name>"` in some feature is now unused.
89+
This change may be surprising to users who have been using the implicit
90+
features `cargo` has been creating for optional dependencies.
91+
92+
### Example
93+
```toml
94+
edition = "2024"
95+
96+
[dependencies]
97+
bar = { version = "0.1.0", optional = true }
98+
99+
[features]
100+
# No explicit feature activation for `bar`
101+
```
102+
103+
Instead, the dependency should be removed or activated in a feature:
104+
```toml
105+
edition = "2024"
106+
107+
[dependencies]
108+
bar = { version = "0.1.0", optional = true }
109+
110+
[features]
111+
bar = ["dep:bar"]
112+
```
113+
114+
[RFC #3491]: https://rust-lang.github.io/rfcs/3491-remove-implicit-features.html
115+
116+

0 commit comments

Comments
 (0)