diff --git a/CHANGELOG.md b/CHANGELOG.md index 531c8794a572..1e1b4879da5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ document. [500e0ff...master](https://github.com/rust-lang/rust-clippy/compare/500e0ff...master) +### New Lints + +* Added [`too_many_lines_in_file`] to `restriction` + [#16675](https://github.com/rust-lang/rust-clippy/pull/16675) + ## Rust 1.94 Current stable, released 2026-03-05 @@ -7168,6 +7173,7 @@ Released 2018-09-13 [`too_long_first_doc_paragraph`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_long_first_doc_paragraph [`too_many_arguments`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments [`too_many_lines`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_lines +[`too_many_lines_in_file`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_lines_in_file [`toplevel_ref_arg`]: https://rust-lang.github.io/rust-clippy/master/index.html#toplevel_ref_arg [`trailing_empty_array`]: https://rust-lang.github.io/rust-clippy/master/index.html#trailing_empty_array [`trait_duplication_in_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#trait_duplication_in_bounds @@ -7400,6 +7406,7 @@ Released 2018-09-13 [`suppress-restriction-lint-in-const`]: https://doc.rust-lang.org/clippy/lint_configuration.html#suppress-restriction-lint-in-const [`too-large-for-stack`]: https://doc.rust-lang.org/clippy/lint_configuration.html#too-large-for-stack [`too-many-arguments-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#too-many-arguments-threshold +[`too-many-lines-in-file-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#too-many-lines-in-file-threshold [`too-many-lines-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#too-many-lines-threshold [`trait-assoc-item-kinds-order`]: https://doc.rust-lang.org/clippy/lint_configuration.html#trait-assoc-item-kinds-order [`trivial-copy-size-limit`]: https://doc.rust-lang.org/clippy/lint_configuration.html#trivial-copy-size-limit diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md index c87f8e9a68de..b20eb1e834e8 100644 --- a/book/src/lint_configuration.md +++ b/book/src/lint_configuration.md @@ -1113,6 +1113,16 @@ The maximum number of argument a function or method can have * [`too_many_arguments`](https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments) +## `too-many-lines-in-file-threshold` +The maximum number of lines a source file can have + +**Default Value:** `1000` + +--- +**Affected lints:** +* [`too_many_lines_in_file`](https://rust-lang.github.io/rust-clippy/master/index.html#too_many_lines_in_file) + + ## `too-many-lines-threshold` The maximum number of lines a function or method can have diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs index 41099f94b044..a986eb77ba0a 100644 --- a/clippy_config/src/conf.rs +++ b/clippy_config/src/conf.rs @@ -882,6 +882,9 @@ define_Conf! { /// The maximum number of argument a function or method can have #[lints(too_many_arguments)] too_many_arguments_threshold: u64 = 7, + /// The maximum number of lines a source file can have + #[lints(too_many_lines_in_file)] + too_many_lines_in_file_threshold: u64 = 1000, /// The maximum number of lines a function or method can have #[lints(too_many_lines)] too_many_lines_threshold: u64 = 100, diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index e16b194c0cad..05eca258f5cc 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -719,6 +719,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::time_subtraction::UNCHECKED_TIME_SUBTRACTION_INFO, crate::to_digit_is_some::TO_DIGIT_IS_SOME_INFO, crate::to_string_trait_impl::TO_STRING_TRAIT_IMPL_INFO, + crate::too_many_lines_in_file::TOO_MANY_LINES_IN_FILE_INFO, crate::toplevel_ref_arg::TOPLEVEL_REF_ARG_INFO, crate::trailing_empty_array::TRAILING_EMPTY_ARRAY_INFO, crate::trait_bounds::TRAIT_DUPLICATION_IN_BOUNDS_INFO, diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 4dca8dfe94d0..d0583f7da057 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -353,6 +353,7 @@ mod tests_outside_test_module; mod time_subtraction; mod to_digit_is_some; mod to_string_trait_impl; +mod too_many_lines_in_file; mod toplevel_ref_arg; mod trailing_empty_array; mod trait_bounds; @@ -517,6 +518,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co Box::new(|| Box::new(byte_char_slices::ByteCharSlice)), Box::new(|| Box::new(cfg_not_test::CfgNotTest)), Box::new(|| Box::new(empty_line_after::EmptyLineAfter::new())), + Box::new(move || Box::new(too_many_lines_in_file::TooManyLinesInFile::new(conf))), // add early passes here, used by `cargo dev new_lint` ]; store.early_passes.extend(early_lints); diff --git a/clippy_lints/src/too_many_lines_in_file.rs b/clippy_lints/src/too_many_lines_in_file.rs new file mode 100644 index 000000000000..15b64c43688a --- /dev/null +++ b/clippy_lints/src/too_many_lines_in_file.rs @@ -0,0 +1,114 @@ +use clippy_config::Conf; +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_session::impl_lint_pass; +use rustc_span::def_id::LOCAL_CRATE; +use rustc_span::{FileName, Span, SyntaxContext}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for source files that have a large number of lines of code. + /// Blank lines and lines containing only comments are not counted. + /// + /// ### Why restrict this? + /// Large files are harder to navigate and understand. They often indicate + /// that a module has too many responsibilities and should be split into + /// smaller, more focused modules. + /// + /// ### Example + /// A file with more lines than the configured threshold will trigger this lint. + /// The fix is to split it into smaller modules. + /// + /// ### Configuration + /// The maximum number of lines is configured with `too-many-lines-in-file-threshold` + /// (default: `1000`). + #[clippy::version = "1.97.0"] + pub TOO_MANY_LINES_IN_FILE, + restriction, + "files with too many lines of code" +} + +impl_lint_pass!(TooManyLinesInFile => [TOO_MANY_LINES_IN_FILE]); + +pub struct TooManyLinesInFile { + threshold: u64, +} + +impl TooManyLinesInFile { + pub fn new(conf: &'static Conf) -> Self { + Self { + threshold: conf.too_many_lines_in_file_threshold, + } + } +} + +impl EarlyLintPass for TooManyLinesInFile { + fn check_crate(&mut self, cx: &EarlyContext<'_>, _: &ast::Crate) { + let source_map = cx.sess().source_map(); + for file in source_map.files().iter() { + if file.cnum != LOCAL_CRATE { + continue; + } + if !matches!(file.name, FileName::Real(_)) { + continue; + } + let Some(src) = file.src.as_deref() else { + continue; + }; + + let mut line_count: u64 = 0; + let mut in_comment = false; + let mut threshold_exceeded_offset: Option = None; + let mut byte_offset: u32 = 0; + + for mut line in src.lines() { + let line_start_offset = byte_offset; + byte_offset = byte_offset + .saturating_add(u32::try_from(line.len()).unwrap_or(u32::MAX)) + .saturating_add(1); // +1 for newline + let mut code_in_line = false; + loop { + line = line.trim_start(); + if line.is_empty() { + break; + } + if in_comment { + if let Some(i) = line.find("*/") { + line = &line[i + 2..]; + in_comment = false; + continue; + } + } else { + let multi_idx = line.find("/*").unwrap_or(line.len()); + let single_idx = line.find("//").unwrap_or(line.len()); + code_in_line |= multi_idx > 0 && single_idx > 0; + if multi_idx < single_idx { + line = &line[multi_idx + 2..]; + in_comment = true; + continue; + } + } + break; + } + if code_in_line { + line_count += 1; + if line_count == self.threshold + 1 { + threshold_exceeded_offset = Some(line_start_offset); + } + } + } + + if line_count > self.threshold { + let start = file.start_pos + rustc_span::BytePos(threshold_exceeded_offset.unwrap_or(0)); + let span = Span::new(start, start, SyntaxContext::root(), None); + span_lint( + cx, + TOO_MANY_LINES_IN_FILE, + span, + format!("this file has too many lines ({line_count}/{})", self.threshold), + ); + } + } + } +} diff --git a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr index 6bb3db8db67f..59474a0fd2d7 100644 --- a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr +++ b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr @@ -83,6 +83,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect third-party too-large-for-stack too-many-arguments-threshold + too-many-lines-in-file-threshold too-many-lines-threshold trait-assoc-item-kinds-order trivial-copy-size-limit @@ -184,6 +185,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect third-party too-large-for-stack too-many-arguments-threshold + too-many-lines-in-file-threshold too-many-lines-threshold trait-assoc-item-kinds-order trivial-copy-size-limit @@ -285,6 +287,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni third-party too-large-for-stack too-many-arguments-threshold + too-many-lines-in-file-threshold too-many-lines-threshold trait-assoc-item-kinds-order trivial-copy-size-limit diff --git a/tests/ui-toml/too_many_lines_in_file/clippy.toml b/tests/ui-toml/too_many_lines_in_file/clippy.toml new file mode 100644 index 000000000000..960afe5cc143 --- /dev/null +++ b/tests/ui-toml/too_many_lines_in_file/clippy.toml @@ -0,0 +1 @@ +too-many-lines-in-file-threshold = 3 diff --git a/tests/ui-toml/too_many_lines_in_file/too_many_lines_in_file.rs b/tests/ui-toml/too_many_lines_in_file/too_many_lines_in_file.rs new file mode 100644 index 000000000000..737db7c95d4b --- /dev/null +++ b/tests/ui-toml/too_many_lines_in_file/too_many_lines_in_file.rs @@ -0,0 +1,9 @@ +#![warn(clippy::too_many_lines_in_file)] + +fn a() {} +fn b() {} +fn c() {} +//~^ too_many_lines_in_file +fn d() {} + +fn main() {} diff --git a/tests/ui-toml/too_many_lines_in_file/too_many_lines_in_file.stderr b/tests/ui-toml/too_many_lines_in_file/too_many_lines_in_file.stderr new file mode 100644 index 000000000000..b567e4e3a52e --- /dev/null +++ b/tests/ui-toml/too_many_lines_in_file/too_many_lines_in_file.stderr @@ -0,0 +1,11 @@ +error: this file has too many lines (6/3) + --> tests/ui-toml/too_many_lines_in_file/too_many_lines_in_file.rs:5:1 + | +LL | fn c() {} + | ^ + | + = note: `-D clippy::too-many-lines-in-file` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::too_many_lines_in_file)]` + +error: aborting due to 1 previous error +