diff --git a/.anvil.lock b/.anvil.lock index eb10892f4..7d0f19e69 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" -tool_version = "0.11.1" -catalog_checksum = "sha256:50ec8969dd7f576cef150439b044e29cef03a1c72c0f1838197cd5bb6d8a6b9e" +tool_version = "0.12.0" +catalog_checksum = "sha256:b097d6ce3927a9bf4acb84ec0d17f18d32a4fc0fd46ea999ea14feab96634dd9" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -141,7 +141,7 @@ checksum = "sha256:bab029c5ca3be78fcea2d6af02763a0e0640722dfe1431a0eaa541801a981 [[file]] path = "justfiles/anvil/checks/miri-tree-borrows.just" -checksum = "sha256:ea3349941fd3cf3811bce48a679490fe466dddafdb6d0955c1f5471155669f79" +checksum = "sha256:65f7a333394fc9ce48a54c281b6b87b28cc622710fb5c985cdd5292921da490e" [[file]] path = "justfiles/anvil/checks/miri.just" @@ -288,8 +288,18 @@ checksum = "sha256:b91854c7f0e6d14c74751f607d2c0e3dfcefef55f5178bbafae59d47e577a [[region]] host = "Cargo.toml" -id = "anvil-workspace-lints" -checksum = "sha256:c0d399b21c665de0831143a3d78715ceb38f1d22511d8705f164aeb62e12f174" +id = "anvil-workspace-clippy-lints" +checksum = "sha256:cf78b123235b694a8222e499e7ba91729de9325d1de3f5275e58a85880fabc4e" + +[[region]] +host = "Cargo.toml" +id = "anvil-workspace-rust-lints" +checksum = "sha256:ade16a15a1ecfbe6a8f98aca044266f570a7e658bef51f4b3f6e23d8ebd14798" + +[[region]] +host = "Cargo.toml" +id = "anvil-workspace-rustdoc-lints" +checksum = "sha256:5040c38d64a80ddedc2c51d25881841a1670de77e68bbd14b67ac66fd0062091" [[region]] host = "Justfile" @@ -299,7 +309,7 @@ checksum = "sha256:f8affd59b69c7083c2f3b6f593c63672116dda974c1e661dcb66a4412eb3e [[region]] host = "clippy.toml" id = "anvil-clippy" -checksum = "sha256:aba0733632eac4cb54c4768db578fe1f7b7cfe730aa0d7dc13e2828c9062d67d" +checksum = "sha256:0c37975f24d538358d792626e32d67a043c3a892d076ddf90267ce6757b3245b" [[region]] host = "crates/automation/Cargo.toml" diff --git a/Cargo.lock b/Cargo.lock index a7b65564e..6fe865cf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,7 +427,7 @@ dependencies = [ [[package]] name = "cargo-anvil" -version = "0.11.1" +version = "0.12.0" dependencies = [ "assert_cmd", "clap", diff --git a/Cargo.toml b/Cargo.toml index 9fe00d78c..382da0550 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,104 +122,94 @@ wiremock = { version = "0.6.5", default-features = false } yaml_serde = { version = "0.10.7", default-features = false } zstd = { version = "0.14.0", default-features = false } -# >>> anvil-managed: anvil-workspace-lints -[workspace.lints] -# Catalog of opinionated lints, in dotted-key form so users can extend the -# same scope (`[workspace.lints]` or `[lints]`) outside the sentinels. -# The host-specific table header (`[workspace.lints]` or `[lints]`) is -# prepended by cargo-anvil based on whether the manifest is a workspace -# root or a single-crate Cargo.toml. - -# --- rust ------------------------------------------------------------------ -rust.ambiguous_negative_literals = "warn" -rust.missing_debug_implementations = "warn" -rust.redundant_imports = "warn" -rust.redundant_lifetimes = "warn" -rust.trivial_numeric_casts = "warn" -rust.unsafe_op_in_unsafe_fn = "warn" -rust.unused_lifetimes = "warn" +# >>> anvil-managed: anvil-workspace-rust-lints +[workspace.lints.rust] +# Rust compiler lint policy. Repository-specific Rust lints can follow this +# region as bare keys in the same table. +ambiguous_negative_literals = "warn" +missing_debug_implementations = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +trivial_numeric_casts = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_lifetimes = "warn" # `unexpected_cfgs` is on-by-default at warn since Rust 1.80; combined # with the catalog's `-D warnings` cloud-workflow policy, any custom cfg name # becomes a hard build failure. Pre-declare the cfgs that # `cargo llvm-cov` sets so the recommended coverage-exclusion pattern # `#[cfg_attr(coverage_nightly, coverage(off))]` works out of the box. # Adopters who need additional cfg names take ownership of this one -# line (edit the check-cfg array); anvil's drift detector will +# assignment (edit the check-cfg array); anvil's drift detector will # emit a `.anvil-proposed` sibling on future catalog bumps so the # customization is preserved. -rust.unexpected_cfgs = { level = "warn", check-cfg = [ +unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage,coverage_nightly)', 'cfg(loom)', 'cfg(miri_race_coverage)', 'cfg(miri_strict_provenance)', 'cfg(miri_tree_borrows)', ] } - -# --- rustdoc --------------------------------------------------------------- -rustdoc.broken_intra_doc_links = "warn" -rustdoc.missing_crate_level_docs = "warn" -rustdoc.unescaped_backticks = "warn" - -# --- clippy: category gates (priority -1 so per-lint allows can override) -- -clippy.cargo = { level = "warn", priority = -1 } -clippy.complexity = { level = "warn", priority = -1 } -clippy.correctness = { level = "warn", priority = -1 } -clippy.nursery = { level = "warn", priority = -1 } -clippy.pedantic = { level = "warn", priority = -1 } -clippy.perf = { level = "warn", priority = -1 } -clippy.style = { level = "warn", priority = -1 } -clippy.suspicious = { level = "warn", priority = -1 } - -# --- clippy: opinionated additions ----------------------------------------- +# <<< anvil-managed: anvil-workspace-rust-lints + +# >>> anvil-managed: anvil-workspace-rustdoc-lints +[workspace.lints.rustdoc] +# Rustdoc lint policy. Repository-specific rustdoc lints can follow this +# region as bare keys in the same table. +broken_intra_doc_links = "warn" +missing_crate_level_docs = "warn" +unescaped_backticks = "warn" +# <<< anvil-managed: anvil-workspace-rustdoc-lints + +# >>> anvil-managed: anvil-workspace-clippy-lints +[workspace.lints.clippy] +# Clippy category gates use priority -1 so per-lint allows can override them. +cargo = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } # Two-repo consensus (oxidizer + oxidizer-github). Restriction-group # lints that catch real code-smell cases. Adding a workspace-wide lint # means adopters can only opt out per-crate or by taking ownership of # this region; only enable when the consensus is strong enough to # justify that cost. -clippy.allow_attributes = "warn" -clippy.allow_attributes_without_reason = "warn" -clippy.as_pointer_underscore = "warn" -clippy.assertions_on_result_states = "warn" -clippy.clone_on_ref_ptr = "warn" -clippy.deref_by_slicing = "warn" -clippy.disallowed_script_idents = "warn" -clippy.empty_drop = "warn" -clippy.empty_enum_variants_with_brackets = "warn" -clippy.fn_to_numeric_cast_any = "warn" -clippy.if_then_some_else_none = "warn" -clippy.map_err_ignore = "warn" -clippy.multiple_unsafe_ops_per_block = "warn" -clippy.redundant_type_annotations = "warn" -clippy.renamed_function_params = "warn" -clippy.semicolon_outside_block = "warn" -clippy.undocumented_unsafe_blocks = "warn" -clippy.unnecessary_safety_comment = "warn" -clippy.unnecessary_safety_doc = "warn" -clippy.unneeded_field_pattern = "warn" -clippy.unused_result_ok = "warn" -clippy.unwrap_used = "warn" - -# --- clippy: opinionated suppressions of category-enabled lints ------------ -clippy.missing_const_for_fn = "allow" -clippy.multiple_crate_versions = "allow" -clippy.option_if_let_else = "allow" -clippy.redundant_pub_crate = "allow" -clippy.should_panic_without_expect = "allow" -clippy.significant_drop_tightening = "allow" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +as_pointer_underscore = "warn" +assertions_on_result_states = "warn" +clone_on_ref_ptr = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +fn_to_numeric_cast_any = "warn" +if_then_some_else_none = "warn" +map_err_ignore = "warn" +multiple_unsafe_ops_per_block = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +semicolon_outside_block = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unneeded_field_pattern = "warn" +unused_result_ok = "warn" +unwrap_used = "warn" +# Literal braces are valid data in templates and structured-logging messages. +literal_string_with_formatting_args = "allow" +missing_const_for_fn = "allow" +multiple_crate_versions = "allow" +option_if_let_else = "allow" +redundant_pub_crate = "allow" +should_panic_without_expect = "allow" +significant_drop_tightening = "allow" # Blocked by Clippy bug: https://github.com/rust-lang/rust-clippy/issues/15036 -clippy.wildcard_imports = "allow" -# <<< anvil-managed: anvil-workspace-lints - -# --- clippy: catalog entries this project adds outside the managed region --- -# The dotted-key form above keeps `[workspace.lints]` open, so these extend the -# same scope without editing the anvil-managed block and provoking its drift -# detector. They complete the `M-STATIC-VERIFICATION` catalog, which names all -# three: https://microsoft.github.io/rust-guidelines/guidelines/universal/#M-STATIC-VERIFICATION -clippy.empty_structs_with_brackets = "warn" -clippy.too_long_first_doc_paragraph = "warn" -# The guideline's own opt-out: a structured-logging call site legitimately -# passes a literal holding `{field}` placeholders for the logger to expand. -clippy.literal_string_with_formatting_args = "allow" +wildcard_imports = "allow" +# <<< anvil-managed: anvil-workspace-clippy-lints # A bit of debugging support for release builds. [profile.release] diff --git a/clippy.toml b/clippy.toml index c3292d956..569b4011b 100644 --- a/clippy.toml +++ b/clippy.toml @@ -28,7 +28,7 @@ allow-panic-in-tests = true allow-unwrap-in-tests = true # Aspirational: when clippy.wildcard_imports is re-enabled (currently -# allowed in cargo-lints-body.toml due to upstream bug rust-clippy#15036), +# allowed in cargo-clippy-lints.toml due to upstream bug rust-clippy#15036), # we want the stricter variant that warns on ALL wildcard imports # including prelude. Setting it now means flipping the lint level to # warn later is a one-line change with no tuning afterthought. diff --git a/crates/cargo-anvil/CHANGELOG.md b/crates/cargo-anvil/CHANGELOG.md index 9d74c8f4b..b6457634b 100644 --- a/crates/cargo-anvil/CHANGELOG.md +++ b/crates/cargo-anvil/CHANGELOG.md @@ -2,6 +2,25 @@ ## [Unreleased] +## [0.12.0] - 2026-09-18 + +- โœจ Features + + - split managed lint regions + - lint empty braced structs + +- ๐Ÿ› Bug Fixes + + - scope coverage reports to affected packages ([#190](https://github.com/microsoft/ox-tools/pull/190)) + +- โ™ป๏ธ Code Refactoring + + - centralize lint overrides + +- ๐Ÿงฉ Miscellaneous + + - Merge origin/main into feat/anvil-empty-struct-lint + ## [0.11.1] - 2026-09-21 - ๐Ÿ› Bug Fixes diff --git a/crates/cargo-anvil/Cargo.toml b/crates/cargo-anvil/Cargo.toml index 05b807591..4be9c4dc7 100644 --- a/crates/cargo-anvil/Cargo.toml +++ b/crates/cargo-anvil/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "cargo-anvil" -version = "0.11.1" +version = "0.12.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index e5a2e9a90..96d039bb3 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -400,7 +400,7 @@ More detailed design and operational guidance is available in the This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQblRYhli3L8qob_NSi_WYo69wbWnMVqZw3jJwb3u56HnT6RDphYvRhcoQbBALu36V1VAYbFUDdfp-8dCobobFjKbRep8AbYNMPgi_aMhFhZIGDa2NhcmdvLWFudmlsZjAuMTEuMWtjYXJnb19hbnZpbA + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQblRYhli3L8qob_NSi_WYo69wbWnMVqZw3jJwb3u56HnT6RDphYvRhcoQbBALu36V1VAYbFUDdfp-8dCobobFjKbRep8AbYNMPgi_aMhFhZIGDa2NhcmdvLWFudmlsZjAuMTIuMGtjYXJnb19hbnZpbA [__link0]: https://github.com/casey/just [__link1]: https://rust-lang.github.io/rustfmt/ [__link10]: https://embarkstudios.github.io/cargo-deny/ @@ -419,9 +419,9 @@ This crate was developed as part of The Oxidizer Project. Br [__link22]: https://mutants.rs/ [__link23]: https://crates.io/crates/cargo-hack [__link24]: https://crates.io/crates/cargo-coverage-gate - [__link25]: https://docs.rs/cargo-anvil/0.11.1/cargo_anvil/?search=Catalog::anvil - [__link26]: https://docs.rs/cargo-anvil/0.11.1/cargo_anvil/?search=Artifact - [__link27]: https://docs.rs/cargo-anvil/0.11.1/cargo_anvil/fn.run_app.html + [__link25]: https://docs.rs/cargo-anvil/0.12.0/cargo_anvil/?search=Catalog::anvil + [__link26]: https://docs.rs/cargo-anvil/0.12.0/cargo_anvil/?search=Artifact + [__link27]: https://docs.rs/cargo-anvil/0.12.0/cargo_anvil/fn.run_app.html [__link3]: https://crates.io/crates/cargo-sort [__link4]: https://crates.io/crates/cargo-heather [__link5]: https://crates.io/crates/cargo-ensure-no-cyclic-deps diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index 4140f15ff..88075ddd3 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -117,8 +117,9 @@ Corollaries that drive every section below: - Drift detection lives inside the files themselves (per-file checksums and per-managed-region checksums). There is no parallel metadata file. See [updates.md](./updates.md). - The tool inserts managed sections into the user's `Justfile` and into a small set of shared - config files (`deny.toml`, `[workspace.lints]` in the workspace `Cargo.toml`, and `[lints]` - in each crate's `Cargo.toml`, plus `.delta.toml` and `rustfmt.toml`). Outside those sections, + config files (`deny.toml`, separate `[workspace.lints.]` regions in the workspace + `Cargo.toml`, and `[lints]` in each member crate's `Cargo.toml`, plus `.delta.toml` and + `rustfmt.toml`). Outside those sections, the user's content is preserved verbatim. Everything else is in tool-owned files under `justfiles/anvil/` and the backend-specific cloud workflows directories. @@ -255,7 +256,7 @@ repo/ โ”œโ”€โ”€ Justfile managed-region: anvil-imports โ”œโ”€โ”€ justfiles/anvil/ owned (see local.md) โ”œโ”€โ”€ .anvil/container/ owned โ€” the container image definition (see containers.md) -โ”œโ”€โ”€ Cargo.toml managed-region: anvil-workspace-lints (or anvil-lints in single-crate) +โ”œโ”€โ”€ Cargo.toml managed-regions: anvil-{workspace-,}{rust,rustdoc,clippy}-lints โ”œโ”€โ”€ crates//Cargo.toml managed-region: anvil-lints (one per workspace member) โ”œโ”€โ”€ deny.toml managed-regions: anvil-deny-{advisories,licenses,bans,sources} โ”œโ”€โ”€ rustfmt.toml managed-region: anvil-rustfmt @@ -296,19 +297,25 @@ Detail on each host: credentials. `justfiles/` holds `.just` recipes and nothing else, so these live in a tool-owned directory of their own; see [containers.md](./containers.md). -- **`Cargo.toml` lints regions** โ€” workspace `Cargo.toml` carries the - `anvil-workspace-lints` region containing a single `[workspace.lints]` table whose - rust/clippy/rustdoc entries are written in dotted-key form - (`rust.unsafe_op_in_unsafe_fn = "warn"`, `clippy.unwrap_used = "warn"`, etc.). This - form is chosen because TOML forbids re-declaring a table header โ€” if anvil wrote - `[workspace.lints.clippy]` inside the region, users couldn't add another - `[workspace.lints.clippy]` block elsewhere in the file. With dotted keys, users - append new lints in the same scope right after the closing sentinel; see ยง7. Each - member `Cargo.toml` carries an `anvil-lints` region with exactly +- **`Cargo.toml` lints regions** โ€” workspace `Cargo.toml` carries separate + `anvil-workspace-rust-lints`, `anvil-workspace-rustdoc-lints`, and + `anvil-workspace-clippy-lints` regions. Each opens its explicit + `[workspace.lints.]` table and contains bare lint names. Users append + repository-specific bare lint keys immediately after the corresponding closing + sentinel, before the next table header. This gives `cargo sort --grouped` a stable + sorting unit for each namespace without mixing managed and repository-owned keys. + Each member `Cargo.toml` carries an `anvil-lints` region with exactly `[lints]\nworkspace = true`. The emitter uses `toml-edit` for round-trip-safe - manipulation. In a single-crate repo (no `[workspace]` table), the workspace region - becomes `anvil-lints` and contains a single `[lints]` table with the same - dotted-key layout. + manipulation. In a single-crate repo (no `[workspace]` table), the root manifest + instead carries `anvil-rust-lints`, `anvil-rustdoc-lints`, and + `anvil-clippy-lints` regions under `[lints.]`. The catalog favors broadly + applicable, low-false-positive diagnostics. It warns on empty braced structs whose unit form is clearer + (`empty_structs_with_brackets`) and allows literal strings with formatting-like + braces because templates and structured-logging messages legitimately carry such + text. Documentation completeness, whether production code may panic, and whether + crate-internal APIs may use `pub` remain repository policy: `missing_docs`, + `clippy::panic`, and `unreachable_pub` are not catalog defaults and adopters can + add them outside the managed region. - **`deny.toml`** โ€” one managed region per top-level section (`[advisories]`, `[licenses]`, `[bans]`, `[sources]`) carrying the tool's baseline license/advisory rules. The bans baseline rejects wildcard registry requirements while allowing versionless path or Git @@ -356,12 +363,12 @@ Four escape valves, in increasing severity: The path of least resistance and the recommended approach for project-specific checks. 2. **Edit a managed-region host file outside the sentinels**: extra recipes in your `Justfile`, extra rules in `deny.toml` outside the managed regions (or in the gaps - between its per-section regions), extra clippy - lints written in dotted-key form after the closing sentinel (e.g. `clippy.pedantic = "warn"` - in the `[workspace.lints]` scope). The tool preserves everything outside the - sentinels verbatim. Note that TOML forbids redeclaring a table header (`[workspace.lints.clippy]` - etc.), so user extensions must use dotted-key form or sit in a different parent - table. Repeating a managed key is invalid TOML; editing it inside the block + between its per-section regions), and extra lint keys written in bare form immediately + after the matching namespace sentinel (for example, `pedantic = "warn"` after + `anvil-workspace-clippy-lints`). The tool preserves everything outside the + sentinels verbatim. Note that TOML forbids redeclaring a table header + (`[workspace.lints.clippy]` etc.), so user extensions continue the table opened by + the managed region. Repeating a managed key is invalid TOML; editing it inside the block causes a refusal, even if the template has not changed. 3. **Disable an owned file by emptying it.** An unchanged template leaves it alone; a changed template can produce a `.anvil-proposed` sibling. Emptying a managed diff --git a/crates/cargo-anvil/docs/design/extensibility.md b/crates/cargo-anvil/docs/design/extensibility.md index 3edfcffb4..523a49d6d 100644 --- a/crates/cargo-anvil/docs/design/extensibility.md +++ b/crates/cargo-anvil/docs/design/extensibility.md @@ -22,7 +22,8 @@ front-end binary. Every tool built on the engine emits the *same* fixed namespac - sidecar manifest `.anvil.lock` - review-sibling suffix `.anvil-proposed` - managed-region sentinels `# >>> anvil-managed: ` โ€ฆ `# <<< anvil-managed: ` -- region IDs `anvil-imports`, `anvil-workspace-lints`, `anvil-lints` +- region IDs such as `anvil-imports`, `anvil-workspace-rust-lints`, + `anvil-rust-lints`, and `anvil-lints` - recipe-name prefix `anvil-` (`anvil-pr`, `anvil-clippy`, โ€ฆ) That shared vocabulary is a feature: it signals "this content is managed by the anvil engine โ€” @@ -208,8 +209,12 @@ pub mod artifacts { // Managed regions spliced into user-composed host files. pub mod region { pub fn justfile_imports() -> Artifact; // Justfile / anvil-imports - pub fn workspace_lints() -> Artifact; // Cargo.toml (workspace) / anvil-workspace-lints - pub fn single_crate_lints() -> Artifact; // Cargo.toml (single crate) / anvil-lints + pub fn workspace_rust_lints() -> Artifact; + pub fn workspace_rustdoc_lints() -> Artifact; + pub fn workspace_clippy_lints() -> Artifact; + pub fn single_crate_rust_lints() -> Artifact; + pub fn single_crate_rustdoc_lints() -> Artifact; + pub fn single_crate_clippy_lints() -> Artifact; pub fn member_lints() -> Artifact; // /Cargo.toml / anvil-lints pub fn deny_advisories() -> Artifact; // deny.toml / anvil-deny-advisories pub fn deny_licenses() -> Artifact; // deny.toml / anvil-deny-licenses @@ -305,13 +310,13 @@ crate's `Cargo.toml` just adds one artifact: and the engine replicates it across all members, tracks each in `.anvil.lock`, and reconciles drift per member โ€” no per-fork engine changes. -> Note anvil's own lint regions are modeled as three separate artifacts under this scheme, with no -> region-id-specific engine logic: a `WorkspaceCargoToml` region carrying `[workspace.lints]` -> (`anvil-workspace-lints`), a `SingleCrateCargoToml` region carrying the full `[lints]` catalog -> (`anvil-lints`), and an `EachMemberManifest` member stub (`anvil-lints`). In a workspace the -> first and third emit; in a single-crate repo only the second does (it has no workspace members), -> so the full catalog lands directly in the root `[lints]`. Which set applies is purely a property -> of the selectors on the built-in artifacts, transparent to forks. +> Note anvil's own lint regions are modeled as seven separate artifacts under this scheme, with no +> region-id-specific engine logic: three `WorkspaceCargoToml` regions carrying the Rust, rustdoc, +> and Clippy `[workspace.lints.]` tables; three `SingleCrateCargoToml` equivalents under +> `[lints.]`; and an `EachMemberManifest` member stub (`anvil-lints`). In a workspace the +> workspace trio and member stub emit; in a single-crate repo only the single-crate trio does. +> Which set applies is purely a property of the selectors on the built-in artifacts, transparent +> to forks. > **On-disk casing.** Host paths and owned-file paths are canonical (`Justfile`, `Cargo.toml`), but > the engine resolves each against the repo case-insensitively and reuses whatever casing already diff --git a/crates/cargo-anvil/docs/design/updates.md b/crates/cargo-anvil/docs/design/updates.md index 8e42b364e..e352c5f79 100644 --- a/crates/cargo-anvil/docs/design/updates.md +++ b/crates/cargo-anvil/docs/design/updates.md @@ -161,6 +161,25 @@ Independently configurable tables have separate catalog regions: * `deny.toml`: advisories, licenses, bans, sources. * `spellcheck.toml`: root settings, `[Hunspell]`, `[Hunspell.quirks]`. +* root `Cargo.toml`: Rust, rustdoc, and Clippy lint subtables. + +The lint regions own `[workspace.lints.rust]`, `[workspace.lints.rustdoc]`, and +`[workspace.lints.clippy]` in workspaces, or the corresponding `[lints.*]` +tables in single-crate repositories. A repository extends a subtable with bare +lint names after that region's closing sentinel. Keeping each namespace in its +own region lets `cargo sort --grouped` treat the managed and repository-owned +blocks as independent groups without moving a sentinel through another lint +namespace. + +An unchanged or empty old combined `anvil-workspace-lints` or root `anvil-lints` +block retires in the same run that introduces the three replacements. An edited +old block remains tracked and causes a refusal rather than being overwritten or +retired. During a successful migration, repository-owned dotted assignments +below the old region are rewritten into bare assignments under the matching new +subtable. Their values and comments are preserved; assignments outside the +three lint namespaces are unaffected. An empty legacy block cannot identify its +parent table when that parent is also declared outside the block, so this +ambiguous layout is refused and left unchanged for manual reconciliation. For example, a repository's `[Hunspell] transform_regex = ["^[0-9]+$"]` stays under `[Hunspell]` below `anvil-spellcheck-hunspell`, not under quirks. diff --git a/crates/cargo-anvil/docs/verification.md b/crates/cargo-anvil/docs/verification.md index e2823bc04..9e8776a34 100644 --- a/crates/cargo-anvil/docs/verification.md +++ b/crates/cargo-anvil/docs/verification.md @@ -29,8 +29,9 @@ See also: ### 2.1 Self-hosting (primary) `microsoft/ox-tools` is the canonical adopter of `cargo-anvil`. Its `.github/workflows/`, -`.github/actions/`, `justfiles/anvil/`, `[workspace.lints]` region in `Cargo.toml`, etc. -are all emitted by `cargo anvil` against the in-repo version of the binary. There +`.github/actions/`, `justfiles/anvil/`, and the `anvil-workspace-rust-lints`, +`anvil-workspace-rustdoc-lints`, and `anvil-workspace-clippy-lints` regions in +`Cargo.toml` are all emitted by `cargo anvil` against the in-repo version of the binary. There is no manual maintenance of these files after the initial migration. Every PR runs (via a small bootstrap workflow described in ยง3): @@ -255,7 +256,7 @@ Acknowledged limits of this strategy: | `.github/workflows/anvil-pr.yml` | Hand-written self-validation wrapper (the one bootstrap file). | | `.github/workflows/anvil-pr-impl.yml` (and friends) | Regenerated by `cargo anvil`. Subject to the regenerate-check. | | `justfiles/anvil/*.just` | Regenerated. Subject to the regenerate-check. | -| `Cargo.toml` (anvil-workspace-lints region) | Regenerated. Subject to the regenerate-check. | +| `Cargo.toml` (anvil workspace lint regions) | Regenerated. Subject to the regenerate-check. | | `.anvil.lock` | The manifest itself. Diffed on every PR. | | `docs/release-checklist.md` | Pre-publish checks for things dogfooding misses. | diff --git a/crates/cargo-anvil/src/anvil/artifacts/mod.rs b/crates/cargo-anvil/src/anvil/artifacts/mod.rs index f7279229f..49bcfa038 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/mod.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/mod.rs @@ -67,8 +67,12 @@ pub(crate) fn anvil_artifacts() -> Vec { instructions::cargo_anvil(), instructions::adoption_skill(), region::justfile_imports(), - region::workspace_lints(), - region::single_crate_lints(), + region::workspace_rust_lints(), + region::workspace_rustdoc_lints(), + region::workspace_clippy_lints(), + region::single_crate_rust_lints(), + region::single_crate_rustdoc_lints(), + region::single_crate_clippy_lints(), region::member_lints(), region::deny_advisories(), region::deny_licenses(), @@ -227,8 +231,12 @@ mod tests { instructions::cargo_anvil(), instructions::adoption_skill(), region::justfile_imports(), - region::workspace_lints(), - region::single_crate_lints(), + region::workspace_rust_lints(), + region::workspace_rustdoc_lints(), + region::workspace_clippy_lints(), + region::single_crate_rust_lints(), + region::single_crate_rustdoc_lints(), + region::single_crate_clippy_lints(), region::member_lints(), region::deny_advisories(), region::deny_licenses(), diff --git a/crates/cargo-anvil/src/anvil/artifacts/region.rs b/crates/cargo-anvil/src/anvil/artifacts/region.rs index 98ecca239..40ec20a50 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/region.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/region.rs @@ -14,16 +14,21 @@ use super::justfile; use crate::catalog::{Artifact, HostSelector, RegionId, RegionSpec}; use crate::region::CommentSyntax; -/// Region id for the workspace-scope lints (multi-crate workspaces). -const WORKSPACE_LINTS_REGION_ID: &str = "anvil-workspace-lints"; +const WORKSPACE_RUST_LINTS_REGION_ID: &str = "anvil-workspace-rust-lints"; +const WORKSPACE_RUSTDOC_LINTS_REGION_ID: &str = "anvil-workspace-rustdoc-lints"; +const WORKSPACE_CLIPPY_LINTS_REGION_ID: &str = "anvil-workspace-clippy-lints"; +const RUST_LINTS_REGION_ID: &str = "anvil-rust-lints"; +const RUSTDOC_LINTS_REGION_ID: &str = "anvil-rustdoc-lints"; +const CLIPPY_LINTS_REGION_ID: &str = "anvil-clippy-lints"; /// Region id for crate-scope lints โ€” used both for single-crate repos (full /// catalog) and for each member of a multi-crate workspace (`workspace = /// true`). const CRATE_LINTS_REGION_ID: &str = "anvil-lints"; -/// Embedded body of the lint catalog, in dotted-key form (no table header). -const LINTS_BODY: &str = include_str!("../../../templates/regions/cargo-lints-body.toml"); +const RUST_LINTS_BODY: &str = include_str!("../../../templates/regions/cargo-rust-lints.toml"); +const RUSTDOC_LINTS_BODY: &str = include_str!("../../../templates/regions/cargo-rustdoc-lints.toml"); +const CLIPPY_LINTS_BODY: &str = include_str!("../../../templates/regions/cargo-clippy-lints.toml"); /// Embedded body of a workspace-member lints region. const MEMBER_LINTS_BODY: &str = include_str!("../../../templates/regions/cargo-member-lints.toml"); @@ -97,24 +102,26 @@ const CLIPPY_BODY: &str = include_str!("../../../templates/regions/clippy.toml") /// Embedded body of the `.gitattributes` managed region. const GITATTRIBUTES_BODY: &str = include_str!("../../../templates/regions/gitattributes"); -/// Render the body of the workspace-scope lints region: `[workspace.lints]` -/// header followed by the embedded catalog. #[must_use] -fn render_workspace_lints_body() -> String { - let mut out = String::with_capacity(LINTS_BODY.len() + 32); - out.push_str("[workspace.lints]\n"); - out.push_str(LINTS_BODY); +fn render_lints_body(scope: &str, namespace: &str, body: &str) -> String { + let mut out = String::with_capacity(scope.len() + namespace.len() + body.len() + 8); + out.push('['); + out.push_str(scope); + out.push('.'); + out.push_str(namespace); + out.push_str("]\n"); + out.push_str(body); out } -/// Render the body of the single-crate lints region: `[lints]` header -/// followed by the embedded catalog. #[must_use] -fn render_single_crate_lints_body() -> String { - let mut out = String::with_capacity(LINTS_BODY.len() + 16); - out.push_str("[lints]\n"); - out.push_str(LINTS_BODY); - out +fn lint_region(host: HostSelector, id: &'static str, scope: &str, namespace: &str, body: &str) -> Artifact { + Artifact::region(RegionSpec { + host, + id: RegionId::new(id), + body: render_lints_body(scope, namespace, body), + syntax: CommentSyntax::Hash, + }) } /// Build a single-path `Hash`-syntax region artifact. @@ -137,35 +144,76 @@ pub fn justfile_imports() -> Artifact { ) } -/// Root `Cargo.toml` / `anvil-workspace-lints`. -/// -/// The workspace-scope lint catalog under `[workspace.lints]`. Emitted only -/// in a multi-crate workspace; the [`HostSelector::WorkspaceCargoToml`] host -/// skips it in a single-crate repo. +/// Workspace `Cargo.toml` / `anvil-workspace-rust-lints` โ€” Rust compiler lints. #[must_use] -pub fn workspace_lints() -> Artifact { - Artifact::region(RegionSpec { - host: HostSelector::WorkspaceCargoToml, - id: RegionId::new(WORKSPACE_LINTS_REGION_ID), - body: render_workspace_lints_body(), - syntax: CommentSyntax::Hash, - }) +pub fn workspace_rust_lints() -> Artifact { + lint_region( + HostSelector::WorkspaceCargoToml, + WORKSPACE_RUST_LINTS_REGION_ID, + "workspace.lints", + "rust", + RUST_LINTS_BODY, + ) } -/// Root `Cargo.toml` / `anvil-lints`. -/// -/// The full lint catalog under `[lints]`. Emitted only in a single-crate -/// repo; the [`HostSelector::SingleCrateCargoToml`] host skips it in a -/// workspace, where the catalog lives under `[workspace.lints]` and members -/// inherit it. +/// Workspace `Cargo.toml` / `anvil-workspace-rustdoc-lints` โ€” rustdoc lints. #[must_use] -pub fn single_crate_lints() -> Artifact { - Artifact::region(RegionSpec { - host: HostSelector::SingleCrateCargoToml, - id: RegionId::new(CRATE_LINTS_REGION_ID), - body: render_single_crate_lints_body(), - syntax: CommentSyntax::Hash, - }) +pub fn workspace_rustdoc_lints() -> Artifact { + lint_region( + HostSelector::WorkspaceCargoToml, + WORKSPACE_RUSTDOC_LINTS_REGION_ID, + "workspace.lints", + "rustdoc", + RUSTDOC_LINTS_BODY, + ) +} + +/// Workspace `Cargo.toml` / `anvil-workspace-clippy-lints` โ€” Clippy lints. +#[must_use] +pub fn workspace_clippy_lints() -> Artifact { + lint_region( + HostSelector::WorkspaceCargoToml, + WORKSPACE_CLIPPY_LINTS_REGION_ID, + "workspace.lints", + "clippy", + CLIPPY_LINTS_BODY, + ) +} + +/// Single-crate `Cargo.toml` / `anvil-rust-lints` โ€” Rust compiler lints. +#[must_use] +pub fn single_crate_rust_lints() -> Artifact { + lint_region( + HostSelector::SingleCrateCargoToml, + RUST_LINTS_REGION_ID, + "lints", + "rust", + RUST_LINTS_BODY, + ) +} + +/// Single-crate `Cargo.toml` / `anvil-rustdoc-lints` โ€” rustdoc lints. +#[must_use] +pub fn single_crate_rustdoc_lints() -> Artifact { + lint_region( + HostSelector::SingleCrateCargoToml, + RUSTDOC_LINTS_REGION_ID, + "lints", + "rustdoc", + RUSTDOC_LINTS_BODY, + ) +} + +/// Single-crate `Cargo.toml` / `anvil-clippy-lints` โ€” Clippy lints. +#[must_use] +pub fn single_crate_clippy_lints() -> Artifact { + lint_region( + HostSelector::SingleCrateCargoToml, + CLIPPY_LINTS_REGION_ID, + "lints", + "clippy", + CLIPPY_LINTS_BODY, + ) } /// `/Cargo.toml` / `anvil-lints` โ€” the per-member `workspace = true` @@ -258,23 +306,23 @@ mod tests { use crate::region::{TomlAdoption, adopt_unmanaged_toml_tables, upsert_region}; #[test] - fn embedded_catalog_uses_dotted_keys() { - for line in LINTS_BODY.lines() { + fn embedded_catalog_uses_bare_keys() { + for line in [RUST_LINTS_BODY, RUSTDOC_LINTS_BODY, CLIPPY_LINTS_BODY] + .into_iter() + .flat_map(str::lines) + { let trimmed = line.trim_start(); - assert!( - !trimmed.starts_with('['), - "unexpected table header in cargo-lints-body.toml: {line}" - ); + assert!(!trimmed.starts_with('['), "unexpected table header in lint body: {line}"); } - assert!(LINTS_BODY.contains("rust.unsafe_op_in_unsafe_fn = \"warn\"")); - assert!(LINTS_BODY.contains("clippy.unwrap_used = \"warn\"")); + assert!(RUST_LINTS_BODY.contains("unsafe_op_in_unsafe_fn = \"warn\"")); + assert!(CLIPPY_LINTS_BODY.contains("unwrap_used = \"warn\"")); } #[test] fn catalog_intentionally_omits_contested_lints() { - for needle in ["rust.missing_docs", "clippy.expect_used", "clippy.panic "] { + for needle in ["missing_docs =", "unreachable_pub =", "expect_used =", "panic ="] { assert!( - !LINTS_BODY.contains(needle), + !format!("{RUST_LINTS_BODY}{RUSTDOC_LINTS_BODY}{CLIPPY_LINTS_BODY}").contains(needle), "catalog now contains '{needle}'; if intentional, update the catalog-omission test" ); } @@ -288,6 +336,7 @@ mod tests { "clippy.deref_by_slicing = \"warn\"", "clippy.empty_drop = \"warn\"", "clippy.empty_enum_variants_with_brackets = \"warn\"", + "clippy.empty_structs_with_brackets = \"warn\"", "clippy.fn_to_numeric_cast_any = \"warn\"", "clippy.if_then_some_else_none = \"warn\"", "clippy.multiple_unsafe_ops_per_block = \"warn\"", @@ -297,43 +346,41 @@ mod tests { "clippy.unnecessary_safety_doc = \"warn\"", "clippy.unneeded_field_pattern = \"warn\"", "clippy.unused_result_ok = \"warn\"", + "clippy.literal_string_with_formatting_args = \"allow\"", "clippy.redundant_pub_crate = \"allow\"", "clippy.should_panic_without_expect = \"allow\"", ] { - assert!(LINTS_BODY.contains(needle), "catalog missing consensus lint '{needle}'"); + assert!( + CLIPPY_LINTS_BODY.contains(needle.trim_start_matches("clippy.")), + "catalog missing consensus lint '{needle}'" + ); } } #[test] fn catalog_declares_llvm_cov_cfgs_for_unexpected_cfgs_lint() { assert!( - LINTS_BODY.contains("rust.unexpected_cfgs"), + RUST_LINTS_BODY.contains("unexpected_cfgs"), "catalog must declare rust.unexpected_cfgs to pre-allow llvm-cov's coverage cfgs" ); assert!( - LINTS_BODY.contains("'cfg(coverage,coverage_nightly)'"), + RUST_LINTS_BODY.contains("'cfg(coverage,coverage_nightly)'"), "catalog's unexpected_cfgs check-cfg list must include coverage,coverage_nightly" ); } #[test] - fn workspace_body_prepends_workspace_lints_header() { - let body = render_workspace_lints_body(); - assert!(body.starts_with("[workspace.lints]\n")); - assert!(body.contains("clippy.pedantic = { level = \"warn\", priority = -1 }")); + fn workspace_bodies_open_namespace_subtables() { + assert!(render_lints_body("workspace.lints", "rust", RUST_LINTS_BODY).starts_with("[workspace.lints.rust]\n")); + assert!( + render_lints_body("workspace.lints", "clippy", CLIPPY_LINTS_BODY).contains("pedantic = { level = \"warn\", priority = -1 }") + ); } #[test] - fn single_crate_body_prepends_lints_header() { - let body = render_single_crate_lints_body(); - assert!(body.starts_with("[lints]\n")); - assert!(body.contains("clippy.unwrap_used = \"warn\"")); - for line in body.lines() { - let trimmed = line.trim_start(); - if trimmed.starts_with('[') { - assert_eq!(trimmed, "[lints]", "unexpected table header in single-crate body: {line}"); - } - } + fn single_crate_bodies_open_namespace_subtables() { + assert!(render_lints_body("lints", "rustdoc", RUSTDOC_LINTS_BODY).starts_with("[lints.rustdoc]\n")); + assert!(render_lints_body("lints", "clippy", CLIPPY_LINTS_BODY).contains("unwrap_used = \"warn\"")); } #[test] @@ -343,10 +390,23 @@ mod tests { } #[test] - fn dotted_key_body_parses_as_valid_toml_when_appended_to_workspace() { + fn namespace_bodies_compose_as_valid_workspace_toml() { let host = "[workspace]\nmembers = [\"crates/a\"]\n"; - let region_body = render_workspace_lints_body(); - let spliced = upsert_region(host, WORKSPACE_LINTS_REGION_ID, ®ion_body, CommentSyntax::Hash).unwrap(); + let spliced = [ + (WORKSPACE_RUST_LINTS_REGION_ID, "rust", RUST_LINTS_BODY), + (WORKSPACE_RUSTDOC_LINTS_REGION_ID, "rustdoc", RUSTDOC_LINTS_BODY), + (WORKSPACE_CLIPPY_LINTS_REGION_ID, "clippy", CLIPPY_LINTS_BODY), + ] + .into_iter() + .try_fold(host.to_owned(), |text, (id, namespace, body)| { + upsert_region( + &text, + id, + &render_lints_body("workspace.lints", namespace, body), + CommentSyntax::Hash, + ) + }) + .unwrap(); let _: toml_edit::DocumentMut = spliced.parse().expect("spliced TOML must be valid"); } diff --git a/crates/cargo-anvil/src/emit/managed_region.rs b/crates/cargo-anvil/src/emit/managed_region.rs index 1f8b41d4d..2e8641d37 100644 --- a/crates/cargo-anvil/src/emit/managed_region.rs +++ b/crates/cargo-anvil/src/emit/managed_region.rs @@ -27,8 +27,9 @@ use crate::decision::Decision; use crate::manifest::Manifest; use crate::plan::{PlanItem, Target}; use crate::region::{ - CommentSyntax, RegionPlacement, TomlAdoption, adopt_unmanaged_toml_tables, find_region, insert_after_region, managed_region_ids, - mask_retiring_managed_regions, start_region_offset, text_newline, upsert_region_with_newline, + CommentSyntax, RegionPlacement, TomlAdoption, adopt_unmanaged_toml_tables, find_region, insert_after_region, legacy_lint_region_id, + lint_region_placement, managed_region_ids, mask_retiring_managed_regions, start_region_offset, text_newline, + upsert_region_with_newline, }; /// What the reader should do about a refused region. @@ -187,6 +188,32 @@ pub fn plan_managed_region( RefusalRemedy::EditedRegion, )); } + if let Some(legacy_id) = legacy_lint_region_id(region_id) + && let Some(text) = host_text + && let Some(legacy_region) = + find_region(text, legacy_id, syntax).map_err(|error| ManagedRegionRefusal::new(error, RefusalRemedy::MalformedMarkers))? + { + let recorded_checksum = manifest.region_checksum(host_relpath, legacy_id); + if recorded_checksum.is_none() { + return Err(ManagedRegionRefusal::new( + app_err!( + "the host contains legacy managed region '{legacy_id}', but the manifest does not record it as owned. \ + Refusing to add replacement lint regions alongside an orphan that cargo-anvil cannot retire." + ), + RefusalRemedy::EditedRetirement, + )); + } + let disk_checksum = checksum_str(legacy_region.body_str()); + if !legacy_region.is_empty() && recorded_checksum != Some(disk_checksum.as_str()) { + return Err(ManagedRegionRefusal::new( + app_err!( + "legacy managed region '{legacy_id}' contains edits that do not match its last render. \ + Restore its generated content, or empty its body before migrating to the replacement lint regions." + ), + RefusalRemedy::EditedRetirement, + )); + } + } let spliced = splice(host_relpath, host_text, region_id, rendered_body, syntax, placement, newline)?; Ok(PlanItem::write_region( host_relpath, @@ -369,6 +396,7 @@ fn splice( base }; + let placement = lint_region_placement(region_id, Some(base)).unwrap_or(placement); let spliced = upsert_region_with_newline(base, region_id, rendered_body, syntax, placement, newline) .map_err(|error| ManagedRegionRefusal::new(error, RefusalRemedy::MalformedMarkers))?; insert_after_region(&spliced, region_id, &residue, syntax) diff --git a/crates/cargo-anvil/src/region.rs b/crates/cargo-anvil/src/region.rs index bd0ba253b..2a3b18fc7 100644 --- a/crates/cargo-anvil/src/region.rs +++ b/crates/cargo-anvil/src/region.rs @@ -19,11 +19,12 @@ //! because TOML rejects a duplicate table header outright. See //! [`adopt_unmanaged_toml_tables`]. //! The `id` is globally unique within the catalog (e.g. `anvil-imports`, -//! `anvil-workspace-lints`). +//! `anvil-workspace-rust-lints`). //! //! Empty bodies are regenerated; nonempty edits require reconciliation. use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write as _; use ohno::{AppError, app_err, bail}; use toml_edit::{Item, Key, RawString, Table}; @@ -592,6 +593,34 @@ pub fn adopt_unmanaged_toml_tables(text: &str, body: &str, syntax: CommentSyntax boundaries.extend(protected.iter().map(|range| range.start)); boundaries.sort_unstable(); + for (legacy_id, parent) in [ + ("anvil-workspace-lints", ["workspace", "lints"].as_slice()), + ("anvil-lints", ["lints"].as_slice()), + ] { + if matches!(find_region(text, legacy_id, syntax), Ok(Some(region)) if region.is_empty()) + && candidates.iter().any(|table| table.path == parent) + { + return TomlAdoption::Unrelocatable { + table: parent.join("."), + tail_table: tail.join("."), + }; + } + } + if let Some(adoption) = adopt_dotted_child_assignments(text, &managed, &candidates, &boundaries, &protected) { + return adoption; + } + let legacy_masked = mask_legacy_lint_region_to_header(text, syntax); + if legacy_masked != masked + && let Some(legacy_candidates) = headed_tables(&legacy_masked) + { + let mut legacy_boundaries: Vec = legacy_candidates.iter().map(|table| table.header.start).collect(); + legacy_boundaries.extend(protected.iter().map(|range| range.start)); + legacy_boundaries.sort_unstable(); + if let Some(adoption) = adopt_dotted_child_assignments(text, &managed, &legacy_candidates, &legacy_boundaries, &protected) { + return adoption; + } + } + let mut deletions: Vec = Vec::new(); let mut residue = String::new(); @@ -599,6 +628,7 @@ pub fn adopt_unmanaged_toml_tables(text: &str, body: &str, syntax: CommentSyntax if candidate.array_of_tables { continue; } + let Some(managed_values) = managed .iter() .find(|table| !table.array_of_tables && table.path == candidate.path) @@ -661,6 +691,204 @@ pub fn adopt_unmanaged_toml_tables(text: &str, body: &str, syntax: CommentSyntax } } +fn mask_legacy_lint_region_to_header(text: &str, syntax: CommentSyntax) -> String { + let mut masked = mask_managed_regions(text, syntax).into_bytes(); + for (id, header) in [("anvil-workspace-lints", "[workspace.lints]"), ("anvil-lints", "[lints]")] { + let Ok(Some(region)) = find_region(text, id, syntax) else { + continue; + }; + let start = if let Some(relative) = region.body_str().find(header) { + region.body.start + relative + } else if region.is_empty() { + region.body.start + } else { + continue; + }; + let end = start + header.len(); + masked[start..end].copy_from_slice(&text.as_bytes()[start..end]); + if region.is_empty() { + masked[start..end].copy_from_slice(header.as_bytes()); + } + } + String::from_utf8(masked).expect("masking preserves UTF-8 and only restores original UTF-8 slices") +} + +pub(crate) fn legacy_lint_region_id(region_id: &str) -> Option<&'static str> { + match region_id { + "anvil-workspace-rust-lints" | "anvil-workspace-rustdoc-lints" | "anvil-workspace-clippy-lints" => Some("anvil-workspace-lints"), + "anvil-rust-lints" | "anvil-rustdoc-lints" | "anvil-clippy-lints" => Some("anvil-lints"), + _ => None, + } +} + +pub(crate) fn lint_region_placement(region_id: &str, current: Option<&str>) -> Option { + const WORKSPACE: &[&str] = &[ + "anvil-workspace-rust-lints", + "anvil-workspace-rustdoc-lints", + "anvil-workspace-clippy-lints", + ]; + const SINGLE_CRATE: &[&str] = &["anvil-rust-lints", "anvil-rustdoc-lints", "anvil-clippy-lints"]; + let order = if WORKSPACE.contains(®ion_id) { + WORKSPACE + } else if SINGLE_CRATE.contains(®ion_id) { + SINGLE_CRATE + } else { + return None; + }; + let Some(text) = current else { + return Some(RegionPlacement::End); + }; + if matches!(find_region(text, region_id, CommentSyntax::Hash), Ok(Some(_))) { + return Some(RegionPlacement::End); + } + let position = order + .iter() + .position(|candidate| *candidate == region_id) + .expect("region membership was established above"); + for successor in order.iter().skip(position).skip(1) { + if let Ok(Some(region)) = find_region(text, successor, CommentSyntax::Hash) { + return Some(RegionPlacement::At(region.start_line.start)); + } + } + + let retiring: BTreeSet = ["anvil-workspace-lints", "anvil-lints"].into_iter().map(str::to_owned).collect(); + let parseable = mask_retiring_managed_regions(text, CommentSyntax::Hash, &retiring); + let before_profiles = headed_tables(&parseable).and_then(|tables| { + tables + .into_iter() + .filter(|table| table.path.first().is_some_and(|root| root == "patch" || root == "profile")) + .map(|table| table.header.start) + .min() + }); + let before_legacy = retiring + .iter() + .filter_map(|id| find_region(text, id, CommentSyntax::Hash).ok().flatten()) + .map(|region| region.start_line.start) + .min(); + Some(RegionPlacement::At( + before_profiles.into_iter().chain(before_legacy).min().unwrap_or(text.len()), + )) +} + +/// Move dotted assignments from a headed parent table into a managed child +/// table. For example, introducing `[workspace.lints.rust]` must turn an +/// existing `rust.missing_docs = "warn"` under `[workspace.lints]` into the +/// bare `missing_docs = "warn"` residue that follows the new region. +fn adopt_dotted_child_assignments( + text: &str, + managed: &[HeadedTable], + candidates: &[HeadedTable], + boundaries: &[usize], + protected: &[ByteRange], +) -> Option { + let [managed] = managed else { + return None; + }; + let (namespace, parent_path) = managed.path.split_last()?; + let candidate = candidates + .iter() + .find(|table| !table.array_of_tables && table.path == parent_path)?; + let end = boundary_after(boundaries, candidate.header.start, text.len()); + let mut deletions = Vec::new(); + let mut residue = String::new(); + let mut parent_residue = String::new(); + let is_final_lint_namespace = namespace == "clippy" && parent_path.last().is_some_and(|segment| segment == "lints"); + + for entry in &candidate.entries { + let start = protected + .iter() + .find(|range| (range.start..range.end).contains(&entry.span.start)) + .map_or(entry.span.start, |range| range.end); + if entry.path.first().is_none_or(|segment| segment != namespace) { + if is_final_lint_namespace { + parent_residue.push_str(&text[start..entry.span.end.min(end)]); + deletions.push(ByteRange { + start, + end: entry.span.end.min(end), + }); + } + continue; + } + if entry.path.len() == 1 { + return Some(TomlAdoption::Unrelocatable { + table: parent_path.iter().chain(entry.path.iter()).cloned().collect::>().join("."), + tail_table: managed.path.join("."), + }); + } + let relative_path = &entry.path[1..]; + match managed.values.get(relative_path) { + Some(managed_value) if *managed_value == entry.value => {} + Some(managed_value) => { + return Some(TomlAdoption::Conflict { + table: candidate.path.join("."), + key: candidate + .path + .iter() + .chain(entry.path.iter()) + .cloned() + .collect::>() + .join("."), + managed: managed_value.clone(), + hand_written: entry.value.clone(), + }); + } + None => { + let source = &text[start..entry.span.end.min(end)]; + let bare = strip_dotted_namespace(source, namespace) + .expect("the parsed entry starts with the namespace selected by the filter above"); + residue.push_str(&bare); + } + } + deletions.push(ByteRange { + start, + end: entry.span.end.min(end), + }); + } + if !parent_residue.trim().is_empty() { + writeln!(residue, "[{}]", parent_path.join(".")).expect("writing to a String cannot fail"); + residue.push_str(&parent_residue); + } + + if deletions.is_empty() { + return None; + } + + let mut out = String::with_capacity(text.len()); + let mut cursor = 0; + for range in deletions { + out.push_str(&text[cursor..range.start]); + cursor = range.end; + } + out.push_str(&text[cursor..]); + Some(TomlAdoption::Adopted { + text: out, + residue: tidy_residue(&residue, text_newline(text)), + }) +} + +fn strip_dotted_namespace(source: &str, namespace: &str) -> Option { + let prefixes = [format!("{namespace}."), format!("\"{namespace}\"."), format!("'{namespace}'.")]; + let (offset, prefix_len) = source + .split_inclusive('\n') + .scan(0, |offset, line| { + let start = *offset; + *offset += line.len(); + Some((start, line)) + }) + .find_map(|(start, line)| { + let indentation = line.len() - line.trim_start().len(); + prefixes + .iter() + .find(|prefix| line[indentation..].starts_with(prefix.as_str())) + .map(|prefix| (start + indentation, prefix.len())) + })?; + + let mut out = String::with_capacity(source.len() - prefix_len); + out.push_str(&source[..offset]); + out.push_str(&source[offset + prefix_len..]); + Some(out) +} + /// Trim the blank lines that bounded the residue inside the table it came /// from, leaving exactly one trailing newline when anything is left. /// @@ -2268,4 +2496,110 @@ mod tests { ); assert!(!masked.contains("yanked = \"deny\""), "the other region is blanked:\n{masked}"); } + + #[test] + fn legacy_lint_mask_restores_only_a_nonleading_table_header() { + let text = "# >>> anvil-managed: anvil-workspace-lints\n\ + # old catalog\n\ + [workspace.lints]\n\ + rust.unsafe_code = \"warn\"\n\ + # <<< anvil-managed: anvil-workspace-lints\n"; + let masked = mask_legacy_lint_region_to_header(text, SYN); + + assert!( + masked.contains("[workspace.lints]"), + "the parent table remains parseable:\n{masked}" + ); + assert!(!masked.contains("# old catalog"), "other legacy content stays masked:\n{masked}"); + assert!(!masked.contains("rust.unsafe_code"), "managed assignments stay masked:\n{masked}"); + } + + #[test] + fn legacy_lint_mask_ignores_a_region_without_its_expected_parent_header() { + let text = "# >>> anvil-managed: anvil-workspace-lints\n\ + [workspace.metadata]\n\ + policy = true\n\ + # <<< anvil-managed: anvil-workspace-lints\n"; + + assert_eq!(mask_legacy_lint_region_to_header(text, SYN), mask_managed_regions(text, SYN)); + } + + #[test] + fn lint_region_placement_uses_the_next_namespace_and_canonical_table_boundary() { + let clippy = "# >>> anvil-managed: anvil-workspace-clippy-lints\n\ + [workspace.lints.clippy]\n\ + # <<< anvil-managed: anvil-workspace-clippy-lints\n"; + let with_profile = format!("{clippy}\n[profile.release]\nlto = true\n"); + + assert_eq!( + lint_region_placement("anvil-workspace-rustdoc-lints", Some(&with_profile)), + Some(RegionPlacement::At(0)), + "rustdoc belongs before the existing Clippy region" + ); + assert_eq!( + lint_region_placement("anvil-workspace-clippy-lints", Some(&with_profile)), + Some(RegionPlacement::End), + "an existing region updates in place" + ); + assert_eq!( + lint_region_placement("anvil-workspace-rust-lints", Some("[profile.release]\nlto = true\n")), + Some(RegionPlacement::At(0)), + "the first lint table precedes Cargo profiles" + ); + assert_eq!(lint_region_placement("unrelated", Some(&with_profile)), None); + assert_eq!( + lint_region_placement("anvil-rust-lints", None), + Some(RegionPlacement::End), + "a new host appends its first region" + ); + } + + #[test] + fn differing_dotted_child_assignment_is_a_conflict() { + let adoption = adopt_unmanaged_toml_tables( + "[workspace.lints]\nrust.unsafe_op_in_unsafe_fn = \"deny\"\n", + "[workspace.lints.rust]\nunsafe_op_in_unsafe_fn = \"warn\"\n", + SYN, + ); + + assert_eq!( + adoption, + TomlAdoption::Conflict { + table: "workspace.lints".to_owned(), + key: "workspace.lints.rust.unsafe_op_in_unsafe_fn".to_owned(), + managed: "\"warn\"".to_owned(), + hand_written: "\"deny\"".to_owned(), + } + ); + } + + #[test] + fn exact_namespace_inline_table_is_refused_instead_of_panicking() { + let adoption = adopt_unmanaged_toml_tables( + "[workspace.lints]\nrust = { missing_docs = \"warn\" }\n", + "[workspace.lints.rust]\nunsafe_op_in_unsafe_fn = \"warn\"\n", + SYN, + ); + + assert_eq!( + adoption, + TomlAdoption::Unrelocatable { + table: "workspace.lints.rust".to_owned(), + tail_table: "workspace.lints.rust".to_owned(), + } + ); + } + + #[test] + fn dotted_namespace_stripping_preserves_leading_trivia_and_quoted_namespaces() { + assert_eq!( + strip_dotted_namespace("\n# policy\n \"rust\".missing_docs = \"warn\"\n", "rust"), + Some("\n# policy\n missing_docs = \"warn\"\n".to_owned()) + ); + assert_eq!( + strip_dotted_namespace("'rust'.missing_docs = \"warn\"\n", "rust"), + Some("missing_docs = \"warn\"\n".to_owned()) + ); + assert_eq!(strip_dotted_namespace("clippy.panic = \"warn\"\n", "rust"), None); + } } diff --git a/crates/cargo-anvil/src/run.rs b/crates/cargo-anvil/src/run.rs index 59cb9525d..cc9cb07cc 100644 --- a/crates/cargo-anvil/src/run.rs +++ b/crates/cargo-anvil/src/run.rs @@ -26,7 +26,10 @@ use crate::manifest::Manifest; use crate::plan::{Plan, PlanItem, Target}; #[cfg(test)] use crate::region::upsert_region; -use crate::region::{CommentSyntax, MarkerRepair, RegionPlacement, find_region, managed_region_ids, remove_region, repair_markers}; +use crate::region::{ + CommentSyntax, MarkerRepair, RegionPlacement, find_region, legacy_lint_region_id, lint_region_placement, managed_region_ids, + remove_region, repair_markers, +}; use crate::workspace::{self, Workspace}; /// Outcome of an `update` invocation. @@ -405,7 +408,16 @@ fn push_region_at( let item = match plan_managed_region(manifest, current.as_deref(), request) { Ok(item) => item, Err(refusal) => { - refuse_region(plan, host, spec.id.as_str(), &refusal.reason.to_string(), refusal.remedy); + let reason = refusal.reason.to_string(); + if legacy_lint_region_id(spec.id.as_str()).is_some_and(|legacy_id| { + current + .as_deref() + .is_some_and(|text| matches!(find_region(text, legacy_id, spec.syntax), Ok(Some(_)))) + }) { + composed.states.insert(host.clone(), ComposedHostState::Unsafe(reason.clone())); + composed.reported.insert(host.clone()); + } + refuse_region(plan, host, spec.id.as_str(), &reason, refusal.remedy); return Ok(()); } }; @@ -707,6 +719,9 @@ fn region_placement(region_id: &str, current: Option<&str>) -> RegionPlacement { // empty block establishes no table context; append replacements instead. return RegionPlacement::At(old.start_line.start); } + if let Some(placement) = lint_region_placement(region_id, current) { + return placement; + } RegionPlacement::End } @@ -1615,7 +1630,13 @@ mod tests { } let root_manifest = fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); - assert!(root_manifest.contains("# >>> anvil-managed: anvil-workspace-lints")); + for region in [ + "anvil-workspace-rust-lints", + "anvil-workspace-rustdoc-lints", + "anvil-workspace-clippy-lints", + ] { + assert!(root_manifest.contains(&format!("# >>> anvil-managed: {region}"))); + } let member_manifest = fs::read_to_string(tmp.path().join("crates/alpha/Cargo.toml")).unwrap(); assert!(member_manifest.contains("# >>> anvil-managed: anvil-lints")); assert!(member_manifest.contains("workspace = true")); diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just index bfafb7a0c..c58e56e6f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just @@ -12,7 +12,7 @@ # `#[cfg_attr(miri_, ignore = "")]`, keeping the # suppression next to the test rather than in a sidecar file. # The cfg names are declared in the workspace check-cfg list -# (regions/cargo-lints-body.toml) so non-miri builds don't warn. +# (regions/cargo-rust-lints.toml) so non-miri builds don't warn. # # Under scheduled-runtime-analysis these profiles run full-workspace: # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves diff --git a/crates/cargo-anvil/templates/regions/cargo-clippy-lints.toml b/crates/cargo-anvil/templates/regions/cargo-clippy-lints.toml new file mode 100644 index 000000000..833a0dfc9 --- /dev/null +++ b/crates/cargo-anvil/templates/regions/cargo-clippy-lints.toml @@ -0,0 +1,47 @@ +# Clippy category gates use priority -1 so per-lint allows can override them. +cargo = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } +# Two-repo consensus (oxidizer + oxidizer-github). Restriction-group +# lints that catch real code-smell cases. Adding a workspace-wide lint +# means adopters can only opt out per-crate or by taking ownership of +# this region; only enable when the consensus is strong enough to +# justify that cost. +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +as_pointer_underscore = "warn" +assertions_on_result_states = "warn" +clone_on_ref_ptr = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +fn_to_numeric_cast_any = "warn" +if_then_some_else_none = "warn" +map_err_ignore = "warn" +multiple_unsafe_ops_per_block = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +semicolon_outside_block = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unneeded_field_pattern = "warn" +unused_result_ok = "warn" +unwrap_used = "warn" +# Literal braces are valid data in templates and structured-logging messages. +literal_string_with_formatting_args = "allow" +missing_const_for_fn = "allow" +multiple_crate_versions = "allow" +option_if_let_else = "allow" +redundant_pub_crate = "allow" +should_panic_without_expect = "allow" +significant_drop_tightening = "allow" +# Blocked by Clippy bug: https://github.com/rust-lang/rust-clippy/issues/15036 +wildcard_imports = "allow" diff --git a/crates/cargo-anvil/templates/regions/cargo-lints-body.toml b/crates/cargo-anvil/templates/regions/cargo-lints-body.toml deleted file mode 100644 index 463475eb4..000000000 --- a/crates/cargo-anvil/templates/regions/cargo-lints-body.toml +++ /dev/null @@ -1,84 +0,0 @@ -# Catalog of opinionated lints, in dotted-key form so users can extend the -# same scope (`[workspace.lints]` or `[lints]`) outside the sentinels. -# The host-specific table header (`[workspace.lints]` or `[lints]`) is -# prepended by cargo-anvil based on whether the manifest is a workspace -# root or a single-crate Cargo.toml. - -# --- rust ------------------------------------------------------------------ -rust.ambiguous_negative_literals = "warn" -rust.missing_debug_implementations = "warn" -rust.redundant_imports = "warn" -rust.redundant_lifetimes = "warn" -rust.trivial_numeric_casts = "warn" -rust.unsafe_op_in_unsafe_fn = "warn" -rust.unused_lifetimes = "warn" -# `unexpected_cfgs` is on-by-default at warn since Rust 1.80; combined -# with the catalog's `-D warnings` cloud-workflow policy, any custom cfg name -# becomes a hard build failure. Pre-declare the cfgs that -# `cargo llvm-cov` sets so the recommended coverage-exclusion pattern -# `#[cfg_attr(coverage_nightly, coverage(off))]` works out of the box. -# Adopters who need additional cfg names take ownership of this one -# line (edit the check-cfg array); anvil's drift detector will -# emit a `.anvil-proposed` sibling on future catalog bumps so the -# customization is preserved. -rust.unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(coverage,coverage_nightly)', - 'cfg(loom)', - 'cfg(miri_race_coverage)', - 'cfg(miri_strict_provenance)', - 'cfg(miri_tree_borrows)', -] } - -# --- rustdoc --------------------------------------------------------------- -rustdoc.broken_intra_doc_links = "warn" -rustdoc.missing_crate_level_docs = "warn" -rustdoc.unescaped_backticks = "warn" - -# --- clippy: category gates (priority -1 so per-lint allows can override) -- -clippy.cargo = { level = "warn", priority = -1 } -clippy.complexity = { level = "warn", priority = -1 } -clippy.correctness = { level = "warn", priority = -1 } -clippy.nursery = { level = "warn", priority = -1 } -clippy.pedantic = { level = "warn", priority = -1 } -clippy.perf = { level = "warn", priority = -1 } -clippy.style = { level = "warn", priority = -1 } -clippy.suspicious = { level = "warn", priority = -1 } - -# --- clippy: opinionated additions ----------------------------------------- -# Two-repo consensus (oxidizer + oxidizer-github). Restriction-group -# lints that catch real code-smell cases. Adding a workspace-wide lint -# means adopters can only opt out per-crate or by taking ownership of -# this region; only enable when the consensus is strong enough to -# justify that cost. -clippy.allow_attributes = "warn" -clippy.allow_attributes_without_reason = "warn" -clippy.as_pointer_underscore = "warn" -clippy.assertions_on_result_states = "warn" -clippy.clone_on_ref_ptr = "warn" -clippy.deref_by_slicing = "warn" -clippy.disallowed_script_idents = "warn" -clippy.empty_drop = "warn" -clippy.empty_enum_variants_with_brackets = "warn" -clippy.fn_to_numeric_cast_any = "warn" -clippy.if_then_some_else_none = "warn" -clippy.map_err_ignore = "warn" -clippy.multiple_unsafe_ops_per_block = "warn" -clippy.redundant_type_annotations = "warn" -clippy.renamed_function_params = "warn" -clippy.semicolon_outside_block = "warn" -clippy.undocumented_unsafe_blocks = "warn" -clippy.unnecessary_safety_comment = "warn" -clippy.unnecessary_safety_doc = "warn" -clippy.unneeded_field_pattern = "warn" -clippy.unused_result_ok = "warn" -clippy.unwrap_used = "warn" - -# --- clippy: opinionated suppressions of category-enabled lints ------------ -clippy.missing_const_for_fn = "allow" -clippy.multiple_crate_versions = "allow" -clippy.option_if_let_else = "allow" -clippy.redundant_pub_crate = "allow" -clippy.should_panic_without_expect = "allow" -clippy.significant_drop_tightening = "allow" -# Blocked by Clippy bug: https://github.com/rust-lang/rust-clippy/issues/15036 -clippy.wildcard_imports = "allow" diff --git a/crates/cargo-anvil/templates/regions/cargo-rust-lints.toml b/crates/cargo-anvil/templates/regions/cargo-rust-lints.toml new file mode 100644 index 000000000..5d1a1c770 --- /dev/null +++ b/crates/cargo-anvil/templates/regions/cargo-rust-lints.toml @@ -0,0 +1,25 @@ +# Rust compiler lint policy. Repository-specific Rust lints can follow this +# region as bare keys in the same table. +ambiguous_negative_literals = "warn" +missing_debug_implementations = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +trivial_numeric_casts = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_lifetimes = "warn" +# `unexpected_cfgs` is on-by-default at warn since Rust 1.80; combined +# with the catalog's `-D warnings` cloud-workflow policy, any custom cfg name +# becomes a hard build failure. Pre-declare the cfgs that +# `cargo llvm-cov` sets so the recommended coverage-exclusion pattern +# `#[cfg_attr(coverage_nightly, coverage(off))]` works out of the box. +# Adopters who need additional cfg names take ownership of this one +# assignment (edit the check-cfg array); anvil's drift detector will +# emit a `.anvil-proposed` sibling on future catalog bumps so the +# customization is preserved. +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(coverage,coverage_nightly)', + 'cfg(loom)', + 'cfg(miri_race_coverage)', + 'cfg(miri_strict_provenance)', + 'cfg(miri_tree_borrows)', +] } diff --git a/crates/cargo-anvil/templates/regions/cargo-rustdoc-lints.toml b/crates/cargo-anvil/templates/regions/cargo-rustdoc-lints.toml new file mode 100644 index 000000000..74ebdba6f --- /dev/null +++ b/crates/cargo-anvil/templates/regions/cargo-rustdoc-lints.toml @@ -0,0 +1,5 @@ +# Rustdoc lint policy. Repository-specific rustdoc lints can follow this +# region as bare keys in the same table. +broken_intra_doc_links = "warn" +missing_crate_level_docs = "warn" +unescaped_backticks = "warn" diff --git a/crates/cargo-anvil/templates/regions/clippy.toml b/crates/cargo-anvil/templates/regions/clippy.toml index 8d5be6246..347933766 100644 --- a/crates/cargo-anvil/templates/regions/clippy.toml +++ b/crates/cargo-anvil/templates/regions/clippy.toml @@ -24,7 +24,7 @@ allow-panic-in-tests = true allow-unwrap-in-tests = true # Aspirational: when clippy.wildcard_imports is re-enabled (currently -# allowed in cargo-lints-body.toml due to upstream bug rust-clippy#15036), +# allowed in cargo-clippy-lints.toml due to upstream bug rust-clippy#15036), # we want the stricter variant that warns on ALL wildcard imports # including prelude. Setting it now means flipping the lint level to # warn later is a one-line change with no tuning afterthought. diff --git a/crates/cargo-anvil/tests/fixtures.rs b/crates/cargo-anvil/tests/fixtures.rs index 0176df1ee..b192dc878 100644 --- a/crates/cargo-anvil/tests/fixtures.rs +++ b/crates/cargo-anvil/tests/fixtures.rs @@ -106,12 +106,14 @@ fn single_crate_emits_crate_lints_and_justfiles() { run(&tmp); let cargo = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + for region in ["anvil-rust-lints", "anvil-rustdoc-lints", "anvil-clippy-lints"] { + assert!( + cargo.contains(&format!("anvil-managed: {region}")), + "single-crate fixture should receive the per-crate {region} region; got:\n{cargo}" + ); + } assert!( - cargo.contains("anvil-managed: anvil-lints"), - "single-crate fixture should receive the per-crate lints region; got:\n{cargo}" - ); - assert!( - !cargo.contains("anvil-workspace-lints"), + !cargo.contains("anvil-workspace-rust-lints"), "single-crate fixture must not receive the workspace lints region" ); @@ -280,10 +282,16 @@ fn migration_preserves_user_content() { cargo.contains("lto = \"thin\""), "user-authored [profile.release] must survive migration; got:\n{cargo}" ); - assert!( - cargo.contains("anvil-workspace-lints"), - "anvil workspace lints region must be spliced into Cargo.toml" - ); + for region in [ + "anvil-workspace-rust-lints", + "anvil-workspace-rustdoc-lints", + "anvil-workspace-clippy-lints", + ] { + assert!( + cargo.contains(&format!("anvil-managed: {region}")), + "anvil workspace {region} region must be spliced into Cargo.toml" + ); + } // The defect this fixture used to hide: the hand-written `[advisories]` // declares an `ignore` list the managed body does not, so adoption cannot diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index e525f8f11..71278ea64 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1663,93 +1663,94 @@ stages: resolver = "2" members = ["crates/*"] -# >>> anvil-managed: anvil-workspace-lints -[workspace.lints] -# Catalog of opinionated lints, in dotted-key form so users can extend the -# same scope (`[workspace.lints]` or `[lints]`) outside the sentinels. -# The host-specific table header (`[workspace.lints]` or `[lints]`) is -# prepended by cargo-anvil based on whether the manifest is a workspace -# root or a single-crate Cargo.toml. - -# --- rust ------------------------------------------------------------------ -rust.ambiguous_negative_literals = "warn" -rust.missing_debug_implementations = "warn" -rust.redundant_imports = "warn" -rust.redundant_lifetimes = "warn" -rust.trivial_numeric_casts = "warn" -rust.unsafe_op_in_unsafe_fn = "warn" -rust.unused_lifetimes = "warn" +# >>> anvil-managed: anvil-workspace-rust-lints +[workspace.lints.rust] +# Rust compiler lint policy. Repository-specific Rust lints can follow this +# region as bare keys in the same table. +ambiguous_negative_literals = "warn" +missing_debug_implementations = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +trivial_numeric_casts = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_lifetimes = "warn" # `unexpected_cfgs` is on-by-default at warn since Rust 1.80; combined # with the catalog's `-D warnings` cloud-workflow policy, any custom cfg name # becomes a hard build failure. Pre-declare the cfgs that # `cargo llvm-cov` sets so the recommended coverage-exclusion pattern # `#[cfg_attr(coverage_nightly, coverage(off))]` works out of the box. # Adopters who need additional cfg names take ownership of this one -# line (edit the check-cfg array); anvil's drift detector will +# assignment (edit the check-cfg array); anvil's drift detector will # emit a `.anvil-proposed` sibling on future catalog bumps so the # customization is preserved. -rust.unexpected_cfgs = { level = "warn", check-cfg = [ +unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage,coverage_nightly)', 'cfg(loom)', 'cfg(miri_race_coverage)', 'cfg(miri_strict_provenance)', 'cfg(miri_tree_borrows)', ] } - -# --- rustdoc --------------------------------------------------------------- -rustdoc.broken_intra_doc_links = "warn" -rustdoc.missing_crate_level_docs = "warn" -rustdoc.unescaped_backticks = "warn" - -# --- clippy: category gates (priority -1 so per-lint allows can override) -- -clippy.cargo = { level = "warn", priority = -1 } -clippy.complexity = { level = "warn", priority = -1 } -clippy.correctness = { level = "warn", priority = -1 } -clippy.nursery = { level = "warn", priority = -1 } -clippy.pedantic = { level = "warn", priority = -1 } -clippy.perf = { level = "warn", priority = -1 } -clippy.style = { level = "warn", priority = -1 } -clippy.suspicious = { level = "warn", priority = -1 } - -# --- clippy: opinionated additions ----------------------------------------- +# <<< anvil-managed: anvil-workspace-rust-lints + +# >>> anvil-managed: anvil-workspace-rustdoc-lints +[workspace.lints.rustdoc] +# Rustdoc lint policy. Repository-specific rustdoc lints can follow this +# region as bare keys in the same table. +broken_intra_doc_links = "warn" +missing_crate_level_docs = "warn" +unescaped_backticks = "warn" +# <<< anvil-managed: anvil-workspace-rustdoc-lints + +# >>> anvil-managed: anvil-workspace-clippy-lints +[workspace.lints.clippy] +# Clippy category gates use priority -1 so per-lint allows can override them. +cargo = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } # Two-repo consensus (oxidizer + oxidizer-github). Restriction-group # lints that catch real code-smell cases. Adding a workspace-wide lint # means adopters can only opt out per-crate or by taking ownership of # this region; only enable when the consensus is strong enough to # justify that cost. -clippy.allow_attributes = "warn" -clippy.allow_attributes_without_reason = "warn" -clippy.as_pointer_underscore = "warn" -clippy.assertions_on_result_states = "warn" -clippy.clone_on_ref_ptr = "warn" -clippy.deref_by_slicing = "warn" -clippy.disallowed_script_idents = "warn" -clippy.empty_drop = "warn" -clippy.empty_enum_variants_with_brackets = "warn" -clippy.fn_to_numeric_cast_any = "warn" -clippy.if_then_some_else_none = "warn" -clippy.map_err_ignore = "warn" -clippy.multiple_unsafe_ops_per_block = "warn" -clippy.redundant_type_annotations = "warn" -clippy.renamed_function_params = "warn" -clippy.semicolon_outside_block = "warn" -clippy.undocumented_unsafe_blocks = "warn" -clippy.unnecessary_safety_comment = "warn" -clippy.unnecessary_safety_doc = "warn" -clippy.unneeded_field_pattern = "warn" -clippy.unused_result_ok = "warn" -clippy.unwrap_used = "warn" - -# --- clippy: opinionated suppressions of category-enabled lints ------------ -clippy.missing_const_for_fn = "allow" -clippy.multiple_crate_versions = "allow" -clippy.option_if_let_else = "allow" -clippy.redundant_pub_crate = "allow" -clippy.should_panic_without_expect = "allow" -clippy.significant_drop_tightening = "allow" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +as_pointer_underscore = "warn" +assertions_on_result_states = "warn" +clone_on_ref_ptr = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +fn_to_numeric_cast_any = "warn" +if_then_some_else_none = "warn" +map_err_ignore = "warn" +multiple_unsafe_ops_per_block = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +semicolon_outside_block = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unneeded_field_pattern = "warn" +unused_result_ok = "warn" +unwrap_used = "warn" +# Literal braces are valid data in templates and structured-logging messages. +literal_string_with_formatting_args = "allow" +missing_const_for_fn = "allow" +multiple_crate_versions = "allow" +option_if_let_else = "allow" +redundant_pub_crate = "allow" +should_panic_without_expect = "allow" +significant_drop_tightening = "allow" # Blocked by Clippy bug: https://github.com/rust-lang/rust-clippy/issues/15036 -clippy.wildcard_imports = "allow" -# <<< anvil-managed: anvil-workspace-lints +wildcard_imports = "allow" +# <<< anvil-managed: anvil-workspace-clippy-lints === Justfile === # >>> anvil-managed: anvil-imports @@ -1784,7 +1785,7 @@ allow-panic-in-tests = true allow-unwrap-in-tests = true # Aspirational: when clippy.wildcard_imports is re-enabled (currently -# allowed in cargo-lints-body.toml due to upstream bug rust-clippy#15036), +# allowed in cargo-clippy-lints.toml due to upstream bug rust-clippy#15036), # we want the stricter variant that warns on ALL wildcard imports # including prelude. Setting it now means flipping the lint level to # warn later is a one-line change with no tuning afterthought. @@ -3203,7 +3204,7 @@ anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-vali # `#[cfg_attr(miri_, ignore = "")]`, keeping the # suppression next to the test rather than in a sidecar file. # The cfg names are declared in the workspace check-cfg list -# (regions/cargo-lints-body.toml) so non-miri builds don't warn. +# (regions/cargo-rust-lints.toml) so non-miri builds don't warn. # # Under scheduled-runtime-analysis these profiles run full-workspace: # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 1b48dc0eb..b24e22199 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1776,93 +1776,94 @@ jobs: resolver = "2" members = ["crates/*"] -# >>> anvil-managed: anvil-workspace-lints -[workspace.lints] -# Catalog of opinionated lints, in dotted-key form so users can extend the -# same scope (`[workspace.lints]` or `[lints]`) outside the sentinels. -# The host-specific table header (`[workspace.lints]` or `[lints]`) is -# prepended by cargo-anvil based on whether the manifest is a workspace -# root or a single-crate Cargo.toml. - -# --- rust ------------------------------------------------------------------ -rust.ambiguous_negative_literals = "warn" -rust.missing_debug_implementations = "warn" -rust.redundant_imports = "warn" -rust.redundant_lifetimes = "warn" -rust.trivial_numeric_casts = "warn" -rust.unsafe_op_in_unsafe_fn = "warn" -rust.unused_lifetimes = "warn" +# >>> anvil-managed: anvil-workspace-rust-lints +[workspace.lints.rust] +# Rust compiler lint policy. Repository-specific Rust lints can follow this +# region as bare keys in the same table. +ambiguous_negative_literals = "warn" +missing_debug_implementations = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +trivial_numeric_casts = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_lifetimes = "warn" # `unexpected_cfgs` is on-by-default at warn since Rust 1.80; combined # with the catalog's `-D warnings` cloud-workflow policy, any custom cfg name # becomes a hard build failure. Pre-declare the cfgs that # `cargo llvm-cov` sets so the recommended coverage-exclusion pattern # `#[cfg_attr(coverage_nightly, coverage(off))]` works out of the box. # Adopters who need additional cfg names take ownership of this one -# line (edit the check-cfg array); anvil's drift detector will +# assignment (edit the check-cfg array); anvil's drift detector will # emit a `.anvil-proposed` sibling on future catalog bumps so the # customization is preserved. -rust.unexpected_cfgs = { level = "warn", check-cfg = [ +unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage,coverage_nightly)', 'cfg(loom)', 'cfg(miri_race_coverage)', 'cfg(miri_strict_provenance)', 'cfg(miri_tree_borrows)', ] } - -# --- rustdoc --------------------------------------------------------------- -rustdoc.broken_intra_doc_links = "warn" -rustdoc.missing_crate_level_docs = "warn" -rustdoc.unescaped_backticks = "warn" - -# --- clippy: category gates (priority -1 so per-lint allows can override) -- -clippy.cargo = { level = "warn", priority = -1 } -clippy.complexity = { level = "warn", priority = -1 } -clippy.correctness = { level = "warn", priority = -1 } -clippy.nursery = { level = "warn", priority = -1 } -clippy.pedantic = { level = "warn", priority = -1 } -clippy.perf = { level = "warn", priority = -1 } -clippy.style = { level = "warn", priority = -1 } -clippy.suspicious = { level = "warn", priority = -1 } - -# --- clippy: opinionated additions ----------------------------------------- +# <<< anvil-managed: anvil-workspace-rust-lints + +# >>> anvil-managed: anvil-workspace-rustdoc-lints +[workspace.lints.rustdoc] +# Rustdoc lint policy. Repository-specific rustdoc lints can follow this +# region as bare keys in the same table. +broken_intra_doc_links = "warn" +missing_crate_level_docs = "warn" +unescaped_backticks = "warn" +# <<< anvil-managed: anvil-workspace-rustdoc-lints + +# >>> anvil-managed: anvil-workspace-clippy-lints +[workspace.lints.clippy] +# Clippy category gates use priority -1 so per-lint allows can override them. +cargo = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } # Two-repo consensus (oxidizer + oxidizer-github). Restriction-group # lints that catch real code-smell cases. Adding a workspace-wide lint # means adopters can only opt out per-crate or by taking ownership of # this region; only enable when the consensus is strong enough to # justify that cost. -clippy.allow_attributes = "warn" -clippy.allow_attributes_without_reason = "warn" -clippy.as_pointer_underscore = "warn" -clippy.assertions_on_result_states = "warn" -clippy.clone_on_ref_ptr = "warn" -clippy.deref_by_slicing = "warn" -clippy.disallowed_script_idents = "warn" -clippy.empty_drop = "warn" -clippy.empty_enum_variants_with_brackets = "warn" -clippy.fn_to_numeric_cast_any = "warn" -clippy.if_then_some_else_none = "warn" -clippy.map_err_ignore = "warn" -clippy.multiple_unsafe_ops_per_block = "warn" -clippy.redundant_type_annotations = "warn" -clippy.renamed_function_params = "warn" -clippy.semicolon_outside_block = "warn" -clippy.undocumented_unsafe_blocks = "warn" -clippy.unnecessary_safety_comment = "warn" -clippy.unnecessary_safety_doc = "warn" -clippy.unneeded_field_pattern = "warn" -clippy.unused_result_ok = "warn" -clippy.unwrap_used = "warn" - -# --- clippy: opinionated suppressions of category-enabled lints ------------ -clippy.missing_const_for_fn = "allow" -clippy.multiple_crate_versions = "allow" -clippy.option_if_let_else = "allow" -clippy.redundant_pub_crate = "allow" -clippy.should_panic_without_expect = "allow" -clippy.significant_drop_tightening = "allow" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +as_pointer_underscore = "warn" +assertions_on_result_states = "warn" +clone_on_ref_ptr = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +fn_to_numeric_cast_any = "warn" +if_then_some_else_none = "warn" +map_err_ignore = "warn" +multiple_unsafe_ops_per_block = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +semicolon_outside_block = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unneeded_field_pattern = "warn" +unused_result_ok = "warn" +unwrap_used = "warn" +# Literal braces are valid data in templates and structured-logging messages. +literal_string_with_formatting_args = "allow" +missing_const_for_fn = "allow" +multiple_crate_versions = "allow" +option_if_let_else = "allow" +redundant_pub_crate = "allow" +should_panic_without_expect = "allow" +significant_drop_tightening = "allow" # Blocked by Clippy bug: https://github.com/rust-lang/rust-clippy/issues/15036 -clippy.wildcard_imports = "allow" -# <<< anvil-managed: anvil-workspace-lints +wildcard_imports = "allow" +# <<< anvil-managed: anvil-workspace-clippy-lints === Justfile === # >>> anvil-managed: anvil-imports @@ -1897,7 +1898,7 @@ allow-panic-in-tests = true allow-unwrap-in-tests = true # Aspirational: when clippy.wildcard_imports is re-enabled (currently -# allowed in cargo-lints-body.toml due to upstream bug rust-clippy#15036), +# allowed in cargo-clippy-lints.toml due to upstream bug rust-clippy#15036), # we want the stricter variant that warns on ALL wildcard imports # including prelude. Setting it now means flipping the lint level to # warn later is a one-line change with no tuning afterthought. @@ -3316,7 +3317,7 @@ anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-vali # `#[cfg_attr(miri_, ignore = "")]`, keeping the # suppression next to the test rather than in a sidecar file. # The cfg names are declared in the workspace check-cfg list -# (regions/cargo-lints-body.toml) so non-miri builds don't warn. +# (regions/cargo-rust-lints.toml) so non-miri builds don't warn. # # Under scheduled-runtime-analysis these profiles run full-workspace: # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index cf721233a..1efc24205 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -460,93 +460,94 @@ Finish with: resolver = "2" members = ["crates/*"] -# >>> anvil-managed: anvil-workspace-lints -[workspace.lints] -# Catalog of opinionated lints, in dotted-key form so users can extend the -# same scope (`[workspace.lints]` or `[lints]`) outside the sentinels. -# The host-specific table header (`[workspace.lints]` or `[lints]`) is -# prepended by cargo-anvil based on whether the manifest is a workspace -# root or a single-crate Cargo.toml. - -# --- rust ------------------------------------------------------------------ -rust.ambiguous_negative_literals = "warn" -rust.missing_debug_implementations = "warn" -rust.redundant_imports = "warn" -rust.redundant_lifetimes = "warn" -rust.trivial_numeric_casts = "warn" -rust.unsafe_op_in_unsafe_fn = "warn" -rust.unused_lifetimes = "warn" +# >>> anvil-managed: anvil-workspace-rust-lints +[workspace.lints.rust] +# Rust compiler lint policy. Repository-specific Rust lints can follow this +# region as bare keys in the same table. +ambiguous_negative_literals = "warn" +missing_debug_implementations = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +trivial_numeric_casts = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_lifetimes = "warn" # `unexpected_cfgs` is on-by-default at warn since Rust 1.80; combined # with the catalog's `-D warnings` cloud-workflow policy, any custom cfg name # becomes a hard build failure. Pre-declare the cfgs that # `cargo llvm-cov` sets so the recommended coverage-exclusion pattern # `#[cfg_attr(coverage_nightly, coverage(off))]` works out of the box. # Adopters who need additional cfg names take ownership of this one -# line (edit the check-cfg array); anvil's drift detector will +# assignment (edit the check-cfg array); anvil's drift detector will # emit a `.anvil-proposed` sibling on future catalog bumps so the # customization is preserved. -rust.unexpected_cfgs = { level = "warn", check-cfg = [ +unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage,coverage_nightly)', 'cfg(loom)', 'cfg(miri_race_coverage)', 'cfg(miri_strict_provenance)', 'cfg(miri_tree_borrows)', ] } - -# --- rustdoc --------------------------------------------------------------- -rustdoc.broken_intra_doc_links = "warn" -rustdoc.missing_crate_level_docs = "warn" -rustdoc.unescaped_backticks = "warn" - -# --- clippy: category gates (priority -1 so per-lint allows can override) -- -clippy.cargo = { level = "warn", priority = -1 } -clippy.complexity = { level = "warn", priority = -1 } -clippy.correctness = { level = "warn", priority = -1 } -clippy.nursery = { level = "warn", priority = -1 } -clippy.pedantic = { level = "warn", priority = -1 } -clippy.perf = { level = "warn", priority = -1 } -clippy.style = { level = "warn", priority = -1 } -clippy.suspicious = { level = "warn", priority = -1 } - -# --- clippy: opinionated additions ----------------------------------------- +# <<< anvil-managed: anvil-workspace-rust-lints + +# >>> anvil-managed: anvil-workspace-rustdoc-lints +[workspace.lints.rustdoc] +# Rustdoc lint policy. Repository-specific rustdoc lints can follow this +# region as bare keys in the same table. +broken_intra_doc_links = "warn" +missing_crate_level_docs = "warn" +unescaped_backticks = "warn" +# <<< anvil-managed: anvil-workspace-rustdoc-lints + +# >>> anvil-managed: anvil-workspace-clippy-lints +[workspace.lints.clippy] +# Clippy category gates use priority -1 so per-lint allows can override them. +cargo = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } # Two-repo consensus (oxidizer + oxidizer-github). Restriction-group # lints that catch real code-smell cases. Adding a workspace-wide lint # means adopters can only opt out per-crate or by taking ownership of # this region; only enable when the consensus is strong enough to # justify that cost. -clippy.allow_attributes = "warn" -clippy.allow_attributes_without_reason = "warn" -clippy.as_pointer_underscore = "warn" -clippy.assertions_on_result_states = "warn" -clippy.clone_on_ref_ptr = "warn" -clippy.deref_by_slicing = "warn" -clippy.disallowed_script_idents = "warn" -clippy.empty_drop = "warn" -clippy.empty_enum_variants_with_brackets = "warn" -clippy.fn_to_numeric_cast_any = "warn" -clippy.if_then_some_else_none = "warn" -clippy.map_err_ignore = "warn" -clippy.multiple_unsafe_ops_per_block = "warn" -clippy.redundant_type_annotations = "warn" -clippy.renamed_function_params = "warn" -clippy.semicolon_outside_block = "warn" -clippy.undocumented_unsafe_blocks = "warn" -clippy.unnecessary_safety_comment = "warn" -clippy.unnecessary_safety_doc = "warn" -clippy.unneeded_field_pattern = "warn" -clippy.unused_result_ok = "warn" -clippy.unwrap_used = "warn" - -# --- clippy: opinionated suppressions of category-enabled lints ------------ -clippy.missing_const_for_fn = "allow" -clippy.multiple_crate_versions = "allow" -clippy.option_if_let_else = "allow" -clippy.redundant_pub_crate = "allow" -clippy.should_panic_without_expect = "allow" -clippy.significant_drop_tightening = "allow" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +as_pointer_underscore = "warn" +assertions_on_result_states = "warn" +clone_on_ref_ptr = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +fn_to_numeric_cast_any = "warn" +if_then_some_else_none = "warn" +map_err_ignore = "warn" +multiple_unsafe_ops_per_block = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +semicolon_outside_block = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unneeded_field_pattern = "warn" +unused_result_ok = "warn" +unwrap_used = "warn" +# Literal braces are valid data in templates and structured-logging messages. +literal_string_with_formatting_args = "allow" +missing_const_for_fn = "allow" +multiple_crate_versions = "allow" +option_if_let_else = "allow" +redundant_pub_crate = "allow" +should_panic_without_expect = "allow" +significant_drop_tightening = "allow" # Blocked by Clippy bug: https://github.com/rust-lang/rust-clippy/issues/15036 -clippy.wildcard_imports = "allow" -# <<< anvil-managed: anvil-workspace-lints +wildcard_imports = "allow" +# <<< anvil-managed: anvil-workspace-clippy-lints === Justfile === # >>> anvil-managed: anvil-imports @@ -581,7 +582,7 @@ allow-panic-in-tests = true allow-unwrap-in-tests = true # Aspirational: when clippy.wildcard_imports is re-enabled (currently -# allowed in cargo-lints-body.toml due to upstream bug rust-clippy#15036), +# allowed in cargo-clippy-lints.toml due to upstream bug rust-clippy#15036), # we want the stricter variant that warns on ALL wildcard imports # including prelude. Setting it now means flipping the lint level to # warn later is a one-line change with no tuning afterthought. @@ -2000,7 +2001,7 @@ anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-vali # `#[cfg_attr(miri_, ignore = "")]`, keeping the # suppression next to the test rather than in a sidecar file. # The cfg names are declared in the workspace check-cfg list -# (regions/cargo-lints-body.toml) so non-miri builds don't warn. +# (regions/cargo-rust-lints.toml) so non-miri builds don't warn. # # Under scheduled-runtime-analysis these profiles run full-workspace: # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves diff --git a/crates/cargo-anvil/tests/snapshots/toml_adoption__keeps_an_unmanaged_dotted_lint.snap b/crates/cargo-anvil/tests/snapshots/toml_adoption__keeps_an_unmanaged_dotted_lint.snap index d1b87263b..3e48cc05c 100644 --- a/crates/cargo-anvil/tests/snapshots/toml_adoption__keeps_an_unmanaged_dotted_lint.snap +++ b/crates/cargo-anvil/tests/snapshots/toml_adoption__keeps_an_unmanaged_dotted_lint.snap @@ -12,8 +12,14 @@ members = ["crates/*"] rust.a_custom_lint = "warn" rust.unsafe_op_in_unsafe_fn = "warn" +[profile.release] +lto = "thin" +incremental = false + --- decisions --- -anvil-workspace-lints: Write +anvil-workspace-clippy-lints: Write +anvil-workspace-rust-lints: Write +anvil-workspace-rustdoc-lints: Write --- refusals --- (none) @@ -23,92 +29,98 @@ anvil-workspace-lints: Write resolver = "2" members = ["crates/*"] -# >>> anvil-managed: anvil-workspace-lints [workspace.lints] -# Catalog of opinionated lints, in dotted-key form so users can extend the -# same scope (`[workspace.lints]` or `[lints]`) outside the sentinels. -# The host-specific table header (`[workspace.lints]` or `[lints]`) is -# prepended by cargo-anvil based on whether the manifest is a workspace -# root or a single-crate Cargo.toml. - -# --- rust ------------------------------------------------------------------ -rust.ambiguous_negative_literals = "warn" -rust.missing_debug_implementations = "warn" -rust.redundant_imports = "warn" -rust.redundant_lifetimes = "warn" -rust.trivial_numeric_casts = "warn" -rust.unsafe_op_in_unsafe_fn = "warn" -rust.unused_lifetimes = "warn" +# >>> anvil-managed: anvil-workspace-rust-lints +[workspace.lints.rust] +# Rust compiler lint policy. Repository-specific Rust lints can follow this +# region as bare keys in the same table. +ambiguous_negative_literals = "warn" +missing_debug_implementations = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +trivial_numeric_casts = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_lifetimes = "warn" # `unexpected_cfgs` is on-by-default at warn since Rust 1.80; combined # with the catalog's `-D warnings` cloud-workflow policy, any custom cfg name # becomes a hard build failure. Pre-declare the cfgs that # `cargo llvm-cov` sets so the recommended coverage-exclusion pattern # `#[cfg_attr(coverage_nightly, coverage(off))]` works out of the box. # Adopters who need additional cfg names take ownership of this one -# line (edit the check-cfg array); anvil's drift detector will +# assignment (edit the check-cfg array); anvil's drift detector will # emit a `.anvil-proposed` sibling on future catalog bumps so the # customization is preserved. -rust.unexpected_cfgs = { level = "warn", check-cfg = [ +unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage,coverage_nightly)', 'cfg(loom)', 'cfg(miri_race_coverage)', 'cfg(miri_strict_provenance)', 'cfg(miri_tree_borrows)', ] } +# <<< anvil-managed: anvil-workspace-rust-lints +# House rule, not in anvil's catalog. +a_custom_lint = "warn" -# --- rustdoc --------------------------------------------------------------- -rustdoc.broken_intra_doc_links = "warn" -rustdoc.missing_crate_level_docs = "warn" -rustdoc.unescaped_backticks = "warn" - -# --- clippy: category gates (priority -1 so per-lint allows can override) -- -clippy.cargo = { level = "warn", priority = -1 } -clippy.complexity = { level = "warn", priority = -1 } -clippy.correctness = { level = "warn", priority = -1 } -clippy.nursery = { level = "warn", priority = -1 } -clippy.pedantic = { level = "warn", priority = -1 } -clippy.perf = { level = "warn", priority = -1 } -clippy.style = { level = "warn", priority = -1 } -clippy.suspicious = { level = "warn", priority = -1 } +# >>> anvil-managed: anvil-workspace-rustdoc-lints +[workspace.lints.rustdoc] +# Rustdoc lint policy. Repository-specific rustdoc lints can follow this +# region as bare keys in the same table. +broken_intra_doc_links = "warn" +missing_crate_level_docs = "warn" +unescaped_backticks = "warn" +# <<< anvil-managed: anvil-workspace-rustdoc-lints -# --- clippy: opinionated additions ----------------------------------------- +# >>> anvil-managed: anvil-workspace-clippy-lints +[workspace.lints.clippy] +# Clippy category gates use priority -1 so per-lint allows can override them. +cargo = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } # Two-repo consensus (oxidizer + oxidizer-github). Restriction-group # lints that catch real code-smell cases. Adding a workspace-wide lint # means adopters can only opt out per-crate or by taking ownership of # this region; only enable when the consensus is strong enough to # justify that cost. -clippy.allow_attributes = "warn" -clippy.allow_attributes_without_reason = "warn" -clippy.as_pointer_underscore = "warn" -clippy.assertions_on_result_states = "warn" -clippy.clone_on_ref_ptr = "warn" -clippy.deref_by_slicing = "warn" -clippy.disallowed_script_idents = "warn" -clippy.empty_drop = "warn" -clippy.empty_enum_variants_with_brackets = "warn" -clippy.fn_to_numeric_cast_any = "warn" -clippy.if_then_some_else_none = "warn" -clippy.map_err_ignore = "warn" -clippy.multiple_unsafe_ops_per_block = "warn" -clippy.redundant_type_annotations = "warn" -clippy.renamed_function_params = "warn" -clippy.semicolon_outside_block = "warn" -clippy.undocumented_unsafe_blocks = "warn" -clippy.unnecessary_safety_comment = "warn" -clippy.unnecessary_safety_doc = "warn" -clippy.unneeded_field_pattern = "warn" -clippy.unused_result_ok = "warn" -clippy.unwrap_used = "warn" - -# --- clippy: opinionated suppressions of category-enabled lints ------------ -clippy.missing_const_for_fn = "allow" -clippy.multiple_crate_versions = "allow" -clippy.option_if_let_else = "allow" -clippy.redundant_pub_crate = "allow" -clippy.should_panic_without_expect = "allow" -clippy.significant_drop_tightening = "allow" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +as_pointer_underscore = "warn" +assertions_on_result_states = "warn" +clone_on_ref_ptr = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +fn_to_numeric_cast_any = "warn" +if_then_some_else_none = "warn" +map_err_ignore = "warn" +multiple_unsafe_ops_per_block = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +semicolon_outside_block = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unneeded_field_pattern = "warn" +unused_result_ok = "warn" +unwrap_used = "warn" +# Literal braces are valid data in templates and structured-logging messages. +literal_string_with_formatting_args = "allow" +missing_const_for_fn = "allow" +multiple_crate_versions = "allow" +option_if_let_else = "allow" +redundant_pub_crate = "allow" +should_panic_without_expect = "allow" +significant_drop_tightening = "allow" # Blocked by Clippy bug: https://github.com/rust-lang/rust-clippy/issues/15036 -clippy.wildcard_imports = "allow" -# <<< anvil-managed: anvil-workspace-lints -# House rule, not in anvil's catalog. -rust.a_custom_lint = "warn" +wildcard_imports = "allow" +# <<< anvil-managed: anvil-workspace-clippy-lints + +[profile.release] +lto = "thin" +incremental = false diff --git a/crates/cargo-anvil/tests/toml_adoption.rs b/crates/cargo-anvil/tests/toml_adoption.rs index a551b2580..7d4f6d437 100644 --- a/crates/cargo-anvil/tests/toml_adoption.rs +++ b/crates/cargo-anvil/tests/toml_adoption.rs @@ -30,7 +30,7 @@ use std::fmt::Write as _; use std::path::Path; -use cargo_anvil::test_support::{Cli, RunOutcome, Target, run_update}; +use cargo_anvil::test_support::{Cli, Decision, Manifest, RunOutcome, Target, checksum_str, run_update}; use tempfile::TempDir; /// A workspace with nothing in it but the manifest anvil needs to find, plus @@ -214,13 +214,246 @@ members = [\"crates/*\"] # House rule, not in anvil's catalog. rust.a_custom_lint = \"warn\" rust.unsafe_op_in_unsafe_fn = \"warn\" + +[profile.release] +lto = \"thin\" +incremental = false "; let tmp = workspace_with("Cargo.toml", before); let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + let document: toml_edit::DocumentMut = after.parse().unwrap(); + assert_eq!(document["workspace"]["lints"]["rust"]["a_custom_lint"].as_str(), Some("warn")); + assert_eq!(document["profile"]["release"]["lto"].as_str(), Some("thin")); + assert_eq!(document["profile"]["release"]["incremental"].as_bool(), Some(false)); insta::assert_snapshot!("keeps_an_unmanaged_dotted_lint", report(before, &tmp, "Cargo.toml", &outcome)); } +#[test] +fn replaces_the_combined_lint_region_without_losing_local_lints() { + let old_body = "[workspace.lints]\nrust.unsafe_op_in_unsafe_fn = \"warn\"\n"; + let before = format!( + "[workspace]\nresolver = \"2\"\nmembers = [\"crates/*\"]\n\n\ + # >>> anvil-managed: anvil-workspace-lints\n{old_body}\ + # <<< anvil-managed: anvil-workspace-lints\n\n\ + # Repository-specific lint policy.\n\ + rust.missing_docs = \"warn\"\n\ + clippy.panic = \"warn\"\n" + ); + let tmp = workspace_with("Cargo.toml", &before); + let mut manifest = Manifest::default(); + manifest.set_region("Cargo.toml", "anvil-workspace-lints", checksum_str(old_body)); + manifest.save(tmp.path()).unwrap(); + + let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + assert!(outcome.plan.refusals().is_empty(), "{:?}\n{after}", outcome.plan.refusals()); + let document: toml_edit::DocumentMut = after.parse().unwrap(); + assert!(!after.contains("anvil-workspace-lints")); + assert!(after.contains("[workspace.lints.rust]\n")); + assert_eq!(document["workspace"]["lints"]["rust"]["missing_docs"].as_str(), Some("warn")); + assert!(after.contains("[workspace.lints.clippy]\n")); + assert_eq!(document["workspace"]["lints"]["clippy"]["panic"].as_str(), Some("warn")); + assert!(!after.contains("rust.missing_docs"), "{after}"); + assert!(!after.contains("clippy.panic"), "{after}"); + + let second = run(&tmp); + assert!( + second.plan.items().iter().all(|item| item.decision == Decision::InSync), + "the migration must settle in one run: {:?}", + second.plan.items() + ); +} + +#[test] +fn combined_lint_migration_preserves_other_lint_namespaces() { + let old_body = "[workspace.lints]\nrust.unsafe_op_in_unsafe_fn = \"warn\"\n"; + let before = format!( + "[workspace]\nresolver = \"2\"\nmembers = [\"crates/*\"]\n\n\ + # >>> anvil-managed: anvil-workspace-lints\n{old_body}\ + # <<< anvil-managed: anvil-workspace-lints\n\n\ + rust.missing_docs = \"warn\"\n\ + other.custom = \"warn\"\n" + ); + let tmp = workspace_with("Cargo.toml", &before); + let mut manifest = Manifest::default(); + manifest.set_region("Cargo.toml", "anvil-workspace-lints", checksum_str(old_body)); + manifest.save(tmp.path()).unwrap(); + + let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + assert!(outcome.plan.refusals().is_empty(), "{:?}\n{after}", outcome.plan.refusals()); + let document: toml_edit::DocumentMut = after.parse().unwrap(); + assert_eq!(document["workspace"]["lints"]["rust"]["missing_docs"].as_str(), Some("warn")); + assert_eq!(document["workspace"]["lints"]["other"]["custom"].as_str(), Some("warn")); + + let second = run(&tmp); + assert!( + second.plan.items().iter().all(|item| item.decision == Decision::InSync), + "the migration must settle in one run: {:?}", + second.plan.items() + ); +} + +#[test] +fn empty_combined_lint_region_still_migrates_following_local_lints() { + let old_body = ""; + let before = "\ +[workspace] +resolver = \"2\" +members = [\"crates/*\"] + +# >>> anvil-managed: anvil-workspace-lints +# <<< anvil-managed: anvil-workspace-lints + +rust.missing_docs = \"warn\" +clippy.panic = \"warn\" +"; + let tmp = workspace_with("Cargo.toml", before); + let mut manifest = Manifest::default(); + manifest.set_region("Cargo.toml", "anvil-workspace-lints", checksum_str(old_body)); + manifest.save(tmp.path()).unwrap(); + + let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + assert!(outcome.plan.refusals().is_empty(), "{:?}\n{after}", outcome.plan.refusals()); + let document: toml_edit::DocumentMut = after.parse().unwrap(); + assert_eq!(document["workspace"]["lints"]["rust"]["missing_docs"].as_str(), Some("warn")); + assert_eq!(document["workspace"]["lints"]["clippy"]["panic"].as_str(), Some("warn")); + assert!(!after.contains("anvil-workspace-lints")); +} + +#[test] +fn untracked_combined_lint_region_is_refused() { + let old_body = "[workspace.lints]\nrust.unsafe_op_in_unsafe_fn = \"warn\"\n"; + let before = format!( + "[workspace]\nresolver = \"2\"\nmembers = [\"crates/*\"]\n\n\ + # >>> anvil-managed: anvil-workspace-lints\n{old_body}\ + # <<< anvil-managed: anvil-workspace-lints\n" + ); + let tmp = workspace_with("Cargo.toml", &before); + + let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + assert!( + outcome + .plan + .refusals() + .iter() + .any(|item| item.contains("manifest does not record it as owned")), + "{:?}", + outcome.plan.refusals() + ); + assert_eq!(after, before); +} + +#[test] +fn untracked_single_crate_combined_lint_region_is_refused() { + let old_body = "[lints]\nrust.unsafe_op_in_unsafe_fn = \"warn\"\n"; + let before = format!( + "[package]\nname = \"alpha\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n\ + # >>> anvil-managed: anvil-lints\n{old_body}\ + # <<< anvil-managed: anvil-lints\n" + ); + let tmp = TempDir::new().unwrap(); + write(&tmp.path().join("Cargo.toml"), &before); + write(&tmp.path().join("src/lib.rs"), ""); + + let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + assert!( + outcome + .plan + .refusals() + .iter() + .any(|item| item.contains("manifest does not record it as owned")), + "{:?}", + outcome.plan.refusals() + ); + assert_eq!(after, before); +} + +#[test] +fn edited_combined_lint_region_is_refused() { + let old_body = "[workspace.lints]\nrust.unsafe_op_in_unsafe_fn = \"warn\"\n"; + let edited_body = "[workspace.lints]\nrust.unsafe_op_in_unsafe_fn = \"deny\"\n"; + let before = format!( + "[workspace]\nresolver = \"2\"\nmembers = [\"crates/*\"]\n\n\ + # >>> anvil-managed: anvil-workspace-lints\n{edited_body}\ + # <<< anvil-managed: anvil-workspace-lints\n" + ); + let tmp = workspace_with("Cargo.toml", &before); + let mut manifest = Manifest::default(); + manifest.set_region("Cargo.toml", "anvil-workspace-lints", checksum_str(old_body)); + manifest.save(tmp.path()).unwrap(); + + let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + assert!( + outcome + .plan + .refusals() + .iter() + .any(|item| item.contains("legacy managed region 'anvil-workspace-lints' contains edits")), + "{:?}", + outcome.plan.refusals() + ); + assert_eq!(after, before); +} + +#[test] +fn empty_combined_lint_region_after_external_parent_table_is_refused() { + let old_body = ""; + let before = "\ +[workspace] +resolver = \"2\" +members = [\"crates/*\"] + +[workspace.lints] +rust.unreachable_pub = \"warn\" + +# >>> anvil-managed: anvil-workspace-lints +# <<< anvil-managed: anvil-workspace-lints + +rust.missing_docs = \"warn\" +"; + let tmp = workspace_with("Cargo.toml", before); + let mut manifest = Manifest::default(); + manifest.set_region("Cargo.toml", "anvil-workspace-lints", checksum_str(old_body)); + manifest.save(tmp.path()).unwrap(); + + let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + assert!(!outcome.plan.refusals().is_empty(), "{after}"); + assert_eq!(after, before); +} + +#[test] +fn replaces_the_single_crate_combined_lint_region() { + let old_body = "[lints]\nrust.unsafe_op_in_unsafe_fn = \"warn\"\n"; + let before = format!( + "[package]\nname = \"alpha\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n\ + # >>> anvil-managed: anvil-lints\n{old_body}\ + # <<< anvil-managed: anvil-lints\n\n\ + rust.missing_docs = \"warn\"\n" + ); + let tmp = TempDir::new().unwrap(); + write(&tmp.path().join("Cargo.toml"), &before); + write(&tmp.path().join("src/lib.rs"), ""); + let mut manifest = Manifest::default(); + manifest.set_region("Cargo.toml", "anvil-lints", checksum_str(old_body)); + manifest.save(tmp.path()).unwrap(); + + let outcome = run(&tmp); + let after = std::fs::read_to_string(tmp.path().join("Cargo.toml")).unwrap(); + assert!(outcome.plan.refusals().is_empty(), "{:?}\n{after}", outcome.plan.refusals()); + let document: toml_edit::DocumentMut = after.parse().unwrap(); + assert_eq!(document["lints"]["rust"]["missing_docs"].as_str(), Some("warn")); + assert!(after.contains("anvil-managed: anvil-rust-lints")); + assert!(!after.contains("anvil-managed: anvil-lints\n")); +} + /// Catalog arrays-of-tables are unsupported, but user bin targets survive. #[test] fn preserves_user_binary_targets() { diff --git a/justfiles/anvil/checks/miri-tree-borrows.just b/justfiles/anvil/checks/miri-tree-borrows.just index bfafb7a0c..c58e56e6f 100644 --- a/justfiles/anvil/checks/miri-tree-borrows.just +++ b/justfiles/anvil/checks/miri-tree-borrows.just @@ -12,7 +12,7 @@ # `#[cfg_attr(miri_, ignore = "")]`, keeping the # suppression next to the test rather than in a sidecar file. # The cfg names are declared in the workspace check-cfg list -# (regions/cargo-lints-body.toml) so non-miri builds don't warn. +# (regions/cargo-rust-lints.toml) so non-miri builds don't warn. # # Under scheduled-runtime-analysis these profiles run full-workspace: # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves