swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/docs/rust-linting-proposal.md
blob: 53b9ae2de01925a8caf7ad0228c04f7b93a8d292 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# Rust Starter Pack: Industrial-Grade Linting Proposal

## Thesis

The best current design is:

1. Make the root `Cargo.toml` the single source of truth for lint levels.
2. Make every member crate opt into workspace lint inheritance with:

   ```toml
   [lints]
   workspace = true
   ```

3. Keep the runner orchestration-only: source-file cap, `fmt --check`, `clippy`, tests, then optional deeper gates.
4. Pin an exact stable toolchain so lint drift is a conscious upgrade event.
5. Centralize repo-wide Clippy carve-outs and structural thresholds in the root manifest, and force local suppressions to carry reasons.

The local corpus already converges on this. The newer `adequate_*`, `memcp`, `outpost`, and `picmash` workspaces are structurally superior to the older `check.py` / shell-script pattern because they put policy in manifest tables instead of command-line tails.

One extra local fact matters: there is effectively no checked-in Rust CI posture in this corpus. The real gate is the repo-local checker. That makes it even more important that the checker stay a thin wrapper around manifest-owned policy instead of becoming a second policy source.

One more distinction matters: Clippy’s `too_many_lines` is about function bodies, not whole source files. If the goal is to force large modules to split, the starter pack needs an explicit file-level check instead of pretending the existing lint already covers it.

## Local Survey

### The old shape

These repos encode lint policy inside scripts:

- [empty_status/scripts/check.py](/home/main/programming/projects/empty_status/scripts/check.py)
- [libgrid/scripts/check_lib.py](/home/main/programming/projects/libgrid/scripts/check_lib.py)
- [swarm.moe/landing/scripts/check.py](/home/main/programming/projects/swarm.moe/landing/scripts/check.py)
- [swarm.moe/tongue/scripts/rust-clippy-pedantic.sh](/home/main/programming/projects/swarm.moe/tongue/scripts/rust-clippy-pedantic.sh)
- [swarm.moe/hyperkiki/scripts/rust-clippy-pedantic.sh](/home/main/programming/projects/swarm.moe/hyperkiki/scripts/rust-clippy-pedantic.sh)

That pattern works, but it has chronic defects:

- the same allow/deny list gets duplicated across scripts, CI, and editor settings
- repo policy hides inside shell noise instead of living in the manifest
- inheritance is implicit rather than explicit
- the runner accumulates semantic policy instead of staying a dumb sequencer

### The better local shape

These repos move policy into workspace lint tables:

- [adequate_rust_mcp/Cargo.toml](/home/main/programming/projects/adequate_rust_mcp/Cargo.toml)
- [adequate_aws_mcp/Cargo.toml](/home/main/programming/projects/adequate_aws_mcp/Cargo.toml)
- [adequate_sloccount/Cargo.toml](/home/main/programming/projects/adequate_sloccount/Cargo.toml)
- [memcp/Cargo.toml](/home/main/programming/projects/memcp/Cargo.toml)
- [swarm.moe/outpost/Cargo.toml](/home/main/programming/projects/swarm.moe/outpost/Cargo.toml)
- [picmash/Cargo.toml](/home/main/programming/projects/picmash/Cargo.toml)

And then use thin runners such as:

- [adequate_rust_mcp/check.py](/home/main/programming/projects/adequate_rust_mcp/check.py)
- [picmash/check.py](/home/main/programming/projects/picmash/check.py)

This is the correct direction. The starter pack should canonize it.

### Local convergence that looks real

Across the stricter workspaces, the same repo-wide Clippy exceptions keep reappearing:

- doc-detail noise: `missing_errors_doc`, `missing_panics_doc`
- threshold/style noise: `too_many_lines`, `too_many_arguments`, `items_after_statements`
- dense-symbol domains: `similar_names`, `many_single_char_names`, `module_name_repetitions`, `struct_field_names`
- numeric pragmatism: `cast_*`, `float_cmp`, `implicit_hasher`
- blanket-annotation fatigue: `must_use_candidate`, `return_self_not_must_use`
- API-shape micro-optimizations: `needless_pass_by_value`, `trivially_copy_pass_by_ref`, `ref_option`, `unused_async`

That is not random drift anymore. It is a de facto house profile and should be made explicit.

Just as notable is what the local corpus mostly does *not* use:

- no `clippy.toml` as the primary home of lint levels
- no checked-in GitHub Actions workflow for Rust checks
- almost no dependency-hygiene rail beyond [empty_status/.pre-commit-config.yaml](/home/main/programming/projects/empty_status/.pre-commit-config.yaml) using `cargo-deny` and [libgrid/scripts/check_full.py](/home/main/programming/projects/libgrid/scripts/check_full.py) using `cargo machete`

That absence is also signal. The local ecosystem has already selected manifest-owned lint policy plus a repo-local runner as the durable core.

## What Current Upstream Supports

### 1. Root manifest lint tables are the right source of truth

Cargo has stable manifest lint tables, including workspace inheritance. The root workspace can define lint policy in `[workspace.lints.*]`, and each member crate must opt into it with `[lints] workspace = true`.

Implication:

- the starter pack should treat root `Cargo.toml` as the canonical lint policy
- forgetting `[lints] workspace = true` is a real failure mode and should be considered a policy violation

### 2. Cargo’s own lint namespace is still unstable

Cargo also has a distinct lint namespace under `[lints.cargo]`, but that remains nightly-only / unstable.

Important naming collision:

- `clippy` has a stable lint group named `cargo`
- Cargo itself has an unstable manifest namespace named `cargo`

They are not the same thing. The starter pack should use stable `rust`, `rustdoc`, and `clippy` tables now, and avoid depending on `[lints.cargo]`.

### 3. `clippy.toml` is for configuration knobs, not the main allow/deny architecture

Clippy supports `clippy.toml` / `.clippy.toml`, but its configuration-file lookup is documented as unstable. That makes it the wrong place for the primary policy lattice.

Use it only when a lint exposes configuration you genuinely want, for example:

- `allow-expect-in-tests = true`
- `allow-unwrap-in-tests = true`
- `allow-panic-in-tests = true`

Implication:

- put lint levels in `Cargo.toml`
- keep `clippy.toml` tiny
- do not split the core allow/deny story across manifest tables and shell flags

### 4. Clippy’s own guidance supports a strict but curated baseline

Official Clippy guidance makes three things clear:

- `pedantic` is intentionally noisy
- `restriction` is not intended to be enabled wholesale
- `nursery` is under development and unstable in spirit even when available on stable toolchains

Inference:

- `pedantic = deny` is fine only if paired with a curated central allowlist
- `restriction` should be cherry-picked lint by lint
- `nursery` should not be part of the default starter-pack baseline

That matches the good local repos, except [picmash/Cargo.toml](/home/main/programming/projects/picmash/Cargo.toml), which also enables `nursery = deny`. I would keep that as an opt-in experiment profile, not the default starter profile.

### 5. `--no-deps` is not the workspace-wide default we want

Clippy’s usage docs are explicit here: `--no-deps` is for linting only a selected package, excluding workspace-member path dependencies, and the documented form is `cargo clippy -p example -- --no-deps`.

That is useful for a narrow crate-local loop, but it is the wrong default for a workspace-wide industrial gate. The starter profile should lint the whole workspace, not silently skip member crates that happen to be path dependencies.

### 6. `rust-version` is part of lint policy

Clippy uses the crate’s MSRV configuration, and the docs note that it defaults to the `rust-version` field in `Cargo.toml`.

Implication:

- every starter-pack workspace should set `rust-version`
- the field should reflect the actual supported minimum, not a decorative guess

### 7. Pinned toolchains are part of the enforcement story

The rustup toolchain file supports exact channels, components, profile, and targets. For strict linting, that matters because Clippy output changes over time.

Implication:

- pin an exact stable patch release in `rust-toolchain.toml`
- include `clippy` and `rustfmt`
- prefer `profile = "minimal"` unless the repo has a real reason not to

### 8. `unexpected_cfgs` plus `check-cfg` is one of the highest-value newer strengthenings

Rust now supports checking custom `cfg` names and values via the `unexpected_cfgs` lint and Cargo-driven `check-cfg` configuration.

Implication:

- `unexpected_cfgs` belongs in the starter baseline
- any repo with custom cfgs should declare them explicitly instead of letting misspellings drift silently

### 9. Rustdoc belongs in the linting story

Rustdoc has its own lint namespace, including `broken_intra_doc_links` and `bare_urls`.

Implication:

- library-heavy repos should define `[workspace.lints.rustdoc]`
- the deeper check posture should include a docs build

## Proposed Starter-Pack Design

### 1. Source-of-truth layout

Use a root `Cargo.toml` like this:

```toml
[workspace]
members = ["crates/*"]
resolver = "3"

[workspace.package]
edition = "2024"
rust-version = "1.xx"
version = "0.1.0"

[workspace.lints.rust]
elided_lifetimes_in_paths = "deny"
unexpected_cfgs = "deny"
unsafe_code = "deny"
unused_crate_dependencies = "warn"
unused_lifetimes = "deny"
unused_qualifications = "deny"
unused_results = "deny"

[workspace.lints.rustdoc]
broken_intra_doc_links = "deny"
bare_urls = "deny"

[workspace.lints.clippy]
all = { level = "deny", priority = -2 }
pedantic = { level = "deny", priority = -1 }
cargo = { level = "warn", priority = -3 }

dbg_macro = "deny"
expect_used = "deny"
panic = "deny"
todo = "deny"
unimplemented = "deny"
unwrap_used = "deny"
allow_attributes_without_reason = "deny"

cargo_common_metadata = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
multiple_crate_versions = "allow"

items_after_statements = "allow"
many_single_char_names = "allow"
match_same_arms = "allow"
module_name_repetitions = "allow"
similar_names = "allow"
struct_field_names = "allow"
too_many_arguments = "allow"
too_many_lines = "allow"
unnested_or_patterns = "allow"

cast_lossless = "allow"
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
float_cmp = "allow"
implicit_hasher = "allow"
manual_let_else = "allow"
map_unwrap_or = "allow"
uninlined_format_args = "allow"

ignored_unit_patterns = "allow"
must_use_candidate = "allow"
needless_pass_by_value = "allow"
no_effect_underscore_binding = "allow"
redundant_closure_for_method_calls = "allow"
ref_option = "allow"
return_self_not_must_use = "allow"
trivially_copy_pass_by_ref = "allow"
unused_async = "allow"
used_underscore_binding = "allow"

[workspace.metadata.rust-starter]
format_command = ["cargo", "fmt", "--all", "--check"]
clippy_command = [
  "cargo",
  "clippy",
  "--workspace",
  "--all-targets",
  "--all-features",
  "--",
  "-D",
  "warnings",
]
test_command = ["cargo", "test", "--workspace", "--all-targets", "--all-features"]
doc_command = ["cargo", "doc", "--workspace", "--all-features", "--no-deps"]
canonicalize_commands = [
  [
    "cargo",
    "fix",
    "--workspace",
    "--all-targets",
    "--all-features",
    "--allow-dirty",
    "--allow-staged",
    "--allow-no-vcs",
  ],
  [
    "cargo",
    "clippy",
    "--fix",
    "--workspace",
    "--all-targets",
    "--all-features",
    "--allow-dirty",
    "--allow-staged",
    "--allow-no-vcs",
  ],
  ["cargo", "fmt", "--all"],
]
```

And every member crate should contain:

```toml
[lints]
workspace = true
```

Why this split is right:

- `Cargo.toml` owns semantics
- `workspace.metadata` owns canonicalization and verification command vectors plus generic runner-backed structural checks for humans, CI, and tools
- any runner script becomes dumb orchestration rather than a second policy language

Add the source-file cap alongside the command vectors:

```toml
[workspace.metadata.rust-starter.source_files]
max_lines = 2500
include = ["*.rs", "**/*.rs"]
exclude = []
```

Why this belongs here:

- Cargo lint tables only cover compiler and tool lints; a whole-file size cap is outside that namespace
- keeping the threshold in root metadata still makes the manifest the policy source of truth
- the runner can stay generic by enforcing a small schema instead of hard-coding repo-specific paths or numbers

### 2. Toolchain posture

Check in a `rust-toolchain.toml` like:

```toml
[toolchain]
channel = "1.xx.0"
profile = "minimal"
components = ["clippy", "rustfmt"]
```

Use an exact stable patch at scaffold time and bump it deliberately. Do not float on `"stable"` if the goal is reproducible strictness.

### 3. Exception policy

This is the center of gravity. If the exception policy is wrong, the rest becomes theater.

#### Repo-wide exceptions

If a lint is genuinely misaligned with the repo’s design, allow it once in root `Cargo.toml`, grouped by policy cluster and commented briefly.

Do not restate repo-wide exceptions:

- in shell flags
- in CI YAML
- in crate roots
- in a graveyard of repeated `#[allow]`s

#### Local exceptions

If a lint suppression is local:

- prefer `#[expect(..., reason = "...")]` when the exception is temporary, evidence-backed, or under review
- use `#[allow(..., reason = "...")]` only when the exception is stably intentional
- keep the scope as narrow as possible

Because the starter profile denies `allow_attributes_without_reason`, local suppressions must explain themselves.

#### Test code policy

The local corpus often solves test ergonomics by setting `expect_used = "allow"` globally. I think that is too blunt.

Better default:

- keep production policy strict
- if the repo wants more relaxed tests, express that via tiny `clippy.toml` configuration

Example:

```toml
allow-expect-in-tests = true
allow-unwrap-in-tests = true
allow-panic-in-tests = true
```

That keeps the exception local to test code instead of weakening the whole codebase.

#### Restriction-lint policy

Never enable `clippy::restriction` wholesale.

The starter baseline should cherry-pick only the restriction lints with durable operational value:

- `dbg_macro`
- `expect_used`
- `todo`
- `unimplemented`
- `unwrap_used`
- `panic`
- `allow_attributes_without_reason`

That is aggressive without becoming absurd.

### 4. Fast gate versus deep gate

#### Fast gate

This should be the canonical local verification gate:

1. enforce the source-file cap from `workspace.metadata.rust-starter.source_files`
2. `cargo fmt --all --check`
3. `cargo clippy --workspace --all-targets --all-features -- -D warnings`
4. `cargo test --workspace --all-targets --all-features`

This is the default industrial baseline.

#### Canonicalization path

The starter pack should also export a manifest-owned canonicalization pipeline:

1. `cargo fix --workspace --all-targets --all-features --allow-dirty --allow-staged --allow-no-vcs`
2. `cargo clippy --fix --workspace --all-targets --all-features --allow-dirty --allow-staged --allow-no-vcs`
3. `cargo fmt --all`

The default local `check` path should run that pipeline before the verification gate so trivial rewrites do not depend on human or agent memory.

If a repo wants CI to fail on canonicalization drift instead of rewriting files in place, expose a non-mutating `verify` path and point CI there.

#### Deep gate

Add a heavier gate for mature repos:

1. `cargo hack clippy --workspace --all-targets --feature-powerset`
2. `cargo hack test --workspace --feature-powerset`
3. `cargo doc --workspace --all-features --no-deps`
4. `cargo deny check`

If the feature graph explodes, degrade to `--each-feature` or grouped feature subsets instead of abandoning feature-matrix coverage entirely.

### 5. Strong optional strengthenings

#### Explicit `check-cfg` declarations

If the repo uses custom cfgs, upgrade `unexpected_cfgs` from a scalar level to an explicit declaration:

```toml
[workspace.lints.rust]
unexpected_cfgs = { level = "deny", check-cfg = ['cfg(tokio_unstable)', 'cfg(has_foo)'] }
```

This catches misspelled or drifting cfg names earlier than most teams realize.

#### Unsafe-enabled profile

The default profile should deny `unsafe_code`. If a repo truly needs unsafe Rust, switch from "unsafe forbidden" to "unsafe tightly governed":

- add `unsafe_op_in_unsafe_fn = "deny"` under `[workspace.lints.rust]`
- add `undocumented_unsafe_blocks = "deny"` under `[workspace.lints.clippy]`
- review unsafe boundaries manually instead of normalizing them through blanket allowlists

#### Editor alignment

The rustup dev guide and rust-analyzer configuration both point toward aligning editor diagnostics with Clippy. The starter pack should recommend editor settings that make the IDE use `clippy`, all targets, and all features.

That closes the common gap where CI is strict but the editor is only running `cargo check`.

#### `cargo nextest`

For real test suites, prefer `cargo nextest` in the deeper posture for speed and CI ergonomics. But keep a separate doctest command because nextest does not run doctests.

#### `cargo semver-checks`

If the repo publishes libraries, add `cargo semver-checks` to the deeper release gate. It is not a lint in the narrow sense, but it is exactly the kind of automated correctness pressure a serious starter pack should know about.

#### Periodic canary job

Run a scheduled CI job against the latest stable toolchain without updating the pinned toolchain file. Let it warn, not block merges. That gives early visibility into upcoming Clippy churn while preserving deterministic mainline CI.

## Choices I Reject

### Repeating `-A/-D` lists in shell or Python

That is the old pattern. It is operationally inferior to manifest-owned policy.

### Treating `clippy::too_many_lines` as a file-size limit

That lint is about function length. It is not a substitute for a hard source-file cap, and pretending otherwise guarantees drift.

### Enabling all of `clippy::restriction`

Clippy is correct to discourage this. The group is too contradictory and too context-sensitive for a starter baseline.

### Enabling all of `clippy::nursery`

It is useful as a canary or opt-in experiment, not as the default industrial profile.

### Relying on `[lints.cargo]`

Not until Cargo’s own lint namespace stabilizes.

### Floating toolchains in strict CI

If the goal is "force industrial-grade linting", ambient toolchain drift is the enemy.

## Proposed AGENTS.md One-Liner

Use this style:

> Set up Rust linting with the `rust-starter` industrial profile: pin an exact stable `rust-toolchain.toml`; put repo policy only in root `[workspace.lints.{rust,rustdoc,clippy}]` plus manifest-owned canonicalization and verification command vectors and source-file-cap metadata in `workspace.metadata`; make every crate opt into `[lints] workspace = true`; make the default local `check` path canonicalize first, but keep a non-mutating `verify` path for CI; cherry-pick restriction bans instead of enabling `clippy::restriction`; keep repo-wide Clippy carve-outs centralized and commented in `Cargo.toml`; and require local suppressions to use `#[expect(..., reason = "...")]` unless a permanent `#[allow(..., reason = "...")]` is truly justified.

That is long, but it is precise and inferable by a model.

## Bottom Line

The starter pack should standardize the newer Cargo-centric pattern already emerging locally, harden it with toolchain pinning, distinguish manifest-owned policy from runner orchestration, export a first-class canonicalization pipeline, enforce a manifest-backed hard source-file cap, tighten suppression discipline with `reason = "..."`, and add an optional deeper gate for feature matrices, docs, dependency hygiene, and public API drift.

The single strongest improvement over the current local state is not any one lint. It is moving the center of gravity from bespoke scripts to manifest-owned policy.

## Sources

Primary sources:

- Cargo manifest `lints` section: <https://doc.rust-lang.org/cargo/reference/manifest.html#the-lints-section>
- Cargo workspaces and inherited lint tables: <https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table>
- `cargo fix`: <https://doc.rust-lang.org/cargo/commands/cargo-fix.html>
- Cargo’s unstable `[lints.cargo]` namespace: <https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#cargo-lints>
- Clippy overview and lint-group guidance: <https://doc.rust-lang.org/stable/clippy/>
- Clippy usage and `-D warnings`: <https://doc.rust-lang.org/stable/clippy/usage.html>
- Clippy configuration files: <https://doc.rust-lang.org/clippy/configuration.html>
- Clippy lint configuration options: <https://doc.rust-lang.org/clippy/lint_configuration.html>
- Rust diagnostic attributes, including `expect` and `reason`: <https://doc.rust-lang.org/reference/attributes/diagnostics.html>
- Rustdoc lints: <https://doc.rust-lang.org/rustdoc/lints.html>
- Cargo-specific `check-cfg` / `unexpected_cfgs`: <https://doc.rust-lang.org/rustc/check-cfg/cargo-specifics.html>
- rustup toolchain file: <https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file>
- rustup profiles: <https://rust-lang.github.io/rustup/concepts/profiles.html>
- rustup dev-guide linting note: <https://rust-lang.github.io/rustup/dev-guide/linting.html>
- rust-analyzer configuration: <https://rust-analyzer.github.io/book/configuration.html>
- `cargo deny`: <https://embarkstudios.github.io/cargo-deny/>
- `cargo hack`: <https://github.com/taiki-e/cargo-hack>
- `cargo nextest` features docs: <https://nexte.st/docs/features/>
- `cargo semver-checks`: <https://github.com/obi1kenobi/cargo-semver-checks>