From 5ba9f29798a71e2bb7de809f59b67a547798646c Mon Sep 17 00:00:00 2001 From: David Rauschenbach Date: Wed, 4 Nov 2026 10:21:03 -0800 Subject: [PATCH 1/3] Provide a new build-lints crate that can hold future custom linters --- Cargo.toml | 5 + lints/build-lints/.cargo/config.toml | 4 + lints/build-lints/.gitignore | 1 + lints/build-lints/Cargo.lock | 1184 +++++++++++++++++ lints/build-lints/Cargo.toml | 28 + lints/build-lints/rust-toolchain.toml | 5 + .../build-lints/src/io_try_without_context.rs | 96 ++ lints/build-lints/src/lib.rs | 40 + .../build-lints/ui/io_try_without_context.rs | 74 ++ .../ui/io_try_without_context.stderr | 35 + vibe/2026-11/2026-11-01-new-lints-crate.md | 147 ++ 11 files changed, 1619 insertions(+) create mode 100644 lints/build-lints/.cargo/config.toml create mode 100644 lints/build-lints/.gitignore create mode 100644 lints/build-lints/Cargo.lock create mode 100644 lints/build-lints/Cargo.toml create mode 100644 lints/build-lints/rust-toolchain.toml create mode 100644 lints/build-lints/src/io_try_without_context.rs create mode 100644 lints/build-lints/src/lib.rs create mode 100644 lints/build-lints/ui/io_try_without_context.rs create mode 100644 lints/build-lints/ui/io_try_without_context.stderr create mode 100644 vibe/2026-11/2026-11-01-new-lints-crate.md diff --git a/Cargo.toml b/Cargo.toml index bdd54f53d..760b7cad9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,6 +169,11 @@ tauri-plugin-process = "2.3.1" tauri-plugin-updater = { version = "2.11.0", default-features = false, features = ["system-proxy"] } tauri-plugin-single-instance = "2.4.4" +# The dylint libraries `cargo dylint --all` runs. Each lives outside the +# workspace because it builds on its own nightly with `rustc_private`. +[workspace.metadata.dylint] +libraries = [{ path = "lints/build-lints" }] + [workspace.lints.rust] unsafe_code = "deny" missing_docs = "warn" diff --git a/lints/build-lints/.cargo/config.toml b/lints/build-lints/.cargo/config.toml new file mode 100644 index 000000000..6ff8cd134 --- /dev/null +++ b/lints/build-lints/.cargo/config.toml @@ -0,0 +1,4 @@ +# dylint-link names the built library `lib@`, the form +# cargo-dylint loads. +[target.'cfg(all())'] +linker = "dylint-link" diff --git a/lints/build-lints/.gitignore b/lints/build-lints/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/lints/build-lints/.gitignore @@ -0,0 +1 @@ +/target diff --git a/lints/build-lints/Cargo.lock b/lints/build-lints/Cargo.lock new file mode 100644 index 000000000..5ef1a4591 --- /dev/null +++ b/lints/build-lints/Cargo.lock @@ -0,0 +1,1184 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "build-lints" +version = "0.3.0" +dependencies = [ + "clippy_utils", + "dylint_linting", + "dylint_testing", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "cc" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "clippy_utils" +version = "0.1.100" +source = "git+https://github.com/rust-lang/rust-clippy?rev=21baba9c10c5d179915cae82e6247bc2337872fb#21baba9c10c5d179915cae82e6247bc2337872fb" +dependencies = [ + "arrayvec", + "itertools", + "rustc_apfloat", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compiletest_rs" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f150fe9105fcd2a57cad53f0c079a24de65195903ef670990f5909f695eac04c" +dependencies = [ + "diff", + "filetime", + "getopts", + "lazy_static", + "libc", + "log", + "miow", + "regex", + "rustfix", + "serde", + "serde_derive", + "serde_json", + "tester", + "windows-sys 0.59.0", +] + +[[package]] +name = "defmt" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "defmt-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dylint" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f860d94af1dd52e932fdaac5351eeb1946cee466616331d5d3f5f570646cbe05" +dependencies = [ + "anstyle", + "anyhow", + "cargo_metadata", + "dylint_internal", + "log", + "once_cell", + "semver", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "dylint_internal" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7cba7031cd44bdc56bde25a451015911a7aacad72cfc0e476003bec091f3ac" +dependencies = [ + "anstyle", + "anyhow", + "bitflags 2.13.2", + "cargo_metadata", + "git2", + "home", + "log", + "pastey", + "regex", + "semver", + "serde", + "serde_json", + "tar", + "tempfile", + "thiserror 2.0.21", + "toml", + "toml_edit", +] + +[[package]] +name = "dylint_linting" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75bb21fcfeb6cf699039c95e2c23fd3dd9c1fce7f487f2025920a822573ac389" +dependencies = [ + "cargo_metadata", + "dylint_internal", + "paste", + "rustversion", + "serde", + "thiserror 2.0.21", + "toml", +] + +[[package]] +name = "dylint_testing" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5be664837ffe83f16823c9c4646fc7428fa1067cb21f435eb2214fd2bffc8dd" +dependencies = [ + "anyhow", + "cargo_metadata", + "compiletest_rs", + "dylint", + "dylint_internal", + "env_logger", + "once_cell", + "regex", + "tempfile", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "git2" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" +dependencies = [ + "bitflags 2.13.2", + "libc", + "libgit2-sys", + "log", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ac703058abac89e5c39b0b879dce2b6b14a4ba36600c7e7e3d0c60a6883ef3c" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee43309eb53bafb8c36d35d6e444769c8f8e6206a9fbf7546f5db9d7d4a0e93" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dc32470c67eb7d0e9ccb74303fa925313dc04cecb1aa048beaa6f391e36a93d" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libgit2-sys" +version = "0.18.8+1.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7c568b25d7489bc3fb2988ed69ab111d2944d2f5fec3d5c987fe545ea97b50" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff20308ecabd635a2dc1dd5834a86c462cca20efba1d5d3ddab1f8aea3455cb2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_apfloat" +version = "0.2.3+llvm-462a31f5a5ab" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486c2179b4796f65bfe2ee33679acf0927ac83ecf583ad6c91c3b4570911b9ad" +dependencies = [ + "bitflags 2.13.2", + "smallvec", +] + +[[package]] +name = "rustfix" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fa69b198d894d84e23afde8e9ab2af4400b2cba20d6bf2b428a8b01c222c5a" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "tester" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8bf7e0eb2dd7b4228cc1b6821fc5114cd6841ae59f652a85488c016091e5f" +dependencies = [ + "cfg-if", + "getopts", + "libc", + "num_cpus", + "term", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "toml" +version = "1.2.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ffcdf8f50436dbbfd675c1fe1b8e0854f42b7defea8584bdebe1aee900e561" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.18+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec6e78bd70db9420d8d212fa9dc97f116448a1ceff5d00eabb991d407e21396" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.2.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "xattr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963d6fab92d5504b971dc8bd90ca743c2575e590bc8071ea5bc571598667ae08" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/lints/build-lints/Cargo.toml b/lints/build-lints/Cargo.toml new file mode 100644 index 000000000..61abd17b9 --- /dev/null +++ b/lints/build-lints/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "build-lints" +version = "0.3.0" +description = "Dylint lints for the runvil workspace" +edition = "2024" +license = "BSL-1.0" +repository = "https://github.com/cppalliance/runvil" +publish = false + +[lib] +# Dylint loads the library as a dynamic library named after `lib.name`. +crate-type = ["cdylib"] + +[dependencies] +# clippy_utils is unpublished and tied to one nightly; this rev and the +# channel in rust-toolchain.toml move together (`cargo dylint upgrade`). +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "21baba9c10c5d179915cae82e6247bc2337872fb" } +dylint_linting = "6.3" + +[dev-dependencies] +dylint_testing = "6.3" + +# Its own workspace: the library builds on a nightly with `rustc_private`, +# while the runvil workspace builds on the pinned stable release. +[workspace] + +[package.metadata.rust-analyzer] +rustc_private = true diff --git a/lints/build-lints/rust-toolchain.toml b/lints/build-lints/rust-toolchain.toml new file mode 100644 index 000000000..761a7f182 --- /dev/null +++ b/lints/build-lints/rust-toolchain.toml @@ -0,0 +1,5 @@ +# The nightly the clippy_utils rev in Cargo.toml builds against. It moves +# only together with that rev. +[toolchain] +channel = "nightly-2026-08-20" +components = ["llvm-tools-preview", "rustc-dev"] diff --git a/lints/build-lints/src/io_try_without_context.rs b/lints/build-lints/src/io_try_without_context.rs new file mode 100644 index 000000000..37db85d99 --- /dev/null +++ b/lints/build-lints/src/io_try_without_context.rs @@ -0,0 +1,96 @@ +//! `io_try_without_context`: `?` applied directly to a `std::io::Result`. + +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::paths::{PathLookup, PathNS}; +use clippy_utils::sym; +use rustc_hir::attrs::lang_items::LangItem; +use rustc_hir::{Expr, ExprKind, MatchSource}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::ty::{self, Ty}; +use rustc_session::{declare_lint, declare_lint_pass}; + +declare_lint! { + /// ### What it does + /// + /// Flags `?` applied directly to a `std::io::Result` when the enclosing + /// function (or closure, or `try` block) returns a different error type, + /// so `?` converts the `std::io::Error` with `From`. + /// + /// ### Why is this bad? + /// + /// A `std::io::Error` names the failure but not the operation or the + /// path: "No such file or directory (os error 2)". Converted bare by + /// `?`, it reaches the reader, often a model reading tool output, with + /// nothing saying which file was missing or what was being done. + /// + /// `?` from one `std::io::Result` into another is not flagged: no + /// conversion happens, and the caller that leaves `std::io::Error` + /// behind is the one that adds the context. + /// + /// ### Example + /// + /// ```rust,ignore + /// fn load(path: &Path) -> Result { + /// Ok(std::fs::read_to_string(path)?) + /// } + /// ``` + /// + /// Use instead: + /// + /// ```rust,ignore + /// fn load(path: &Path) -> Result { + /// std::fs::read_to_string(path).map_err(|e| { + /// Error::new(ErrorCode::ReadFailed, format!("read {}", path.display())).with_cause(&e) + /// }) + /// } + /// ``` + pub IO_TRY_WITHOUT_CONTEXT, + Deny, + "`?` converts a `std::io::Error` without adding the operation or path" +} + +declare_lint_pass!(IoTryWithoutContext => [IO_TRY_WITHOUT_CONTEXT]); + +/// `std::io::Error`, resolved through std's re-export to wherever the +/// toolchain defines it. +static IO_ERROR: PathLookup = PathLookup::new(PathNS::Type, &[sym::std, sym::io, sym::Error]); + +impl<'tcx> LateLintPass<'tcx> for IoTryWithoutContext { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + // `x?` lowers to `match Try::branch(x) { Continue(v) => v, + // Break(r) => return FromResidual::from_residual(r) }`; the break + // arm comes first. + if let ExprKind::Match(scrutinee, arms, MatchSource::TryDesugar(_)) = expr.kind + && let ExprKind::Call(callee, [operand]) = scrutinee.kind + && let ExprKind::Path(qpath) = callee.kind + && cx.tcx.qpath_is_lang_item(qpath, LangItem::TryTraitBranch) + && !expr.span.in_external_macro(cx.sess().source_map()) + && is_io_result(cx, cx.typeck_results().expr_ty(operand)) + && let Some(arm) = arms.first() + && let ExprKind::Ret(Some(residual)) | ExprKind::Break(_, Some(residual)) = + arm.body.kind + && !is_io_result(cx, cx.typeck_results().expr_ty(residual)) + { + span_lint_and_help( + cx, + IO_TRY_WITHOUT_CONTEXT, + expr.span, + "`?` converts a `std::io::Error` that names no operation or path", + None, + "convert the error before `?` with a message naming the operation and the path, \ + e.g. `.map_err(|e| ...)`", + ); + } + } +} + +/// Whether `ty` is `Result<_, std::io::Error>`. +fn is_io_result<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { + if let ty::Adt(adt, args) = ty.kind() + && cx.tcx.is_diagnostic_item(sym::Result, adt.did()) + { + IO_ERROR.matches_ty(cx, args.type_at(1)) + } else { + false + } +} diff --git a/lints/build-lints/src/lib.rs b/lints/build-lints/src/lib.rs new file mode 100644 index 000000000..b663f4585 --- /dev/null +++ b/lints/build-lints/src/lib.rs @@ -0,0 +1,40 @@ +//! Dylint lints for the runvil workspace: rules that clippy has no lint +//! for, checked with `cargo dylint --all` from the workspace root. +//! +//! Each lint lives in its own module and is registered in +//! [`register_lints`]. A library holding more than one lint registers them +//! by hand rather than through `dylint_linting::declare_late_lint!`, which +//! declares a whole library around a single lint. +//! +//! ## Invariants +//! +//! - The library builds only on the nightly in `rust-toolchain.toml`, the +//! one the `clippy_utils` rev in `Cargo.toml` targets; both move together. +//! - Every lint has a UI test under `ui/`, run by `cargo test` here. + +#![feature(rustc_private)] +#![warn(unused_extern_crates)] + +extern crate rustc_hir; +extern crate rustc_lint; +extern crate rustc_middle; +extern crate rustc_session; + +mod io_try_without_context; + +dylint_linting::dylint_library!(); + +/// Registers every lint in this library with the compiler session. +#[unsafe(no_mangle)] +pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) { + dylint_linting::init_config(sess); + lint_store.register_lints(&[io_try_without_context::IO_TRY_WITHOUT_CONTEXT]); + lint_store.register_late_lint_pass(Box::new(|_| { + Box::new(io_try_without_context::IoTryWithoutContext) + })); +} + +#[test] +fn ui() { + dylint_testing::ui_test(env!("CARGO_CRATE_NAME"), "ui"); +} diff --git a/lints/build-lints/ui/io_try_without_context.rs b/lints/build-lints/ui/io_try_without_context.rs new file mode 100644 index 000000000..0f0ab0cc6 --- /dev/null +++ b/lints/build-lints/ui/io_try_without_context.rs @@ -0,0 +1,74 @@ +#![allow(dead_code)] + +use std::fmt; +use std::io; + +#[derive(Debug)] +struct Error(String); + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(error: io::Error) -> Self { + Error(error.to_string()) + } +} + +fn io_op() -> io::Result { + Ok(0) +} + +fn spelled_out() -> Result { + Ok(0) +} + +// Flagged: `From` converts with no context. +fn into_custom() -> Result { + let byte = io_op()?; + Ok(byte) +} + +// Flagged: the alias and the spelled-out type are the same type. +fn spelled_into_custom() -> Result { + Ok(spelled_out()?) +} + +// Flagged: boxing erases the type but adds no context. +fn into_box() -> Result> { + Ok(io_op()?) +} + +// Flagged: inside a closure returning another error type. +fn in_closure() -> Result { + let read = || -> Result { Ok(io_op()?) }; + read() +} + +// Not flagged: context added before `?`. +fn with_context(path: &str) -> Result { + Ok(io_op().map_err(|e| Error(format!("read {path}: {e}")))?) +} + +// Not flagged: io to io is no conversion; the caller adds the context. +fn io_to_io() -> io::Result { + Ok(io_op()? + spelled_out()?) +} + +// Not flagged: not an io::Result. +fn other_result() -> Result { + let parsed: Result = "1".parse(); + Ok(parsed.map_err(|e| Error(e.to_string()))?) +} + +// Not flagged: `?` on an Option. +fn option() -> Option { + Some(Some(1)?) +} + +fn main() {} diff --git a/lints/build-lints/ui/io_try_without_context.stderr b/lints/build-lints/ui/io_try_without_context.stderr new file mode 100644 index 000000000..5e72a2e58 --- /dev/null +++ b/lints/build-lints/ui/io_try_without_context.stderr @@ -0,0 +1,35 @@ +error: `?` converts a `std::io::Error` that names no operation or path + --> $DIR/io_try_without_context.rs:33:16 + | +LL | let byte = io_op()?; + | ^^^^^^^^ + | + = help: convert the error before `?` with a message naming the operation and the path, e.g. `.map_err(|e| ...)` + = note: `#[deny(io_try_without_context)]` on by default + +error: `?` converts a `std::io::Error` that names no operation or path + --> $DIR/io_try_without_context.rs:39:8 + | +LL | Ok(spelled_out()?) + | ^^^^^^^^^^^^^^ + | + = help: convert the error before `?` with a message naming the operation and the path, e.g. `.map_err(|e| ...)` + +error: `?` converts a `std::io::Error` that names no operation or path + --> $DIR/io_try_without_context.rs:44:8 + | +LL | Ok(io_op()?) + | ^^^^^^^^ + | + = help: convert the error before `?` with a message naming the operation and the path, e.g. `.map_err(|e| ...)` + +error: `?` converts a `std::io::Error` that names no operation or path + --> $DIR/io_try_without_context.rs:49:45 + | +LL | let read = || -> Result { Ok(io_op()?) }; + | ^^^^^^^^ + | + = help: convert the error before `?` with a message naming the operation and the path, e.g. `.map_err(|e| ...)` + +error: aborting due to 4 previous errors + diff --git a/vibe/2026-11/2026-11-01-new-lints-crate.md b/vibe/2026-11/2026-11-01-new-lints-crate.md new file mode 100644 index 000000000..f45032fa2 --- /dev/null +++ b/vibe/2026-11/2026-11-01-new-lints-crate.md @@ -0,0 +1,147 @@ +--- +name: New lints crate +overview: Add build-lints, a dylint library for workspace rules clippy has no lint for, and its first lint, io_try_without_context, which rejects `?` that converts a std::io::Error into another error type without adding the operation or path. +todos: + - id: crate + content: "Create lints/build-lints as its own cargo workspace: cdylib, nightly-2026-08-20 rust-toolchain.toml paired with the clippy_utils rev, dylint-link linker in .cargo/config.toml, dylint_linting and dylint_testing 6.3" + status: completed + - id: register + content: "lib.rs registers every lint by hand in register_lints (dylint_library! plus #[unsafe(no_mangle)]), so the library can hold more than one lint" + status: completed + - id: io-lint + content: "io_try_without_context: flag a TryDesugar match whose Try::branch operand is Result<_, std::io::Error> and whose residual target is not; Deny by default; skip external macros" + status: completed + - id: ui-test + content: "ui/io_try_without_context.rs covers four flagged shapes (custom From, spelled-out Result, Box, closure) and four allowed ones (map_err first, io to io, non-io Result, Option); cargo test in the lints crate passes" + status: completed + - id: wire + content: "Name the library in the root Cargo.toml's [workspace.metadata.dylint] so `cargo dylint --all` runs it from the repository root" + status: completed + - id: survey + content: "Run the lint over the whole workspace at warn level and record the existing sites (29 in 12 files)" + status: completed + - id: fix-sites + content: "Add context at the 29 existing sites, then run `cargo dylint --all` with the lint at its Deny default" + status: pending + - id: ci + content: "Add a CI job that installs cargo-dylint and dylint-link, runs `cargo test` in lints/build-lints, and runs `cargo dylint --all -- --workspace --all-targets --all-features`; document the command in AGENTS.md Verification" + status: pending + - id: naming + content: "Rename runvil-lints to build-lints: `runvil-*` crates are by definition the Engine, and this crate is not" + status: completed +isProject: false +--- + +# New lints crate + +## Problem + +The Runvil workspace requires a means by which custom lints can be implemented +and applied to the codebase. + +## Chosen: a dylint based library + +Dylint loads lint libraries built against `rustc_private` into a rustc +driver, so a workspace can write its own late lints with full type +information, the same API clippy's own lints use. This is the smallest +facility that can see the operand's type at a `?`: a structural check in +build-xtask reads source text and cannot tell an `io::Result` from any +other `Result`. + +The crate holds every future lint, one module each, registered in +`register_lints`. `dylint_linting::declare_late_lint!` is not used +because it declares a whole library around one lint. + +### Placement: `lints/build-lints/`, outside `crates/` + +- The library needs a nightly with `rustc-dev` and `#![feature(rustc_private)]`, + while the workspace pins stable 1.99 in `rust-toolchain.toml`. It has + its own `[workspace]` table and `rust-toolchain.toml`, so it cannot be a + workspace member. +- build-xtask's checks walk every crate under `crates/` and bind each one: + `[lints] workspace = true` inheritance, the `build_ceiling` build-script + wiring, and, for a `runvil-*` package name, the Engine guards. None of + those can hold for a crate outside the workspace on another toolchain. + Outside `crates/`, the walk never sees it. +- The root `Cargo.toml` names it in `[workspace.metadata.dylint]`, so + `cargo dylint --all` from the repository root builds and runs it. + +### Toolchain pairing + +`clippy_utils` is unpublished and each git rev builds on one nightly. The +rev and `nightly-2026-08-20` come from `cargo dylint new` (cargo-dylint +6.3.2) and move together, with `cargo dylint upgrade`. This is a different +nightly from the one `cargo xtask api` pins (`nightly-2026-09-05`), and the +two are not tied to each other. + +## The first lint: `io_try_without_context` + +`x?` lowers to `match Try::branch(x) { Continue(v) => v, Break(r) => return +FromResidual::from_residual(r) }` (`MatchSource::TryDesugar`). The lint +fires when: + +1. the scrutinee is a call to the `TryTraitBranch` lang item, +2. the operand's type is `Result<_, std::io::Error>`, and +3. the type of the break arm's `from_residual` call (the enclosing + function's, closure's, or `try` block's return type) is not. + +`std::io::Error` is resolved with clippy_utils' `PathLookup` on +`std::io::Error`, following std's re-export. On this nightly the type is +defined in `core::io`, and it has no diagnostic item. + +Default level is `Deny`. Spans from external macros are skipped. + +### Rejected: also flag io to io + +When the function itself returns `std::io::Result`, `?` converts nothing. +Adding context there would mean wrapping in `io::Error::new(kind, msg)`, +which keeps the error type and also breaks implementations of +`std::io::Read` and `Write`. The caller that converts away from +`std::io::Error` is where the context goes, and the lint fires there. + +### What counts as context + +Any conversion before the `?` changes the operand's type and so satisfies +the lint: `.map_err(...)` into the universal `Error` with +`Error::with_cause`, anyhow's `.context(...)` or `.with_context(...)`, or a +crate's own conversion. The lint does not judge the message, only that the +bare `From` conversion was not the one used. + +## Existing sites + +Run at warn level over the whole workspace (`DYLINT_RUSTFLAGS="-W +io_try_without_context" cargo dylint --all -- --workspace --all-targets +--all-features --keep-going`), on 2026-11-04: + +| File | Sites | +|---|---| +| `crates/build-xtask/src/new_crate.rs` | 6 | +| `crates/build-ui/tests/it/main.rs` | 6 | +| `crates/workshop/server/src/serve.rs` | 3 | +| `crates/workshop/server/src/agents/language_guide/tests.rs` | 3 | +| `crates/workshop/run-log/src/layout.rs` | 3 | +| `crates/workshop/desktop/build.rs` | 2 | +| `crates/workshop/run-log/src/recorder.rs` | 1 | +| `crates/workshop/run-log/src/append.rs` | 1 | +| `crates/runvil/tests/suite/support.rs` | 1 | +| `crates/runvil/tests/suite/shipped.rs` | 1 | +| `crates/runvil/tests/suite/prepare.rs` | 1 | +| `crates/runvil/tests/suite/parsing.rs` | 1 | + +These counts may include a site twice when it compiles for both a lib and +its lib test target. The Engine and Harness crates have no sites outside +the `runvil` facade's test suite. + +## The crate name: `build-lints` + +The dylint library name is `build_lints`. + +## Verification + +- `cargo test` in `lints/build-lints`: the UI test passes. +- `cargo clippy --all-targets` and `cargo fmt --check` in + `lints/build-lints`: clean. +- `cargo test -p build-xtask`: 254 passed. The new top-level directory and + the root manifest's metadata table break no structural check. +- `node --test crates/workshop/ui/test/docs-claims.mjs`: 4 passed. +- `cargo fmt --all --check`: clean. From 4645f6d7eabce65d90e10ef99f6acfd3c7e724c4 Mon Sep 17 00:00:00 2001 From: David Rauschenbach Date: Wed, 4 Nov 2026 10:35:48 -0800 Subject: [PATCH 2/3] Fix code that didn't confirm to the new first lint (the dylint example, which happens to be a good lint) --- AGENTS.md | 1 + crates/build-ui/tests/it/main.rs | 22 +++++-- crates/build-xtask/src/new_crate.rs | 22 ++++--- crates/runvil/tests/suite/parsing.rs | 4 +- crates/runvil/tests/suite/prepare.rs | 4 +- crates/runvil/tests/suite/shipped.rs | 2 +- crates/runvil/tests/suite/support.rs | 3 +- crates/workshop/desktop/build.rs | 12 +++- crates/workshop/run-log/src/append.rs | 5 +- crates/workshop/run-log/src/error.rs | 20 +++++-- crates/workshop/run-log/src/layout.rs | 16 ++++- crates/workshop/run-log/src/recorder.rs | 4 +- .../server/src/agents/language_guide/tests.rs | 10 +++- crates/workshop/server/src/serve.rs | 20 +++++-- vibe/2026-11/2026-11-01-new-lints-crate.md | 58 ++++++++++++++++++- 15 files changed, 163 insertions(+), 40 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fc292ce57..5fbef2d38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,7 @@ Seven terms have exactly one meaning each, everywhere in this repository: code c - Docs: `cargo doc --workspace --no-deps --all-features --exclude workshop --exclude workshop-server --exclude workshop-server-api` with `RUSTDOCFLAGS="-D warnings"`. Rustdoc lints are not covered by clippy; never skip the docs gate. - Facade docs: `RUSTDOCFLAGS="-D warnings" cargo doc -p runvil --no-deps`, without `--all-features`, so the facade's docs build with default features. - Facade surface: `cargo + xtask api --check`, where the pinned nightly is the one named in `crates/build-xtask/src/api/toolchain.rs`; on any other toolchain it fails at once, naming the nightly it needs. It checks that every path a surface item's signature, fields, bounds, impls, or doc links name is a facade re-export (or std, core, alloc, or an allowlisted crate), that no surface doc text names an internal crate, and that the surface listing matches the committed `crates/runvil/public-api.txt`. Run it with `CARGO_BUILD_WARNINGS` unset (in PowerShell, `Remove-Item Env:CARGO_BUILD_WARNINGS` after the clippy gate set it), because the nightly cargo turns its own warnings into errors while that variable is set. +- Custom lints: `cargo dylint --all -- --locked --workspace --all-targets --all-features`, with `cargo-dylint` and `dylint-link` installed. It builds the dylint library in `lints/build-lints` on the nightly that crate's `rust-toolchain.toml` names and checks every workspace crate with it. The library sits outside the workspace and outside `crates/`, so the gates above never build it; its UI tests run with `cargo test` in `lints/build-lints`. A new lint is a module in that crate, registered in its `register_lints`, with a UI test under its `ui/`. - Boundary and structural checks: `cargo test -p build-xtask`. It enforces the product and container boundaries, the Workshop tier graph, the `## Invariants` marker, and lint inheritance. - Wording and rulebook scan: `node --test crates/workshop/ui/test/docs-claims.mjs`. It enforces the Engine wording rule, the rulebook Definitions, and the Pack lifecycle wording (installed, snapshotted, or declared). It needs no `npm ci` and runs from any directory, because the file resolves the repository root from its own path. diff --git a/crates/build-ui/tests/it/main.rs b/crates/build-ui/tests/it/main.rs index 23b95c6bd..925a595c7 100644 --- a/crates/build-ui/tests/it/main.rs +++ b/crates/build-ui/tests/it/main.rs @@ -1,6 +1,8 @@ //! Integration tests for the `build-ui` helper. use std::collections::BTreeSet; + +use anyhow::Context as _; use std::path::{Path, PathBuf}; use std::process::Command; @@ -30,8 +32,8 @@ fn both_implementers_emit_the_same_layout() -> anyhow::Result<()> { return Ok(()); } - let rust_dist = tempfile::TempDir::new()?; - let node_dist = tempfile::TempDir::new()?; + let rust_dist = tempfile::TempDir::new().context("create the Rust build's temp dir")?; + let node_dist = tempfile::TempDir::new().context("create the Node build's temp dir")?; build_ui::build_in( &ui_dir, @@ -48,7 +50,8 @@ fn both_implementers_emit_the_same_layout() -> anyhow::Result<()> { .arg("--out") .arg(node_dist.path()) .current_dir(&ui_dir) - .output()?; + .output() + .with_context(|| format!("run node build.mjs in {}", ui_dir.display()))?; anyhow::ensure!( output.status.success(), "node build.mjs --out failed (status {}):\n{}", @@ -99,8 +102,12 @@ fn file_set(root: &Path) -> anyhow::Result> { let mut files = BTreeSet::new(); let mut stack = vec![root.to_path_buf()]; while let Some(dir) = stack.pop() { - for entry in std::fs::read_dir(&dir)? { - let path = entry?.path(); + let entries = + std::fs::read_dir(&dir).with_context(|| format!("read dir {}", dir.display()))?; + for entry in entries { + let path = entry + .with_context(|| format!("read an entry of {}", dir.display()))? + .path(); if path.is_dir() { stack.push(path); } else { @@ -117,7 +124,10 @@ fn file_set(root: &Path) -> anyhow::Result> { /// Reads one file under `root` with its content hashes normalized. fn normalized_file(root: &Path, name: &str) -> anyhow::Result { - Ok(normalize_hashes(&std::fs::read_to_string(root.join(name))?)) + let path = root.join(name); + let text = + std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?; + Ok(normalize_hashes(&text)) } /// Replaces esbuild content hashes (`-XXXXXXXX` in base32, A-Z and 2-7) diff --git a/crates/build-xtask/src/new_crate.rs b/crates/build-xtask/src/new_crate.rs index 6a55336cc..83fb18579 100644 --- a/crates/build-xtask/src/new_crate.rs +++ b/crates/build-xtask/src/new_crate.rs @@ -3,6 +3,8 @@ use std::fs; use std::path::{Path, PathBuf}; +use anyhow::Context as _; + /// Scaffold `crates//` with a manifest, a facade `lib.rs` holding /// the invariant docs, a build script that runs the compile-time file-size /// check, and the crate's integration-test binary. @@ -19,14 +21,20 @@ pub(crate) fn scaffold(root: &Path, name: &str) -> anyhow::Result { "crate directory already exists: {}", dir.display() ); - fs::create_dir_all(dir.join("src"))?; - fs::create_dir_all(dir.join("tests").join("it"))?; - fs::write(dir.join("Cargo.toml"), manifest(name))?; - fs::write(dir.join("build.rs"), BUILD_RS)?; - fs::write(dir.join("src").join("lib.rs"), lib_rs(name))?; - fs::write( + let create = |path: PathBuf| { + fs::create_dir_all(&path).with_context(|| format!("create {}", path.display())) + }; + let write = |path: PathBuf, contents: &str| { + fs::write(&path, contents).with_context(|| format!("write {}", path.display())) + }; + create(dir.join("src"))?; + create(dir.join("tests").join("it"))?; + write(dir.join("Cargo.toml"), &manifest(name))?; + write(dir.join("build.rs"), BUILD_RS)?; + write(dir.join("src").join("lib.rs"), &lib_rs(name))?; + write( dir.join("tests").join("it").join("main.rs"), - test_main_rs(name), + &test_main_rs(name), )?; Ok(dir) } diff --git a/crates/runvil/tests/suite/parsing.rs b/crates/runvil/tests/suite/parsing.rs index 08f4151c4..bce20ece8 100644 --- a/crates/runvil/tests/suite/parsing.rs +++ b/crates/runvil/tests/suite/parsing.rs @@ -43,7 +43,9 @@ fn invalid_prompt_files_report_public_error_contracts() -> TestResult { let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/prompts/invalid"); for (stem, code, message_fragment) in INVALID_FIXTURES { let name = format!("invalid/{stem}.md"); - let source = fs::read_to_string(dir.join(format!("{stem}.md")))?; + let path = dir.join(format!("{stem}.md")); + let source = + fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?; let Err(error) = Prompt::parse(&source, &name, &name).0 else { return Err(format!("fixture {name} unexpectedly parsed").into()); }; diff --git a/crates/runvil/tests/suite/prepare.rs b/crates/runvil/tests/suite/prepare.rs index 65879c98d..10bd2f78e 100644 --- a/crates/runvil/tests/suite/prepare.rs +++ b/crates/runvil/tests/suite/prepare.rs @@ -59,7 +59,9 @@ fn two_runs_writing_the_same_real_file_through_the_shared_base_conflict() -> Tes matches!(error, VfsError::Conflict { .. }), "a determinism violation, not a backend error: {error}" ); - assert_eq!(std::fs::read(temp.path().join("shared.txt"))?, b"from a"); + let shared = temp.path().join("shared.txt"); + let bytes = std::fs::read(&shared).map_err(|e| format!("read {}: {e}", shared.display()))?; + assert_eq!(bytes, b"from a"); Ok(()) } diff --git a/crates/runvil/tests/suite/shipped.rs b/crates/runvil/tests/suite/shipped.rs index 285b9de56..6bb5d3cb7 100644 --- a/crates/runvil/tests/suite/shipped.rs +++ b/crates/runvil/tests/suite/shipped.rs @@ -27,7 +27,7 @@ fn research_ada_lovelace() -> TestResult { ) .args("Ada Lovelace") .config(move |ctx| ctx.with_tools(catalog.clone())) - .run(&fs::read_to_string(path)?) + .run(&fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?) } #[test] diff --git a/crates/runvil/tests/suite/support.rs b/crates/runvil/tests/suite/support.rs index bcdec57cf..8f6461f71 100644 --- a/crates/runvil/tests/suite/support.rs +++ b/crates/runvil/tests/suite/support.rs @@ -188,7 +188,8 @@ fn temp_roots_with_one_label_get_distinct_paths_and_drop_removes_the_folder() -> let (first, second) = (TempRoot::new(label), TempRoot::new(label)); assert_ne!(first.path(), second.path()); let dropped = first.path().to_owned(); - fs::write(dropped.join("f.txt"), b"x")?; + let file = dropped.join("f.txt"); + fs::write(&file, b"x").map_err(|e| format!("write {}: {e}", file.display()))?; drop(first); assert!(!dropped.exists(), "{} survived its drop", dropped.display()); assert!(second.path().is_dir(), "the second root outlives the first"); diff --git a/crates/workshop/desktop/build.rs b/crates/workshop/desktop/build.rs index 74ce82a71..6bcef4699 100644 --- a/crates/workshop/desktop/build.rs +++ b/crates/workshop/desktop/build.rs @@ -114,11 +114,17 @@ fn install_gateway_sidecar() -> Result<(), Box> { /// Copies `source` over `sidecar`, creating the directory and reporting /// the copy so a surprising sidecar is visible in the build log. -fn copy_forward(source: &Path, sidecar: &Path) -> std::io::Result<()> { +fn copy_forward(source: &Path, sidecar: &Path) -> Result<(), String> { if let Some(parent) = sidecar.parent() { - fs::create_dir_all(parent)?; + fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?; } - fs::copy(source, sidecar)?; + fs::copy(source, sidecar).map_err(|e| { + format!( + "copy the gateway sidecar {} to {}: {e}", + source.display(), + sidecar.display() + ) + })?; println!( "cargo:warning=installed gateway sidecar {} from {}", sidecar.display(), diff --git a/crates/workshop/run-log/src/append.rs b/crates/workshop/run-log/src/append.rs index 07fda9335..f6a09abf7 100644 --- a/crates/workshop/run-log/src/append.rs +++ b/crates/workshop/run-log/src/append.rs @@ -41,7 +41,10 @@ impl RunLog { /// apply the schema. pub async fn open(path: &Path) -> Result { let name = path.to_str().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "run log path must be utf-8") + LogError::io( + format!("open {}", path.display()), + io::Error::new(io::ErrorKind::InvalidInput, "run log path must be utf-8"), + ) })?; let conn = connect(name).await?; if layout::is_stale(&conn).await? { diff --git a/crates/workshop/run-log/src/error.rs b/crates/workshop/run-log/src/error.rs index 06431fc66..f04140841 100644 --- a/crates/workshop/run-log/src/error.rs +++ b/crates/workshop/run-log/src/error.rs @@ -22,9 +22,13 @@ pub enum LogError { #[source] source: DatabaseSource, }, - /// The log file could not be addressed; the I/O error is the source. - #[error("run log file")] + /// The log file could not be addressed; `context` names the operation + /// and the path, and the I/O error is the source. + #[error("run log file: {context}")] Io { + /// The operation and the path it addressed, such as + /// `rename /a/runs.db to /a/runs.db.stale-1`. + context: String, /// The I/O error. #[source] source: io::Error, @@ -49,9 +53,15 @@ pub enum LogError { Corrupt(String), } -impl From for LogError { - fn from(source: io::Error) -> Self { - LogError::Io { source } +impl LogError { + /// [`LogError::Io`] for `source`, with `context` naming the operation + /// and the path. There is no `From`: a bare `?` would drop + /// both. + pub(crate) fn io(context: impl Into, source: io::Error) -> Self { + LogError::Io { + context: context.into(), + source, + } } } diff --git a/crates/workshop/run-log/src/layout.rs b/crates/workshop/run-log/src/layout.rs index 16e64e9e5..6ab354655 100644 --- a/crates/workshop/run-log/src/layout.rs +++ b/crates/workshop/run-log/src/layout.rs @@ -55,16 +55,26 @@ pub(crate) async fn stamp(conn: &turso::Connection) -> Result<(), LogError> { /// Returns [`LogError::Io`] when a rename fails. pub(crate) async fn set_aside(path: &Path, stamp: i64) -> Result { let aside = with_suffix(path, &format!(".stale-{stamp}")); - tokio::fs::rename(path, &aside).await?; + rename(path, &aside).await?; for sidecar in SIDECARS { let from = with_suffix(path, sidecar); - if tokio::fs::try_exists(&from).await? { - tokio::fs::rename(&from, with_suffix(&aside, sidecar)).await?; + let exists = tokio::fs::try_exists(&from) + .await + .map_err(|e| LogError::io(format!("check {}", from.display()), e))?; + if exists { + rename(&from, &with_suffix(&aside, sidecar)).await?; } } Ok(aside) } +/// Renames `from` to `to`, naming both in the error. +async fn rename(from: &Path, to: &Path) -> Result<(), LogError> { + tokio::fs::rename(from, to) + .await + .map_err(|e| LogError::io(format!("rename {} to {}", from.display(), to.display()), e)) +} + /// The version the file was stamped with, `None` when it holds no stamp. async fn stamped(conn: &turso::Connection) -> Result, LogError> { let mut rows = conn.query(schema::SELECT_LAYOUT, ()).await?; diff --git a/crates/workshop/run-log/src/recorder.rs b/crates/workshop/run-log/src/recorder.rs index c0f19cd57..0aba27fbe 100644 --- a/crates/workshop/run-log/src/recorder.rs +++ b/crates/workshop/run-log/src/recorder.rs @@ -73,7 +73,9 @@ impl TursoRecorder { if let Some(log) = slot { Ok(log) } else { - tokio::fs::create_dir_all(&self.dir).await?; + tokio::fs::create_dir_all(&self.dir) + .await + .map_err(|e| LogError::io(format!("create {}", self.dir.display()), e))?; let log = RunLog::open(&self.path).await?; Ok(slot.insert(log)) } diff --git a/crates/workshop/server/src/agents/language_guide/tests.rs b/crates/workshop/server/src/agents/language_guide/tests.rs index 79e6174bb..a593a9e06 100644 --- a/crates/workshop/server/src/agents/language_guide/tests.rs +++ b/crates/workshop/server/src/agents/language_guide/tests.rs @@ -31,8 +31,9 @@ const TEXT: &str = "# Hello from a local page\n"; /// A directory holding the one page. fn local_directory() -> Result> { - let directory = tempfile::tempdir()?; - std::fs::write(directory.path().join(PAGE), TEXT)?; + let directory = tempfile::tempdir().map_err(|e| format!("create a temp dir: {e}"))?; + let page = directory.path().join(PAGE); + std::fs::write(&page, TEXT).map_err(|e| format!("write {}: {e}", page.display()))?; Ok(directory) } @@ -96,7 +97,10 @@ fn the_mapping_refuses_a_write_and_leaves_the_page_alone() -> Outcome { matches!(replaced, Err(VfsError::ReadOnly { .. })), "{replaced:?}" ); - assert_eq!(std::fs::read_to_string(directory.path().join(PAGE))?, TEXT); + let page = directory.path().join(PAGE); + let text = + std::fs::read_to_string(&page).map_err(|e| format!("read {}: {e}", page.display()))?; + assert_eq!(text, TEXT); assert!(!directory.path().join("new.md").exists()); Ok(()) } diff --git a/crates/workshop/server/src/serve.rs b/crates/workshop/server/src/serve.rs index 130305e74..b0b9eeb73 100644 --- a/crates/workshop/server/src/serve.rs +++ b/crates/workshop/server/src/serve.rs @@ -191,7 +191,8 @@ fn spawn_inner( ) -> Result { // Composing state sweeps the state directory and reopening follows // the workspace pointer, so a refused address must fail before either. - loopback_address(&config.server.bind)?; + // `loopback_address` names the address in its message already. + loopback_address(&config.server.bind).map_err(SpawnError::Io)?; // Discovery runs before the server thread starts: a resolution // failure is the plain no-gateway error, never a bind-then-fail. let gateway = match gateway { @@ -204,7 +205,13 @@ fn spawn_inner( let (stopped_tx, stopped_rx) = mpsc::channel(); let thread = std::thread::Builder::new() .name("workshop-server".to_string()) - .spawn(move || serve_thread(config, gateway, ready_tx, shutdown_rx, &stopped_tx, grace))?; + .spawn(move || serve_thread(config, gateway, ready_tx, shutdown_rx, &stopped_tx, grace)) + .map_err(|e| { + SpawnError::Io(std::io::Error::new( + e.kind(), + format!("spawn the workshop-server thread: {e}"), + )) + })?; match ready_rx.recv() { Ok(Ok((url, gateway))) => Ok(ServerHandle { url, @@ -333,9 +340,12 @@ fn serve_thread( /// Parses `address` as a socket address and refuses any that is not /// loopback, with [`std::io::ErrorKind::InvalidInput`] either way. fn loopback_address(address: &str) -> std::io::Result { - let addr: std::net::SocketAddr = address - .parse() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + let addr: std::net::SocketAddr = address.parse().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("parse bind address {address}: {e}"), + ) + })?; if !addr.ip().is_loopback() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, diff --git a/vibe/2026-11/2026-11-01-new-lints-crate.md b/vibe/2026-11/2026-11-01-new-lints-crate.md index f45032fa2..37dfbc36d 100644 --- a/vibe/2026-11/2026-11-01-new-lints-crate.md +++ b/vibe/2026-11/2026-11-01-new-lints-crate.md @@ -22,10 +22,10 @@ todos: status: completed - id: fix-sites content: "Add context at the 29 existing sites, then run `cargo dylint --all` with the lint at its Deny default" - status: pending + status: completed - id: ci content: "Add a CI job that installs cargo-dylint and dylint-link, runs `cargo test` in lints/build-lints, and runs `cargo dylint --all -- --workspace --all-targets --all-features`; document the command in AGENTS.md Verification" - status: pending + status: completed - id: naming content: "Rename runvil-lints to build-lints: `runvil-*` crates are by definition the Engine, and this crate is not" status: completed @@ -132,6 +132,31 @@ These counts may include a site twice when it compiles for both a lib and its lib test target. The Engine and Harness crates have no sites outside the `runvil` facade's test suite. +### How each site was fixed + +- anyhow callers (`build-xtask`'s `new_crate.rs`, `build-ui`'s tests): add + `.context` or `.with_context` naming the operation and the path. + `new_crate.rs` uses two local closures, `create` and `write`, so each of + the six file operations names its path once. +- `Box` test helpers (the `runvil` suite, `workshop-server`'s + language guide tests): `.map_err(|e| format!("read {}: {e}", ...))`. +- `workshop-desktop`'s `build.rs`: `copy_forward` returns + `Result<(), String>` and names the source, the sidecar, and the + directory it creates, instead of returning a bare `io::Result` to two + callers. +- `workshop-run-log`: `LogError::Io` gains a `context: String` field + (`run log file: {context}`), and `impl From for LogError` + is replaced by `LogError::io(context, source)`. Without the `From`, a + bare `?` on an `io::Result` no longer compiles anywhere in the crate, + so the type system enforces the rule there as well as the lint. A + private `rename` helper in `layout.rs` names both paths. +- `workshop-server`'s `serve.rs`: `SpawnError::Io` keeps its + `io::Error`, because tests match on its kind. The context goes into the + error's own message: `loopback_address` names the address when it fails + to parse it, and the thread spawn failure says + `spawn the workshop-server thread`. The call site converts with an + explicit `.map_err(SpawnError::Io)`. + ## The crate name: `build-lints` The dylint library name is `build_lints`. @@ -145,3 +170,32 @@ The dylint library name is `build_lints`. the root manifest's metadata table break no structural check. - `node --test crates/workshop/ui/test/docs-claims.mjs`: 4 passed. - `cargo fmt --all --check`: clean. +- After the site fixes: `cargo dylint --all -- --workspace --all-targets + --all-features` at the Deny default finds nothing. Both clippy gates + with `CARGO_BUILD_WARNINGS=deny` are clean. nextest for `runvil`, + `build-xtask`, `build-ui`, and `workshop-run-log`: 340 passed. Workshop + crates: 313 passed, and 1 failed: + `realtime_relay::overload::stalled_browser_cleanup_is_bounded_after_gateway_disconnect`. + That test also fails with the site fixes stashed, so the failure was + already there before this change. + +## CI + +The `dylint` job in `.github/workflows/ci.yml`, listed in `ci-green`'s +`needs`: + +- installs `cargo-dylint` and `dylint-link` 6.3.2 through + `taiki-e/install-action`; +- reads the nightly from `lints/build-lints/rust-toolchain.toml` with + `sed`, as `api-surface` and `msrv` read their toolchains, and installs it + with `rustc-dev` and `llvm-tools-preview`. A bare + `rustup toolchain install` reads the file only on rustup 1.28 and later; +- runs on `ubuntu-22.04` with `check-workshop-linux`'s Tauri packages, + `npm ci`, and stub Gateway sidecar, because `cargo dylint` compiles the + desktop app too; +- runs `cargo test --locked` in `lints/build-lints`, then + `cargo dylint --all -- --locked --workspace --all-targets --all-features`, + then checks that the tree is clean. + +AGENTS.md gains the rule under Engineering and the command under +Verification. From fbe8f005034729729eb2ed95a3269d41ae6ba464 Mon Sep 17 00:00:00 2001 From: David Rauschenbach Date: Wed, 4 Nov 2026 10:54:02 -0800 Subject: [PATCH 3/3] add to CI --- .github/workflows/ci.yml | 78 +++++++++++++++++++++- vibe/2026-11/2026-11-01-new-lints-crate.md | 7 +- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31c8d90df..781dd5d6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -315,6 +315,82 @@ jobs: - name: Nightly-only fixtures run: cargo nextest run --locked -p build-xtask --run-ignored only + # The workspace's own lints, the dylint library in lints/build-lints. It + # builds on the nightly in that crate's rust-toolchain.toml, never on the + # workspace's stable. The date is read from that file, never repeated + # here. `cargo dylint` checks every crate, the desktop app included, so + # this job installs what check-workshop-linux does. It runs on 24.04, + # not check-workshop-linux's 22.04: the prebuilt cargo-dylint and + # dylint-link binaries need glibc 2.38 or later, and 22.04 ships 2.35. + dylint: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + # The same release as the `dylint_linting` the library depends on. + - name: Install cargo-dylint and dylint-link + uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2 + with: + tool: cargo-dylint@6.3.2, dylint-link@6.3.2 + + - name: Install the lints' nightly + shell: bash + run: | + set -euo pipefail + nightly="$(sed -n 's/^channel = "\(nightly-[0-9-]*\)"$/\1/p' lints/build-lints/rust-toolchain.toml)" + case "$nightly" in + nightly-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; + *) + echo "expected one nightly channel in lints/build-lints/rust-toolchain.toml, found: '$nightly'" + exit 1 + ;; + esac + rustup toolchain install "$nightly" --profile minimal --component rustc-dev,llvm-tools-preview + + - name: Cache cargo + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + with: + workspaces: | + . -> target + lints/build-lints -> target + + - name: Install Tauri system packages + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev libssl-dev librsvg2-dev + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: crates/workshop/package-lock.json + + - name: Install UI dependencies + run: npm ci --prefix crates/workshop + + - name: Stage a stub Gateway sidecar + run: node tools/fetch-gateway-sidecar.mjs --stub --target x86_64-unknown-linux-gnu + + - name: Lint UI tests + working-directory: lints/build-lints + run: cargo test --locked + + - name: Dylint + run: cargo dylint --all -- --locked --workspace --all-targets --all-features + + - name: Remove Gateway sidecar + if: always() + run: node tools/stage-gateway-sidecar.mjs remove --target x86_64-unknown-linux-gnu + + - name: Clean tree + shell: bash + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "the build dirtied the repository:" + git status --porcelain + exit 1 + fi + # The Engine crates inherit `rust-version` from the root Cargo.toml's # [workspace.package]; this job builds them at that toolchain. The version # is read from the manifest, never repeated here, so the two cannot drift. @@ -351,7 +427,7 @@ jobs: # or that job bypasses this gate. ci-green: runs-on: ubuntu-latest - needs: [fmt, clippy, test, docs, check-workshop, check-workshop-linux, ui, supply-chain, api-surface, msrv] + needs: [fmt, clippy, test, docs, check-workshop, check-workshop-linux, ui, supply-chain, api-surface, dylint, msrv] if: always() steps: - name: Verify every job succeeded diff --git a/vibe/2026-11/2026-11-01-new-lints-crate.md b/vibe/2026-11/2026-11-01-new-lints-crate.md index 37dfbc36d..17cc11561 100644 --- a/vibe/2026-11/2026-11-01-new-lints-crate.md +++ b/vibe/2026-11/2026-11-01-new-lints-crate.md @@ -190,9 +190,12 @@ The `dylint` job in `.github/workflows/ci.yml`, listed in `ci-green`'s `sed`, as `api-surface` and `msrv` read their toolchains, and installs it with `rustc-dev` and `llvm-tools-preview`. A bare `rustup toolchain install` reads the file only on rustup 1.28 and later; -- runs on `ubuntu-22.04` with `check-workshop-linux`'s Tauri packages, +- runs on `ubuntu-24.04` with `check-workshop-linux`'s Tauri packages, `npm ci`, and stub Gateway sidecar, because `cargo dylint` compiles the - desktop app too; + desktop app too. It does not use `check-workshop-linux`'s `ubuntu-22.04`, + because the prebuilt `cargo-dylint` and `dylint-link` binaries need + glibc 2.38 or later and 22.04 ships 2.35. The first CI run on 22.04 + failed with "version `GLIBC_2.38' not found"; - runs `cargo test --locked` in `lints/build-lints`, then `cargo dylint --all -- --locked --workspace --all-targets --all-features`, then checks that the tree is clean.