diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index ce6ae89c1a..9e3f717a0a 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -259,6 +259,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index a1236947bd..a51ff57957 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -261,6 +261,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 5868ceb04f..67166fff4a 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -263,6 +263,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 726abc733e..aae6f95360 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -263,6 +263,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index db8a289004..3c6903a8b9 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -260,6 +260,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index 193aac85f3..e8b3562b53 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -260,6 +260,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index e7914052c3..6fb0184c70 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -259,6 +259,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index 76a57bae28..e4ea7e54d1 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -259,6 +259,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index ecdc9bfc62..aa37d2d8e3 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -259,6 +259,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index 94d6cbc603..4ab93f1903 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -372,6 +372,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pylint.max_nested_blocks = 5 +linter.pylint.max_module_lines = 1000 linter.pyupgrade.keep_runtime_typing = false linter.ruff.parenthesize_tuple_in_subscript = false diff --git a/crates/ruff_linter/src/checkers/physical_lines.rs b/crates/ruff_linter/src/checkers/physical_lines.rs index 5cc8f7a908..73ca5fdb6a 100644 --- a/crates/ruff_linter/src/checkers/physical_lines.rs +++ b/crates/ruff_linter/src/checkers/physical_lines.rs @@ -78,6 +78,10 @@ pub(crate) fn check_physical_lines( if enforce_copyright_notice { missing_copyright_notice(locator, settings, context); } + + if context.is_rule_enabled(Rule::TooManyLines) { + pylint::rules::too_many_lines(locator, settings, context); + } } #[cfg(test)] diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index ebec5f4acc..9c9c6c82a0 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -221,6 +221,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "C0206") => rules::pylint::rules::DictIndexMissingItems, (Pylint, "C0207") => rules::pylint::rules::MissingMaxsplitArg, (Pylint, "C0208") => rules::pylint::rules::IterationOverSet, + (Pylint, "C0302") => rules::pylint::rules::TooManyLines, (Pylint, "C0414") => rules::pylint::rules::UselessImportAlias, (Pylint, "C0415") => rules::pylint::rules::ImportOutsideTopLevel, (Pylint, "C1802") => rules::pylint::rules::LenTest, diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 9351cb5de2..d097027f7e 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -259,6 +259,7 @@ impl Rule { | Rule::MissingCopyrightNotice | Rule::MissingNewlineAtEndOfFile | Rule::MixedSpacesAndTabs + | Rule::TooManyLines | Rule::TrailingWhitespace => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::BlanketTypeIgnore diff --git a/crates/ruff_linter/src/rules/flake8_lineleak/settings.rs b/crates/ruff_linter/src/rules/flake8_lineleak/settings.rs new file mode 100644 index 0000000000..d706be161a --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_lineleak/settings.rs @@ -0,0 +1,33 @@ +//! Settings for the `flake8-lineleak` plugin. + +use std::fmt::{Display, Formatter}; + +use ruff_macros::CacheKey; + +use crate::display_settings; + +#[derive(Debug, Clone, CacheKey)] +pub struct Settings { + pub max_line_count: usize, +} + +impl Default for Settings { + fn default() -> Self { + Self { + max_line_count: 100, + } + } +} + +impl Display for Settings { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + display_settings! { + formatter = f, + namespace = "linter.flake8_lineleak", + fields = [ + self.max_line_count, + ] + } + Ok(()) + } +} diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index de341b1146..0af530921d 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -470,4 +470,90 @@ mod tests { assert_diagnostics!(diagnostics); Ok(()) } + + #[test] + fn too_many_lines_below_limit() { + use crate::test::test_snippet; + + let diagnostics = test_snippet( + r" +# Line 1 +# Line 2 +# Line 3 +import os +" + .trim(), + &LinterSettings { + preview: PreviewMode::Enabled, + pylint: pylint::settings::Settings { + max_module_lines: 1000, + ..pylint::settings::Settings::default() + }, + ..LinterSettings::for_rules(vec![Rule::TooManyLines]) + }, + ); + assert_diagnostics!(diagnostics); + } + + #[test] + fn too_many_lines_exceeds_limit() { + use crate::test::test_snippet; + + let diagnostics = test_snippet( + r" +# Line 1 +# Line 2 +# Line 3 +# Line 4 +# Line 5 +# Line 6 +# Line 7 +# Line 8 +# Line 9 +# Line 10 +# Line 11 +import os +" + .trim(), + &LinterSettings { + preview: PreviewMode::Enabled, + pylint: pylint::settings::Settings { + max_module_lines: 10, + ..pylint::settings::Settings::default() + }, + ..LinterSettings::for_rules(vec![Rule::TooManyLines]) + }, + ); + assert!(!diagnostics.is_empty()); + } + + #[test] + fn too_many_lines_at_limit() { + use crate::test::test_snippet; + + let diagnostics = test_snippet( + r" +# Line 1 +# Line 2 +# Line 3 +# Line 4 +# Line 5 +# Line 6 +# Line 7 +# Line 8 +# Line 9 +# Line 10 +" + .trim(), + &LinterSettings { + preview: PreviewMode::Enabled, + pylint: pylint::settings::Settings { + max_module_lines: 10, + ..pylint::settings::Settings::default() + }, + ..LinterSettings::for_rules(vec![Rule::TooManyLines]) + }, + ); + assert_diagnostics!(diagnostics); + } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index 2853bb84c9..bc6794dec4 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -83,6 +83,7 @@ pub(crate) use sys_exit_alias::*; pub(crate) use too_many_arguments::*; pub(crate) use too_many_boolean_expressions::*; pub(crate) use too_many_branches::*; +pub(crate) use too_many_lines::*; pub(crate) use too_many_locals::*; pub(crate) use too_many_nested_blocks::*; pub(crate) use too_many_positional_arguments::*; @@ -194,6 +195,7 @@ mod sys_exit_alias; mod too_many_arguments; mod too_many_boolean_expressions; mod too_many_branches; +mod too_many_lines; mod too_many_locals; mod too_many_nested_blocks; mod too_many_positional_arguments; diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_lines.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_lines.rs new file mode 100644 index 0000000000..fb8cc22594 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_lines.rs @@ -0,0 +1,58 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_text_size::TextRange; + +use crate::Locator; +use crate::Violation; +use crate::checkers::ast::LintContext; +use crate::settings::LinterSettings; + +/// ## What it does +/// Checks for modules with too many lines. +/// +/// By default, this rule allows up to 1000 lines, as configured by the +/// [`lint.pylint.max-module-lines`] option. +/// +/// ## Why is this bad? +/// Modules with many lines are generally harder to read and understand. +/// Extracting functionality into separate modules can improve code organization +/// and maintainability. +/// +/// ## Example +/// A module with 1500 lines when `max-module-lines` is set to 1000 will trigger +/// this rule. +/// +/// ## Options +/// - `lint.pylint.max-module-lines` +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "v0.14.9")] +pub(crate) struct TooManyLines { + actual_lines: usize, + max_lines: usize, +} + +impl Violation for TooManyLines { + #[derive_message_formats] + fn message(&self) -> String { + let TooManyLines { + actual_lines, + max_lines, + } = self; + format!("Too many lines in module ({actual_lines}/{max_lines})") + } +} + +/// C0302 +pub(crate) fn too_many_lines(locator: &Locator, settings: &LinterSettings, context: &LintContext) { + let actual_lines = locator.contents().lines().count(); + let max_lines = settings.pylint.max_module_lines; + + if actual_lines > max_lines { + context.report_diagnostic( + TooManyLines { + actual_lines, + max_lines, + }, + TextRange::default(), + ); + } +} diff --git a/crates/ruff_linter/src/rules/pylint/settings.rs b/crates/ruff_linter/src/rules/pylint/settings.rs index c3abc7e83a..90200a237b 100644 --- a/crates/ruff_linter/src/rules/pylint/settings.rs +++ b/crates/ruff_linter/src/rules/pylint/settings.rs @@ -61,6 +61,7 @@ pub struct Settings { pub max_public_methods: usize, pub max_locals: usize, pub max_nested_blocks: usize, + pub max_module_lines: usize, } impl Default for Settings { @@ -77,6 +78,7 @@ impl Default for Settings { max_public_methods: 20, max_locals: 15, max_nested_blocks: 5, + max_module_lines: 1000, } } } @@ -97,7 +99,8 @@ impl fmt::Display for Settings { self.max_statements, self.max_public_methods, self.max_locals, - self.max_nested_blocks + self.max_nested_blocks, + self.max_module_lines ] } Ok(()) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_lines_at_limit.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_lines_at_limit.snap new file mode 100644 index 0000000000..6c123427ab --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_lines_at_limit.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_lines_below_limit.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_lines_below_limit.snap new file mode 100644 index 0000000000..6c123427ab --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_lines_below_limit.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- + diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 472b0e66f4..2ec4541d97 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3343,6 +3343,14 @@ pub struct PylintOptions { example = r"max-nested-blocks = 10" )] pub max_nested_blocks: Option, + + /// Maximum number of lines allowed in a module (see `PLC0302`). + #[option( + default = r"1000", + value_type = "int", + example = r"max-module-lines = 1500" + )] + pub max_module_lines: Option, } impl PylintOptions { @@ -3367,6 +3375,7 @@ impl PylintOptions { .unwrap_or(defaults.max_public_methods), max_locals: self.max_locals.unwrap_or(defaults.max_locals), max_nested_blocks: self.max_nested_blocks.unwrap_or(defaults.max_nested_blocks), + max_module_lines: self.max_module_lines.unwrap_or(defaults.max_module_lines), } } } diff --git a/ruff.schema.json b/ruff.schema.json index 1c8a092042..1e201b8593 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2794,6 +2794,15 @@ "format": "uint", "minimum": 0 }, + "max-module-lines": { + "description": "Maximum number of lines allowed in a module (see `PLC0302`).", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0 + }, "max-nested-blocks": { "description": "Maximum number of nested blocks allowed within a function or method body\n(see `PLR1702`).", "type": [ @@ -3590,6 +3599,9 @@ "PLC0206", "PLC0207", "PLC0208", + "PLC03", + "PLC030", + "PLC0302", "PLC04", "PLC041", "PLC0414",