diff --git a/.config/nextest.toml b/.config/nextest.toml index 22498d0789e3..6952eaa8d7de 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -32,11 +32,6 @@ slow-timeout = { period = "120s", terminate-after = 3 } filter = 'test(state_migration_generate_actors_metadata)' slow-timeout = { period = "120s", terminate-after = 3 } -[[profile.default.overrides]] -# lint runs `cargo check` for source file discovery, which can take a while -filter = 'binary(lint)' -slow-timeout = { period = "120s", terminate-after = 3 } - # These tests download test snapshots from the network, which can take a while. # There might be some network issues, so we allow some retries with backoff. # Jitter is enabled to avoid [thundering herd issues](https://en.wikipedia.org/wiki/Thundering_herd_problem). diff --git a/Cargo.lock b/Cargo.lock index e2dd0a03adad..90efbb06e60e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,16 +250,6 @@ dependencies = [ "password-hash", ] -[[package]] -name = "ariadne" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8454c8a44ce2cb9cc7e7fae67fc6128465b343b92c6631e94beca3c8d1524ea5" -dependencies = [ - "unicode-width", - "yansi", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -743,7 +733,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.2", + "rustc-hash", "shlex", "syn 2.0.117", ] @@ -911,16 +901,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "bytes", - "cfg_aliases", -] - [[package]] name = "brotli" version = "8.0.2" @@ -1004,39 +984,6 @@ dependencies = [ "serde", ] -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -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.18", -] - [[package]] name = "cast" version = "0.3.0" @@ -1529,12 +1476,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "countme" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1610,7 +1551,7 @@ dependencies = [ "log", "pulley-interpreter", "regalloc2", - "rustc-hash 2.1.2", + "rustc-hash", "serde", "smallvec", "target-lexicon", @@ -2319,12 +2260,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "drop_bomb" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" - [[package]] name = "dtoa" version = "1.0.11" @@ -3257,7 +3192,6 @@ dependencies = [ "anes 0.2.1", "anyhow", "argon2", - "ariadne", "assert_cmd", "async-compression", "async-fs", @@ -3274,7 +3208,6 @@ dependencies = [ "blstrs", "byteorder", "bytes", - "cargo_metadata", "cbor4ii", "cfg-if", "cfg-vis", @@ -3335,7 +3268,6 @@ dependencies = [ "get-size2", "gethostname", "git-version", - "glob", "group", "hashbrown 0.17.0", "hashlink 0.11.0", @@ -3386,20 +3318,17 @@ dependencies = [ "positioned-io", "predicates", "pretty_assertions", - "proc-macro2", "prometheus-client", "puruspe", "quick-protobuf", "quick-protobuf-codec", "quickcheck", "quickcheck_macros", - "ra_ap_syntax", "rand 0.8.6", "rand_chacha 0.3.1", "rand_distr", "rayon", "regex", - "regex-automata", "reqwest 0.13.2", "rlimit", "rlp", @@ -3428,7 +3357,6 @@ dependencies = [ "static_assertions", "statrs", "strum", - "syn 2.0.117", "tabled", "tap", "tar", @@ -5148,12 +5076,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jod-thread" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24" - [[package]] name = "js-sys" version = "0.3.95" @@ -5222,7 +5144,7 @@ dependencies = [ "parking_lot", "pin-project", "rand 0.9.4", - "rustc-hash 2.1.2", + "rustc-hash", "serde", "serde_json", "thiserror 2.0.18", @@ -6153,15 +6075,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "merkletree" version = "0.23.0" @@ -6222,15 +6135,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[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 = "moka" version = "0.12.15" @@ -6938,7 +6842,7 @@ dependencies = [ "indexmap 2.14.0", "integer-sqrt", "num-traits", - "rustc-hash 2.1.2", + "rustc-hash", "thiserror 2.0.18", ] @@ -7548,7 +7452,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.2", + "rustc-hash", "rustls", "socket2 0.6.3", "thiserror 2.0.18", @@ -7569,7 +7473,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.2", + "rustc-hash", "rustls", "rustls-pki-types", "slab", @@ -7636,72 +7540,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "ra-ap-rustc_lexer" -version = "0.160.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b50f19d5856b8e2b36150e89b53a6102ab096e8044e1f55fd6fef977b10d85" -dependencies = [ - "memchr", - "unicode-ident", - "unicode-properties", -] - -[[package]] -name = "ra_ap_edition" -version = "0.0.329" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37fefe2a74525f036bb8c8d7a180a8a2e3cf77dbaeecd50b7f73d88ac5667e00" - -[[package]] -name = "ra_ap_parser" -version = "0.0.329" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46af96c3979d0ad4249cc7906a4fdf1815cb353d56fb07c3c37296f034e5692d" -dependencies = [ - "drop_bomb", - "ra-ap-rustc_lexer", - "ra_ap_edition", - "rustc-literal-escaper", - "tracing", - "winnow 0.7.15", -] - -[[package]] -name = "ra_ap_stdx" -version = "0.0.329" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c644315338b6df153f9688e33090c466a73792eb1dcf933654270f768a950f" -dependencies = [ - "crossbeam-channel", - "crossbeam-utils", - "itertools 0.14.0", - "jod-thread", - "libc", - "miow", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "ra_ap_syntax" -version = "0.0.329" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c3430505dccc8c96acbc9c696fb72220cefdd4b050f1a567b547ef9a6351f1" -dependencies = [ - "either", - "itertools 0.14.0", - "ra_ap_parser", - "ra_ap_stdx", - "rowan", - "rustc-hash 2.1.2", - "rustc-literal-escaper", - "smallvec", - "smol_str", - "tracing", - "triomphe", -] - [[package]] name = "radium" version = "0.7.0" @@ -7909,7 +7747,7 @@ dependencies = [ "bumpalo", "hashbrown 0.15.5", "log", - "rustc-hash 2.1.2", + "rustc-hash", "smallvec", ] @@ -8131,19 +7969,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" -[[package]] -name = "rowan" -version = "0.15.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f509095fc8cc0c8c8564016771d458079c11a8d857e65861f045145c0d3208" -dependencies = [ - "countme", - "hashbrown 0.14.5", - "memoffset", - "rustc-hash 1.1.0", - "text-size", -] - [[package]] name = "rs-car" version = "0.5.0" @@ -8277,12 +8102,6 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.2" @@ -8295,12 +8114,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" -[[package]] -name = "rustc-literal-escaper" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab03008eb631b703dd16978282ae36c73282e7922fe101a4bd072a40ecea7b8b" - [[package]] name = "rustc_version" version = "0.4.1" @@ -9050,16 +8863,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "smol_str" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" -dependencies = [ - "borsh", - "serde_core", -] - [[package]] name = "snap" version = "1.1.1" @@ -9707,12 +9510,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "text-size" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" - [[package]] name = "thiserror" version = "1.0.69" @@ -10271,12 +10068,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "triomphe" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" - [[package]] name = "try-lock" version = "0.2.5" @@ -10329,12 +10120,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-segmentation" version = "1.13.2" diff --git a/Cargo.toml b/Cargo.toml index 71473c0b77e5..9d9a48ae50cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -248,31 +248,24 @@ termios = "0.3" [dev-dependencies] all_asserts = "2" -ariadne = "0.6" assert_cmd = "2" bimap = "0.6" -cargo_metadata = "0.23" criterion = { version = "0.8", features = ["async_tokio", "csv_output"] } cs_serde_bytes = "0.12" derive-quickcheck-arbitrary = "0.1" fickle = "0.3" fvm_shared3 = { package = "fvm_shared", version = "~3.14", features = ["arb", "proofs", "testing"] } fvm_shared4 = { package = "fvm_shared", version = "~4.8", features = ["arb", "proofs", "testing"] } -glob = "0.3" http-range-header = "0.4" insta = { version = "1", features = ["yaml"] } libp2p-swarm-test = { workspace = true } num-bigint = { version = "0.4", features = ['quickcheck'] } petgraph = "0.8" predicates = "3" -proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } quickcheck = "1" quickcheck_macros = "1" -ra_ap_syntax = "0.0.329" -regex-automata = "0.4" rstest = "0.26" serial_test = "3" -syn = { version = "2", default-features = false, features = ["full", "parsing", "visit", "printing", "extra-traits"] } tokio-test = "0.4" [build-dependencies] diff --git a/tests/lint.rs b/tests/lint.rs deleted file mode 100644 index 0534ebb8ef12..000000000000 --- a/tests/lint.rs +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright 2019-2026 ChainSafe Systems -// SPDX-License-Identifier: Apache-2.0, MIT -//! # Custom lints -//! -//! A simple, syntactical custom linting framework for forest, to unconditionally -//! forbid certain constructs. -//! -//! Excessive custom lints can be a codebase hazard, so careful consideration is -//! required for what to lint. -//! -//! Out of scope for the current design: -//! - Any conditionality. -//! We intentionally don't support any `#[allow(..)]`-type gating. -//! - Resolved types, modules. -//! - Cross-file scope. -//! -//! ## Alternative designs. -//! -//! [`clippy`](https://github.com/rust-lang/rust-clippy/) can handle all of the -//! "out of scope" points above. -//! But is a lot more heavyweight, as is the similar project [`dylint`](https://github.com/trailofbits/dylint). -//! -//! If we need more functionality, we should consider porting. -//! -//! ## Technical overview -//! -//! - We parse `rustc`'s Makefile-style dependency files to know which source files -//! to lint. -//! This means that new, e.g. `examples/...` artifacts don't need special handling. -//! - We use [`syn`] to parse source files into an Abstract Syntax Tree. -//! These are inputs to the custom linters, which are run on each file. -//! Linters return [`proc_macro2::Span`]s to point to lint violations. -//! - We use [`ariadne`] to format violations into pretty `rustc`-style error -//! messages. -//! This involves converting [`proc_macro2::Span`]s to utf-8 character offsets -//! into the file. - -mod lints; - -use std::{fs, ops::Range}; - -use ariadne::{Color, ReportKind, Source}; -use cargo_metadata::camino::{Utf8Path, Utf8PathBuf}; -use lints::{Lint, Violation}; -use proc_macro2::{LineColumn, Span}; -use syn::visit::Visit; -use tracing::{debug, info, level_filters::LevelFilter}; -use tracing_subscriber::{EnvFilter, util::SubscriberInitExt as _}; - -#[test] -fn lint() { - let _guard = tracing_subscriber::fmt() - .without_time() - .with_env_filter( - EnvFilter::builder() - .with_default_directive(LevelFilter::DEBUG.into()) - .from_env() - .unwrap(), - ) - .set_default(); - LintRunner::new() - .run::() - .run::() - .run_comment_linter() - .finish(); -} - -#[must_use = "you must drive the runner to completion"] -struct LintRunner { - files: Cache, - num_violations: usize, -} - -impl LintRunner { - /// Performs source file discovery and parsing. - pub fn new() -> Self { - info!("collecting source files..."); - - // The initial implementation here tried to ask `cargo` and `rustc` - // what the source files were, but it was flaky. - // - // So just go for a simple globbing of well-known directories. - - let files = [ - concat!(env!("CARGO_MANIFEST_DIR"), "/build.rs"), - concat!(env!("CARGO_MANIFEST_DIR"), "/src/**/*.rs"), - concat!(env!("CARGO_MANIFEST_DIR"), "/tests/**/*.rs"), - concat!(env!("CARGO_MANIFEST_DIR"), "/benches/**/*.rs"), - concat!(env!("CARGO_MANIFEST_DIR"), "/examples/**/*.rs"), - ] - .into_iter() - .map(glob::glob) - .flat_map(|it| { - it.expect("patterns above are valid") - .map(|it| it.expect("couldn't compare globbed path with pattern")) - }) - .map(|path| { - debug!(?path, "import file"); - // skip files we can't read or aren't syntactically valid - let path = Utf8PathBuf::from_path_buf(path).unwrap(); - let s = fs::read_to_string(&path).expect("couldn't read file"); - let s = SourceFile::try_from(s).expect("couldn't parse file"); - (path, s) - }) - .collect::(); - - info!(num_source_files = files.map.len()); - - Self { - files, - num_violations: 0, - } - } - - /// Run the given linter. - /// - /// This prints out any messages, and updates the internal failure count. - pub fn run Visit<'a> + Default + Lint>(mut self) -> Self { - info!("running {}", std::any::type_name::()); - let mut linter = T::default(); - let mut all_violations = vec![]; - for (path, SourceFile { linewise, ast, .. }) in self.files.map.iter() { - linter.visit_file(ast); - for Violation { - span, - message, - color, - } in linter.flush() - { - let mut label = ariadne::Label::new((path, span2span(linewise, span))); - if let Some(message) = message { - label = label.with_message(message) - } - if let Some(color) = color { - label = label.with_color(color) - } - all_violations.push(label) - } - } - let num_violations = all_violations.len(); - let auto = Utf8PathBuf::new(); // ariadne figures out the file label if it doesn't have one - let mut builder = ariadne::Report::build(ReportKind::Error, (&auto, 0..1)) - .with_labels(all_violations) - .with_message(T::DESCRIPTION); - if let Some(help) = T::HELP { - builder.set_help(help) - } - if let Some(note) = T::NOTE { - builder.set_note(note) - } - match num_violations { - _none @ 0 => {} - _mid @ 1..=20 => { - builder.finish().print(&self.files).unwrap(); - } - _many => { - builder - .with_config(ariadne::Config::default().with_compact(true)) - .finish() - .print(&self.files) - .unwrap(); - } - } - self.num_violations += num_violations; - self - } - /// Panics with an appropriate error message on failure - pub fn finish(self) { - match self.num_violations { - 0 => { - println!("no violations found in {} files", self.files.map.len()); - } - nonzero => { - panic!( - "found {} violations in {} files", - nonzero, - self.files.map.len() - ); - } - } - } -} - -impl LintRunner { - /// Special case comments because: - /// - They operate on a concrete syntax tree. - /// (This is because `rustc`'s lexer diregards comments). - /// - We get byteoffset spans from [`rowan`](https://docs.rs/rowan/latest/rowan), - /// not char-offset spans. - pub fn run_comment_linter(mut self) -> Self { - use ra_ap_syntax::{AstNode as _, AstToken as _, ast}; - use regex_automata::{Anchored, Input, meta::Regex}; - info!("linting comments"); - let mut all_violations = vec![]; - let finder = Regex::new("(TODO)|(XXX)|(FIXME)").unwrap(); - let checker = Regex::new(r"TODO\(.*\): https://github.com/").unwrap(); - for (path, SourceFile { plaintext, .. }) in self.files.map.iter() { - for comment in - ra_ap_syntax::SourceFile::parse(plaintext, ra_ap_syntax::Edition::Edition2021) - .tree() - .syntax() // downcast from AST to untyped syntax tree - .descendants_with_tokens() // comments are tokens - .filter_map(|it| it.into_token().and_then(ast::Comment::cast)) - { - let haystack = comment.text(); - for found in finder.find_iter(haystack) { - if !checker.is_match( - Input::new(comment.text()) - .range(found.start()..) - .anchored(Anchored::Yes), - ) { - let byte_offset_of_comment_in_file = - usize::from(comment.syntax().text_range().start()); - let byte_offset_of_todo_in_comment = found.start(); - let byte_offset_needle = - byte_offset_of_comment_in_file + byte_offset_of_todo_in_comment; - let char_offset = plaintext - .char_indices() - .enumerate() - .find_map(|(char_offset, (byte_offset_haystack, _char))| { - (byte_offset_haystack == byte_offset_needle).then_some(char_offset) - }) - .unwrap(); - all_violations.push( - ariadne::Label::new(( - path, - char_offset..char_offset + (haystack[found.range()].len()), - )) - .with_color(Color::Red), - ); - } - } - } - } - let num_violations = all_violations.len(); - let auto = Utf8PathBuf::new(); // ariadne figures out the file label if it doesn't have one - let builder = ariadne::Report::build(ReportKind::Error, (&auto, 0..1)) - .with_labels(all_violations) - .with_message("TODOs must have owners and tracking issues") - .with_help("Change these to be `TODO(): https://github.com/ChainSafe/forest/issues/"); - match num_violations { - 0 => {} - _ => { - builder - .with_config(ariadne::Config::default().with_compact(true)) - .finish() - .print(&self.files) - .unwrap(); - } - } - self.num_violations += num_violations; - self - } -} - -struct SourceFile { - plaintext: String, - /// For formatting. - linewise: ariadne::Source, - /// Abstract syntax tree. - ast: syn::File, -} - -impl TryFrom for SourceFile { - type Error = syn::Error; - - fn try_from(plaintext: String) -> Result { - Ok(Self { - ast: syn::parse_file(&plaintext)?, - linewise: ariadne::Source::from(plaintext.clone()), - plaintext, - }) - } -} - -/// Stores all the files for repeated linting and formatting into pretty reports -struct Cache { - map: ahash::HashMap, -} - -impl ariadne::Cache for &Cache -where - Id: AsRef, -{ - type Storage = String; - - fn fetch(&mut self, id: &Id) -> Result<&Source, impl std::fmt::Debug> { - fn id_not_found_error(id: impl AsRef) -> Box { - Box::new(format!("{} not in cache", id.as_ref())) - } - - self.map - .get(Utf8Path::new(&id)) - .map(|SourceFile { linewise, .. }| linewise) - .ok_or_else(|| id_not_found_error(id)) - } - - fn display<'a>(&self, id: &'a Id) -> Option { - Some(Box::new(id.as_ref())) - } -} - -impl FromIterator<(Utf8PathBuf, SourceFile)> for Cache { - fn from_iter>(iter: T) -> Self { - Self { - map: iter.into_iter().collect(), - } - } -} - -fn span2span(text: &Source, span: Span) -> Range { - coord2offset(text, span.start())..coord2offset(text, span.end()) -} - -fn coord2offset(text: &Source, coord: LineColumn) -> usize { - let line = text.line(coord.line - 1).expect("line is past end of file"); - assert!(coord.column <= line.len(), "column is past end of line"); - line.offset() + coord.column -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - #[should_panic = "found 3 violations in 1 files"] - fn should_lint_bad_comments() { - LintRunner { - files: Cache::from_iter([( - Utf8PathBuf::from("test.rs"), - SourceFile::try_from(String::from( - " - // TODO - const _: () = {}; - fn foo() { - /* FIXME */ - } - // XXX: a comment left by David - mod bar; - ", - )) - .unwrap(), - )]), - num_violations: 0, - } - .run_comment_linter() - .finish(); - } - - #[test] - fn should_not_lint_good_comments() { - LintRunner { - files: Cache::from_iter([( - Utf8PathBuf::from("test.rs"), - SourceFile::try_from(String::from( - "// TODO(aatifsyed): https://github.com/ChainSafe/forest/issues/1234", - )) - .unwrap(), - )]), - num_violations: 0, - } - .run_comment_linter() - .finish(); - } -} diff --git a/tests/lints/mod.rs b/tests/lints/mod.rs deleted file mode 100644 index 3148f04b17c2..000000000000 --- a/tests/lints/mod.rs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2019-2026 ChainSafe Systems -// SPDX-License-Identifier: Apache-2.0, MIT -use std::{fmt::Display, mem}; - -use ariadne::Color; -use proc_macro2::Span; -use syn::{ - BinOp, Expr, ItemFn, Macro, ReturnType, Token, parse_quote, - punctuated::Punctuated, - spanned::Spanned, - visit::{self, Visit}, -}; - -/// A custom linter. -/// -/// Names of linters should describe the codebase state that the lint "wants". -/// e.g [`NoTestsWithReturn`]: "there are no tests with return values". -pub trait Lint { - /// Finish linting a single file. - /// - /// The linter must not retain any [`Span`]s after this is called. - fn flush(&mut self) -> Vec; - - /// The top-level explanation of the lint, as a declaration. - const DESCRIPTION: &'static str; - /// Why this lint exists. - const NOTE: Option<&'static str> = None; - /// What code changes the user should make. - const HELP: Option<&'static str> = None; -} - -/// A linting violation -pub struct Violation { - pub span: Span, - pub message: Option, - pub color: Option, -} - -impl Violation { - pub fn new(span: impl Spanned) -> Self { - Self { - span: span.span(), - message: None, - color: None, - } - } - pub fn with_message(mut self, msg: impl Display) -> Self { - self.message = Some(msg.to_string()); - self - } - pub fn with_color(mut self, color: Color) -> Self { - self.color = Some(color); - self - } -} - -#[cfg(test)] -#[track_caller] -fn should_lint Visit<'ast> + Lint>(file: syn::File) { - let mut linter = T::default(); - linter.visit_file(&file); - assert!(!linter.flush().is_empty()) -} - -#[cfg(test)] -#[track_caller] -fn should_not_lint Visit<'ast> + Lint>(file: syn::File) { - let mut linter = T::default(); - linter.visit_file(&file); - assert!(linter.flush().is_empty()) -} - -////////// -// Linters -////////// - -#[derive(Default)] -pub struct NoTestsWithReturn { - violations: Vec, -} - -impl<'ast> Visit<'ast> for NoTestsWithReturn { - fn visit_item_fn(&mut self, i: &'ast ItemFn) { - if i.attrs - .iter() - .any(|attr| attr == &parse_quote!(#[test]) || attr == &parse_quote!(#[tokio::test])) - && let ReturnType::Type(..) = i.sig.output - { - self.violations.push( - Violation::new(&i.sig.output) - .with_message("not allowed to have a return type") - .with_color(Color::Red), - ) - } - visit::visit_item_fn(self, i) - } -} - -impl Lint for NoTestsWithReturn { - fn flush(&mut self) -> Vec { - mem::take(&mut self.violations) - } - - const DESCRIPTION: &'static str = "`#[test]` functions are not allowed to have a return type"; - const NOTE: Option<&'static str> = - Some("`assert`s and `unwrap`s provide better error messages in tests"); - const HELP: Option<&'static str> = - Some("Remove the return type, and any `?` error propogations"); -} - -#[test] -fn no_tests_with_return() { - should_lint::(parse_quote! { - #[test] - fn foo() -> Bar { - todo!() - } - }); - should_not_lint::(parse_quote! { - #[test] - fn foo() {} - fn bar() -> Bar {} - }); -} - -#[derive(Default)] -pub struct SpecializedAssertions { - violations: Vec, -} - -impl<'ast> Visit<'ast> for SpecializedAssertions { - fn visit_macro(&mut self, i: &'ast Macro) { - if i.path.is_ident("assert") - && let Ok(exprs) = i.parse_body_with(Punctuated::::parse_terminated) - && let Some(Expr::Binary(binary)) = exprs.first() - { - match binary.op { - BinOp::Eq(_) => self.violations.push( - Violation::new(i) - .with_message("should be `assert_eq(..)`") - .with_color(Color::Red), - ), - BinOp::Ne(_) => self.violations.push( - Violation::new(i) - .with_message("should be `assert_ne(..)`") - .with_color(Color::Red), - ), - _ => {} - } - } - visit::visit_macro(self, i) - } -} - -impl Lint for SpecializedAssertions { - fn flush(&mut self) -> Vec { - mem::take(&mut self.violations) - } - - const DESCRIPTION: &'static str = "`assert!(..)` that should use a more specialized macro"; - const NOTE: Option<&'static str> = Some("specialized macros provides better error messages"); -} - -#[test] -fn specialized_assertions() { - should_lint::(parse_quote! { - assert!(1 != 2); - }); - should_lint::(parse_quote! { - assert!(1 == 1, "these should be equal"); - }); - should_not_lint::(parse_quote! { - assert_ne!(1, 2); - }); - should_not_lint::(parse_quote! { - assert_eq!(1, 2, "these should be equal"); - }); -}