Skip to content

Commit

Permalink
feat: Add an xtask to generate lint documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Muscraft committed Jun 7, 2024
1 parent c4192bb commit cb95f86
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 1 deletion.
1 change: 1 addition & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
build-man = "run --package xtask-build-man --"
stale-label = "run --package xtask-stale-label --"
bump-check = "run --package xtask-bump-check --"
lint-docs = "run --package xtask-lint-docs --"

[env]
# HACK: Until this is stabilized, `snapbox`s polyfill could get confused
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ jobs:
- run: rustup update stable && rustup default stable
- run: cargo stale-label

lint-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup update stable && rustup default stable
- run: cargo lint-docs --check

# Ensure Cargo.lock is up-to-date
lockfile:
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions crates/xtask-lint-docs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "xtask-lint-docs"
version = "0.1.0"
edition.workspace = true
publish = false

[dependencies]
anyhow.workspace = true
cargo.workspace = true
clap.workspace = true

[lints]
workspace = true
122 changes: 122 additions & 0 deletions crates/xtask-lint-docs/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use cargo::util::command_prelude::{flag, ArgMatchesExt};
use cargo::util::lints::{Lint, LintLevel};
use std::fmt::Write;
use std::path::PathBuf;

struct LintSection {
level: LintLevel,
names: Vec<&'static str>,
docs: String,
}

impl LintSection {
fn new(level: LintLevel) -> Self {
Self {
level,
names: Vec::new(),
docs: String::new(),
}
}

fn add_lint(&mut self, lint: &Lint) {
self.names.push(lint.name);

// We want to add an extra `#` to each heading as it may be unexpected that
// each lint is a subheading of the lint level.
// We only want to do this for headings that are at least `##` as `#` is
// used for comments and links.
let docs = lint.docs.unwrap().replace("## ", "### ");

self.docs.push_str("### `");
self.docs.push_str(lint.name);
self.docs.push_str("`\n");
self.docs.push_str(&docs);
self.docs.push_str("\n");
}

fn write(&self, buf: &mut String) -> std::fmt::Result {
let title = match self.level {
LintLevel::Allow => "Allowed-by-default",
LintLevel::Warn => "Warn-by-default",
LintLevel::Deny => "Deny-by-default",
LintLevel::Forbid => "Forbid-by-default",
};
writeln!(buf, "## {title}\n")?;
writeln!(
buf,
"These lints are all set to the '{}' level by default.",
self.level
)?;
for name in &self.names {
writeln!(buf, "- [`{}`](#{})", name, name)?;
}
writeln!(buf, "\n{}", self.docs)
}
}

fn cli() -> clap::Command {
clap::Command::new("xtask-lint-docs").arg(flag("check", "Check that the docs are up-to-date"))
}

fn main() -> anyhow::Result<()> {
let args = cli().get_matches();
let check = args.flag("check");

let mut allow = LintSection::new(LintLevel::Allow);
let mut warn = LintSection::new(LintLevel::Warn);
let mut deny = LintSection::new(LintLevel::Deny);
let mut forbid = LintSection::new(LintLevel::Forbid);
for lint in cargo::util::lints::LINTS {
if lint.docs.is_some() {
let sectipn = match lint.default_level {
LintLevel::Allow => &mut allow,
LintLevel::Warn => &mut warn,
LintLevel::Deny => &mut deny,
LintLevel::Forbid => &mut forbid,
};
sectipn.add_lint(lint);
}
}

let mut buf = String::new();
writeln!(buf, "# Lints\n")?;
writeln!(
buf,
"Note: [Cargo's linting system is unstable](unstable.md#lintscargo) and can only be used on nightly toolchains"
)?;

if !allow.names.is_empty() {
allow.write(&mut buf)?;
}
if !warn.names.is_empty() {
warn.write(&mut buf)?;
}
if !deny.names.is_empty() {
deny.write(&mut buf)?;
}
if !forbid.names.is_empty() {
forbid.write(&mut buf)?;
}

if check {
let old = std::fs::read_to_string(lint_docs_path())?;
if old != buf {
anyhow::bail!(
"The lints documentation is out-of-date. Run `cargo lint-docs` to update it."
);
}
} else {
std::fs::write(lint_docs_path(), buf)?;
}
Ok(())
}

fn lint_docs_path() -> PathBuf {
let pkg_root = env!("CARGO_MANIFEST_DIR");
let ws_root = PathBuf::from(format!("{pkg_root}/../.."));
let path = {
let path = ws_root.join("src/doc/src/reference/lints.md");
path.canonicalize().unwrap_or(path)
};
path
}
2 changes: 1 addition & 1 deletion src/cargo/util/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::path::Path;
use toml_edit::ImDocument;

const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE];
const LINTS: &[Lint] = &[
pub const LINTS: &[Lint] = &[
IM_A_TEAPOT,
IMPLICIT_FEATURES,
UNKNOWN_LINTS,
Expand Down
1 change: 1 addition & 0 deletions src/doc/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* [SemVer Compatibility](reference/semver.md)
* [Future incompat report](reference/future-incompat-report.md)
* [Reporting build timings](reference/timings.md)
* [Lints](reference/lints.md)
* [Unstable Features](reference/unstable.md)

* [Cargo Commands](commands/index.md)
Expand Down
1 change: 1 addition & 0 deletions src/doc/src/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ The reference covers the details of various areas of Cargo.
* [SemVer Compatibility](semver.md)
* [Future incompat report](future-incompat-report.md)
* [Reporting build timings](timings.md)
* [Lints](lints.md)
* [Unstable Features](unstable.md)
115 changes: 115 additions & 0 deletions src/doc/src/reference/lints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Lints

Note: [Cargo's linting system is unstable](unstable.md#lintscargo) and can only be used on nightly toolchains
## Allowed-by-default

These lints are all set to the 'allow' level by default.
- [`implicit_features`](#implicit_features)

### `implicit_features`

Note: This only runs on edition 2021 and below

#### What it does
Checks for implicit features for optional dependencies

#### Why it is bad
By default, cargo will treat any optional dependency as a [feature]. As of
cargo 1.60, these can be disabled by declaring a feature that activates the
optional dependency as `dep:<name>` (see [RFC #3143]).

In the 2024 edition, `cargo` will stop exposing optional dependencies as
features implicitly, requiring users to add `foo = ["dep:foo"]` if they
still want it exposed.

For more information, see [RFC #3491]

#### Example
```toml
edition = "2021"

[dependencies]
bar = { version = "0.1.0", optional = true }

[features]
# No explicit feature activation for `bar`
```

Instead, the dependency should have an explicit feature:
```toml
edition = "2021"

[dependencies]
bar = { version = "0.1.0", optional = true }

[features]
bar = ["dep:bar"]
```

[feature]: https://doc.rust-lang.org/cargo/reference/features.html
[RFC #3143]: https://rust-lang.github.io/rfcs/3143-cargo-weak-namespaced-features.html
[RFC #3491]: https://rust-lang.github.io/rfcs/3491-remove-implicit-features.html


## Warn-by-default

These lints are all set to the 'warn' level by default.
- [`unknown_lints`](#unknown_lints)
- [`unused_optional_dependency`](#unused_optional_dependency)

### `unknown_lints`

#### What it does
Checks for unknown lints in the `[lints.cargo]` table

#### Why it is bad
- The lint name could be misspelled, leading to confusion as to why it is
not working as expected
- The unknown lint could end up causing an error if `cargo` decides to make
a lint with the same name in the future

#### Example
```toml
[lints.cargo]
this-lint-does-not-exist = "warn"
```

### `unused_optional_dependency`

Note: This only runs on edition 2024+

#### What it does
Checks for optional dependencies that are not activated by any feature

#### Why it is bad
Starting in the 2024 edition, `cargo` no longer implicitly creates features
for optional dependencies (see [RFC #3491]). This means that any optional
dependency not specified with `"dep:<name>"` in some feature is now unused.
This change may be surprising to users who have been using the implicit
features `cargo` has been creating for optional dependencies.

#### Example
```toml
edition = "2024"

[dependencies]
bar = { version = "0.1.0", optional = true }

[features]
# No explicit feature activation for `bar`
```

Instead, the dependency should be removed or activated in a feature:
```toml
edition = "2024"

[dependencies]
bar = { version = "0.1.0", optional = true }

[features]
bar = ["dep:bar"]
```

[RFC #3491]: https://rust-lang.github.io/rfcs/3491-remove-implicit-features.html


0 comments on commit cb95f86

Please sign in to comment.