Skip to content

Commit fe383f9

Browse files
committed
feat: Add a typos CI job
1 parent 7590ff6 commit fe383f9

File tree

7 files changed

+291
-0
lines changed

7 files changed

+291
-0
lines changed

.cargo/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ build-man = "run --package xtask-build-man --"
33
stale-label = "run --package xtask-stale-label --"
44
bump-check = "run --package xtask-bump-check --"
55
lint-docs = "run --package xtask-lint-docs --"
6+
spellcheck = "run --package xtask-spellcheck --"
67

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

.github/workflows/main.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
- resolver
2929
- rustfmt
3030
- schema
31+
- spellcheck
3132
- test
3233
- test_gitoxide
3334
permissions:
@@ -304,3 +305,12 @@ jobs:
304305
- uses: actions/checkout@v5
305306
- uses: taiki-e/install-action@cargo-hack
306307
- run: cargo hack check --all-targets --rust-version --workspace --ignore-private --locked
308+
309+
spellcheck:
310+
name: Spell Check with Typos
311+
runs-on: ubuntu-latest
312+
steps:
313+
- name: Checkout Actions Repository
314+
uses: actions/checkout@v5
315+
- name: Spell Check Repo
316+
uses: crate-ci/[email protected]

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/xtask-spellcheck/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "xtask-spellcheck"
3+
version = "0.0.0"
4+
edition.workspace = true
5+
publish = false
6+
7+
[dependencies]
8+
anyhow.workspace = true
9+
cargo_metadata.workspace = true
10+
cargo-util.workspace = true
11+
clap.workspace = true
12+
semver.workspace = true
13+
14+
[lints]
15+
workspace = true
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#![allow(clippy::disallowed_methods)]
2+
#![allow(clippy::print_stderr)]
3+
#![allow(clippy::print_stdout)]
4+
5+
use anyhow::Result;
6+
use cargo_metadata::{Metadata, MetadataCommand};
7+
use clap::{Arg, ArgAction};
8+
use semver::Version;
9+
use std::{
10+
env, io,
11+
path::{Path, PathBuf},
12+
process::Command,
13+
};
14+
15+
const BIN_NAME: &str = "typos";
16+
const PKG_NAME: &str = "typos-cli";
17+
const TYPOS_STEP_PREFIX: &str = " uses: crate-ci/typos@v";
18+
19+
fn main() -> anyhow::Result<()> {
20+
let cli = cli();
21+
exec(&cli.get_matches())?;
22+
Ok(())
23+
}
24+
25+
pub fn cli() -> clap::Command {
26+
clap::Command::new("xtask-spellcheck")
27+
.arg(
28+
Arg::new("color")
29+
.long("color")
30+
.help("Coloring: auto, always, never")
31+
.action(ArgAction::Set)
32+
.value_name("WHEN")
33+
.global(true),
34+
)
35+
.arg(
36+
Arg::new("quiet")
37+
.long("quiet")
38+
.short('q')
39+
.help("Do not print cargo log messages")
40+
.action(ArgAction::SetTrue)
41+
.global(true),
42+
)
43+
.arg(
44+
Arg::new("verbose")
45+
.long("verbose")
46+
.short('v')
47+
.help("Use verbose output (-vv very verbose/build.rs output)")
48+
.action(ArgAction::Count)
49+
.global(true),
50+
)
51+
.arg(
52+
Arg::new("write-changes")
53+
.long("write-changes")
54+
.short('w')
55+
.help("Write fixes out")
56+
.action(ArgAction::SetTrue)
57+
.global(true),
58+
)
59+
}
60+
61+
pub fn exec(matches: &clap::ArgMatches) -> Result<()> {
62+
let mut args = vec![];
63+
64+
match matches.get_one::<String>("color") {
65+
Some(c) if matches!(c.as_str(), "auto" | "always" | "never") => {
66+
args.push("--color");
67+
args.push(c);
68+
}
69+
Some(c) => {
70+
anyhow::bail!(
71+
"argument for --color must be auto, always, or \
72+
never, but found `{}`",
73+
c
74+
);
75+
}
76+
_ => {}
77+
}
78+
79+
if matches.get_flag("quiet") {
80+
args.push("--quiet");
81+
}
82+
83+
let verbose_count = matches.get_count("verbose");
84+
85+
for _ in 0..verbose_count {
86+
args.push("--verbose");
87+
}
88+
if matches.get_flag("write-changes") {
89+
args.push("--write-changes");
90+
}
91+
92+
let metadata = MetadataCommand::new()
93+
.exec()
94+
.expect("cargo_metadata failed");
95+
96+
let required_version = extract_workflow_typos_version(&metadata)?;
97+
98+
let outdir = metadata.target_directory.as_std_path().join("tmp");
99+
let workspace_root = metadata.workspace_root.as_path().as_std_path();
100+
let bin_path = crate::ensure_version_or_cargo_install(&outdir, required_version)?;
101+
102+
eprintln!("running {BIN_NAME}");
103+
Command::new(bin_path)
104+
.current_dir(workspace_root)
105+
.args(args)
106+
.status()?;
107+
108+
Ok(())
109+
}
110+
111+
fn extract_workflow_typos_version(metadata: &Metadata) -> anyhow::Result<Version> {
112+
let ws_root = metadata.workspace_root.as_path().as_std_path();
113+
let workflow_path = ws_root.join(".github").join("workflows").join("main.yml");
114+
let file_content = std::fs::read_to_string(workflow_path)?;
115+
116+
if let Some(line) = file_content
117+
.lines()
118+
.find(|line| line.contains(TYPOS_STEP_PREFIX))
119+
&& let Some(stripped) = line.strip_prefix(TYPOS_STEP_PREFIX)
120+
&& let Ok(v) = Version::parse(stripped)
121+
{
122+
Ok(v)
123+
} else {
124+
Err(anyhow::anyhow!("Could not find typos version in workflow"))
125+
}
126+
}
127+
128+
/// If the given executable is installed with the given version, use that,
129+
/// otherwise install via cargo.
130+
pub fn ensure_version_or_cargo_install(
131+
build_dir: &Path,
132+
required_version: Version,
133+
) -> io::Result<PathBuf> {
134+
// Check if the user has a sufficient version already installed
135+
let bin_path = PathBuf::from(BIN_NAME).with_extension(env::consts::EXE_EXTENSION);
136+
if let Some(user_version) = get_typos_version(&bin_path) {
137+
if user_version >= required_version {
138+
return Ok(bin_path);
139+
}
140+
}
141+
142+
let tool_root_dir = build_dir.join("misc-tools");
143+
let tool_bin_dir = tool_root_dir.join("bin");
144+
let bin_path = tool_bin_dir
145+
.join(BIN_NAME)
146+
.with_extension(env::consts::EXE_EXTENSION);
147+
148+
// Check if we have already installed sufficient version
149+
if let Some(misc_tools_version) = get_typos_version(&bin_path) {
150+
if misc_tools_version >= required_version {
151+
return Ok(bin_path);
152+
}
153+
}
154+
155+
eprintln!("required `typos` version ({required_version}) not found, building from source");
156+
// use --force to ensure that if the required version is bumped, we update it.
157+
// use --target-dir to ensure we have a build cache so repeated invocations aren't slow.
158+
// modify PATH so that cargo doesn't print a warning telling the user to modify the path.
159+
let mut cmd = Command::new("cargo");
160+
cmd.args(["install", "--locked", "--force", "--quiet"])
161+
.arg("--root")
162+
.arg(&tool_root_dir)
163+
.arg("--target-dir")
164+
.arg(tool_root_dir.join("target"))
165+
.arg(format!("{PKG_NAME}@{required_version}"))
166+
.env(
167+
"PATH",
168+
env::join_paths(
169+
env::split_paths(&env::var("PATH").unwrap())
170+
.chain(std::iter::once(tool_bin_dir.clone())),
171+
)
172+
.expect("build dir contains invalid char"),
173+
);
174+
175+
// On CI, we set opt-level flag for quicker installation.
176+
// Since lower opt-level decreases the tool's performance,
177+
// we don't set this option on local.
178+
if cargo_util::is_ci() {
179+
cmd.env("RUSTFLAGS", "-Copt-level=0");
180+
}
181+
182+
let cargo_exit_code = cmd.spawn()?.wait()?;
183+
if !cargo_exit_code.success() {
184+
return Err(io::Error::other("cargo install failed"));
185+
}
186+
assert!(
187+
matches!(bin_path.try_exists(), Ok(true)),
188+
"cargo install did not produce the expected binary"
189+
);
190+
eprintln!("finished {BIN_NAME}");
191+
Ok(bin_path)
192+
}
193+
194+
fn get_typos_version(bin: &PathBuf) -> Option<Version> {
195+
// ignore the process exit code here and instead just let the version number check fail
196+
if let Ok(output) = Command::new(&bin).arg("--version").output()
197+
&& let Ok(s) = String::from_utf8(output.stdout)
198+
&& let Some(version_str) = s.trim().split_whitespace().last()
199+
{
200+
Version::parse(version_str).ok()
201+
} else {
202+
None
203+
}
204+
}

src/doc/contrib/src/process/working-on-cargo.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ Some guidelines on working on a change:
9999
* Include tests that cover all non-trivial code. See the [Testing chapter] for
100100
more about writing and running tests.
101101
* All code should be warning-free. This is checked during tests.
102+
* All changes should be free of typos. Cargo's CI has a job that runs [`typos`]
103+
to enforce this. You can use `cargo spellcheck` to run this check locally,
104+
and `cargo spellcheck --write-changes` to fix most typos automatically.
105+
106+
[`typos`]: https://github.com/crate-ci/typos
102107

103108
## Submitting a Pull Request
104109

typos.toml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[files]
2+
extend-exclude = [
3+
"crates/resolver-tests/*",
4+
"LICENSE-THIRD-PARTY",
5+
"tests/testsuite/script/rustc_fixtures",
6+
]
7+
8+
[default]
9+
extend-ignore-re = [
10+
# Handles ssh keys
11+
"AAAA[0-9A-Za-z+/]+[=]{0,3}",
12+
13+
# Handles paseto from login tests
14+
"k3[.](secret|public)[.][a-zA-Z0-9_-]+",
15+
]
16+
extend-ignore-identifiers-re = [
17+
# Handles git short SHA-1 hashes
18+
"[a-f0-9]{8,9}",
19+
]
20+
extend-ignore-words-re = [
21+
# words with length <= 4 chars is likely noise
22+
"^[a-zA-Z]{1,4}$",
23+
]
24+
25+
[default.extend-identifiers]
26+
# This comes from `windows_sys`
27+
ERROR_FILENAME_EXCED_RANGE = "ERROR_FILENAME_EXCED_RANGE"
28+
29+
# Name of a dependency
30+
flate2 = "flate2"
31+
32+
[default.extend-words]
33+
filetimes = "filetimes"
34+
35+
[type.cargo_command]
36+
extend-glob = ["cargo_command.rs"]
37+
38+
[type.cargo_command.extend-words]
39+
biuld = "biuld"
40+
41+
[type.random-sample]
42+
extend-glob = ["random-sample"]
43+
44+
[type.random-sample.extend-words]
45+
objekt = "objekt"

0 commit comments

Comments
 (0)