diff --git a/CHANGELOG.md b/CHANGELOG.md index 24b91932567a..b6c2c8db99c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6702,6 +6702,7 @@ Released 2018-09-13 [`err_expect`]: https://rust-lang.github.io/rust-clippy/master/index.html#err_expect [`error_impl_error`]: https://rust-lang.github.io/rust-clippy/master/index.html#error_impl_error [`eval_order_dependence`]: https://rust-lang.github.io/rust-clippy/master/index.html#eval_order_dependence +[`excessive_file_length`]: https://rust-lang.github.io/rust-clippy/master/index.html#excessive_file_length [`excessive_nesting`]: https://rust-lang.github.io/rust-clippy/master/index.html#excessive_nesting [`excessive_precision`]: https://rust-lang.github.io/rust-clippy/master/index.html#excessive_precision [`exhaustive_enums`]: https://rust-lang.github.io/rust-clippy/master/index.html#exhaustive_enums @@ -7485,6 +7486,7 @@ Released 2018-09-13 [`enforced-import-renames`]: https://doc.rust-lang.org/clippy/lint_configuration.html#enforced-import-renames [`enum-variant-name-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#enum-variant-name-threshold [`enum-variant-size-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#enum-variant-size-threshold +[`excessive-file-length-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#excessive-file-length-threshold [`excessive-nesting-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#excessive-nesting-threshold [`future-size-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#future-size-threshold [`ignore-interior-mutability`]: https://doc.rust-lang.org/clippy/lint_configuration.html#ignore-interior-mutability diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md index 64d0bf9b62f6..d98a2b49443f 100644 --- a/book/src/lint_configuration.md +++ b/book/src/lint_configuration.md @@ -682,6 +682,16 @@ The maximum size of an enum's variant to avoid box suggestion * [`large_enum_variant`](https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant) +## `excessive-file-length-threshold` +The maximum number of code lines (excluding comments and blanks) a file can have + +**Default Value:** `500` + +--- +**Affected lints:** +* [`excessive_file_length`](https://rust-lang.github.io/rust-clippy/master/index.html#excessive_file_length) + + ## `excessive-nesting-threshold` The maximum amount of nesting a block can reside in diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs index 465e88a783ed..dd2f09ccc3fe 100644 --- a/clippy_config/src/conf.rs +++ b/clippy_config/src/conf.rs @@ -677,6 +677,9 @@ define_Conf! { /// The maximum size of an enum's variant to avoid box suggestion #[lints(large_enum_variant)] enum_variant_size_threshold: u64 = 200, + /// The maximum number of code lines (excluding comments and blanks) a file can have + #[lints(excessive_file_length)] + excessive_file_length_threshold: u64 = 500, /// The maximum amount of nesting a block can reside in #[lints(excessive_nesting)] excessive_nesting_threshold: u64 = 0, diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 79ed199147f1..dd194f5f15ac 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -156,6 +156,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::eta_reduction::REDUNDANT_CLOSURE_FOR_METHOD_CALLS_INFO, crate::excessive_bools::FN_PARAMS_EXCESSIVE_BOOLS_INFO, crate::excessive_bools::STRUCT_EXCESSIVE_BOOLS_INFO, + crate::excessive_file_length::EXCESSIVE_FILE_LENGTH_INFO, crate::excessive_nesting::EXCESSIVE_NESTING_INFO, crate::exhaustive_items::EXHAUSTIVE_ENUMS_INFO, crate::exhaustive_items::EXHAUSTIVE_STRUCTS_INFO, diff --git a/clippy_lints/src/excessive_file_length.rs b/clippy_lints/src/excessive_file_length.rs new file mode 100644 index 000000000000..602fc43fee8a --- /dev/null +++ b/clippy_lints/src/excessive_file_length.rs @@ -0,0 +1,135 @@ +use clippy_config::Conf; +use clippy_utils::diagnostics::span_lint_hir_and_then; +use clippy_utils::tokenize_with_text; +use rustc_data_structures::fx::FxIndexMap; +use rustc_hir::{HirId, Mod}; +use rustc_lexer::TokenKind; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_session::impl_lint_pass; +use rustc_span::def_id::LOCAL_CRATE; +use rustc_span::{FileName, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for source files that exceed a configurable number of lines of code. + /// + /// ### Why restrict this? + /// Long source files can be difficult to navigate, understand, and maintain. + /// Splitting them into smaller, focused modules encourages better code + /// organization. + /// + /// ### Example + /// An example clippy.toml configuration: + /// ```toml + /// # clippy.toml + /// excessive-file-length-threshold = 500 + /// ``` + /// + /// A file exceeding the threshold should be refactored by extracting + /// logical sections into separate modules. + #[clippy::version = "1.96.0"] + pub EXCESSIVE_FILE_LENGTH, + restriction, + "source file exceeds a configurable line count" +} + +impl_lint_pass!(ExcessiveFileLength => [EXCESSIVE_FILE_LENGTH]); + +struct FileInfo { + hir_id: HirId, + span: Span, + code_lines: u64, +} + +pub struct ExcessiveFileLength { + threshold: u64, + files: FxIndexMap, +} + +impl ExcessiveFileLength { + pub fn new(conf: &'static Conf) -> Self { + Self { + threshold: conf.excessive_file_length_threshold, + files: FxIndexMap::default(), + } + } +} + +/// Counts lines that contain at least one code token, skipping lines that are +/// blank or only contain comments. Uses `rustc_lexer` for correct tokenization. +fn count_code_lines(src: &str) -> u64 { + let mut lines_with_code = vec![false; src.lines().count()]; + let mut current_line = 0; + + for (kind, text, _) in tokenize_with_text(src) { + let is_code = !matches!( + kind, + TokenKind::Whitespace | TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } + ); + + if is_code { + lines_with_code[current_line] = true; + } + + current_line += text.bytes().filter(|&b| b == b'\n').count(); + } + + lines_with_code.iter().filter(|&&has_code| has_code).count() as u64 +} + +impl<'tcx> LateLintPass<'tcx> for ExcessiveFileLength { + fn check_mod(&mut self, cx: &LateContext<'_>, module: &Mod<'_>, hir_id: HirId) { + if self.threshold == 0 { + return; + } + + let span = module.spans.inner_span; + if span.from_expansion() { + return; + } + + let source_map = cx.sess().source_map(); + let file = source_map.lookup_source_file(span.lo()); + if file.cnum != LOCAL_CRATE { + return; + } + + let name = file.name.clone(); + if !matches!(name, FileName::Real(_)) { + return; + } + + if self.files.contains_key(&name) { + return; + } + + let code_lines = file.src.as_ref().map_or(0, |src| count_code_lines(src)); + + self.files.entry(name).or_insert(FileInfo { + hir_id, + span, + code_lines, + }); + } + + fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { + for info in self.files.values() { + if info.code_lines > self.threshold { + let over = info.code_lines - self.threshold; + span_lint_hir_and_then( + cx, + EXCESSIVE_FILE_LENGTH, + info.hir_id, + info.span, + format!( + "file has {} code lines ({over} over the limit of {})", + info.code_lines, self.threshold, + ), + |diag| { + diag.help("consider splitting this file into smaller modules"); + }, + ); + } + } + } +} diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 0875982f3bbf..94acf0f2f851 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -126,6 +126,7 @@ mod error_impl_error; mod escape; mod eta_reduction; mod excessive_bools; +mod excessive_file_length; mod excessive_nesting; mod exhaustive_items; mod exit; @@ -869,6 +870,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co Box::new(move |_| Box::new(manual_noop_waker::ManualNoopWaker::new(conf))), Box::new(|_| Box::new(byte_char_slices::ByteCharSlice)), Box::new(|_| Box::new(manual_assert_eq::ManualAssertEq)), + Box::new(move |_| Box::new(excessive_file_length::ExcessiveFileLength::new(conf))), // add late passes here, used by `cargo dev new_lint` ]; store.late_passes.extend(late_lints); diff --git a/tests/ui-toml/excessive_file_length/allow.rs b/tests/ui-toml/excessive_file_length/allow.rs new file mode 100644 index 000000000000..6a9826212499 --- /dev/null +++ b/tests/ui-toml/excessive_file_length/allow.rs @@ -0,0 +1,13 @@ +//@check-pass +#![allow(clippy::excessive_file_length)] +fn main() {} +fn one() {} +fn two() {} +fn three() {} +fn four() {} +fn five() {} +fn six() {} +fn seven() {} +fn eight() {} +fn nine() {} +fn ten() {} diff --git a/tests/ui-toml/excessive_file_length/below_threshold.rs b/tests/ui-toml/excessive_file_length/below_threshold.rs new file mode 100644 index 000000000000..2ae7a850a481 --- /dev/null +++ b/tests/ui-toml/excessive_file_length/below_threshold.rs @@ -0,0 +1,19 @@ +//@check-pass +// This file has many total lines but only a few code lines. +// Comment lines and blank lines should not be counted. +#![warn(clippy::excessive_file_length)] + +// more comments +// to pad the total line count + +fn main() {} + +fn one() {} + +fn two() {} + +// even more comments here +// and here +// and here + +fn three() {} diff --git a/tests/ui-toml/excessive_file_length/clippy.toml b/tests/ui-toml/excessive_file_length/clippy.toml new file mode 100644 index 000000000000..6f21d8bff01e --- /dev/null +++ b/tests/ui-toml/excessive_file_length/clippy.toml @@ -0,0 +1 @@ +excessive-file-length-threshold = 10 diff --git a/tests/ui-toml/excessive_file_length/excessive_file_length.rs b/tests/ui-toml/excessive_file_length/excessive_file_length.rs new file mode 100644 index 000000000000..6600b36d1dcf --- /dev/null +++ b/tests/ui-toml/excessive_file_length/excessive_file_length.rs @@ -0,0 +1,12 @@ +#![warn(clippy::excessive_file_length)] //~ excessive_file_length +fn main() {} +fn one() {} +fn two() {} +fn three() {} +fn four() {} +fn five() {} +fn six() {} +fn seven() {} +fn eight() {} +fn nine() {} +fn ten() {} diff --git a/tests/ui-toml/excessive_file_length/excessive_file_length.stderr b/tests/ui-toml/excessive_file_length/excessive_file_length.stderr new file mode 100644 index 000000000000..3966a34d88fe --- /dev/null +++ b/tests/ui-toml/excessive_file_length/excessive_file_length.stderr @@ -0,0 +1,18 @@ +error: file has 12 code lines (2 over the limit of 10) + --> tests/ui-toml/excessive_file_length/excessive_file_length.rs:1:1 + | +LL | / #![warn(clippy::excessive_file_length)] +LL | | fn main() {} +LL | | fn one() {} +LL | | fn two() {} +... | +LL | | fn nine() {} +LL | | fn ten() {} + | |___________^ + | + = help: consider splitting this file into smaller modules + = note: `-D clippy::excessive-file-length` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::excessive_file_length)]` + +error: aborting due to 1 previous error + diff --git a/tests/ui-toml/excessive_file_length/expect.rs b/tests/ui-toml/excessive_file_length/expect.rs new file mode 100644 index 000000000000..a47e13c88f25 --- /dev/null +++ b/tests/ui-toml/excessive_file_length/expect.rs @@ -0,0 +1,13 @@ +//@check-pass +#![expect(clippy::excessive_file_length)] +fn main() {} +fn one() {} +fn two() {} +fn three() {} +fn four() {} +fn five() {} +fn six() {} +fn seven() {} +fn eight() {} +fn nine() {} +fn ten() {} 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..14e9516f1d18 100644 --- a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr +++ b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr @@ -49,6 +49,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect enforced-import-renames enum-variant-name-threshold enum-variant-size-threshold + excessive-file-length-threshold excessive-nesting-threshold future-size-threshold ignore-interior-mutability @@ -150,6 +151,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect enforced-import-renames enum-variant-name-threshold enum-variant-size-threshold + excessive-file-length-threshold excessive-nesting-threshold future-size-threshold ignore-interior-mutability @@ -251,6 +253,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni enforced-import-renames enum-variant-name-threshold enum-variant-size-threshold + excessive-file-length-threshold excessive-nesting-threshold future-size-threshold ignore-interior-mutability