diff --git a/.changeset/wet-squids-agree.md b/.changeset/wet-squids-agree.md new file mode 100644 index 000000000000..d27fcd9f0062 --- /dev/null +++ b/.changeset/wet-squids-agree.md @@ -0,0 +1,13 @@ +--- +"@biomejs/biome": patch +--- + +Added the nursery lint rule [`noExcessiveLinesPerFile`](https://biomejs.dev/linter/rules/no-excessive-lines-per-file/). +Biome now reports files that exceed a configurable line limit. + +```js +// maxLines: 2 +const a = 1; +const b = 2; +const c = 3; +``` diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 37258753f237..4296416c1071 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -1409,6 +1409,22 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "max-lines" => { + if !options.include_inspired { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); + return false; + } + if !options.include_nursery { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .no_excessive_lines_per_file + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "max-lines-per-function" => { if !options.include_inspired { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 0045d0cbca58..fa6cd00974d5 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -166,6 +166,7 @@ pub enum RuleName { NoEqualsToNull, NoEvolvingTypes, NoExcessiveCognitiveComplexity, + NoExcessiveLinesPerFile, NoExcessiveLinesPerFunction, NoExcessiveNestedTestSuites, NoExplicitAny, @@ -587,6 +588,7 @@ impl RuleName { Self::NoEqualsToNull => "noEqualsToNull", Self::NoEvolvingTypes => "noEvolvingTypes", Self::NoExcessiveCognitiveComplexity => "noExcessiveCognitiveComplexity", + Self::NoExcessiveLinesPerFile => "noExcessiveLinesPerFile", Self::NoExcessiveLinesPerFunction => "noExcessiveLinesPerFunction", Self::NoExcessiveNestedTestSuites => "noExcessiveNestedTestSuites", Self::NoExplicitAny => "noExplicitAny", @@ -1012,6 +1014,7 @@ impl RuleName { Self::NoEqualsToNull => RuleGroup::Nursery, Self::NoEvolvingTypes => RuleGroup::Suspicious, Self::NoExcessiveCognitiveComplexity => RuleGroup::Complexity, + Self::NoExcessiveLinesPerFile => RuleGroup::Nursery, Self::NoExcessiveLinesPerFunction => RuleGroup::Complexity, Self::NoExcessiveNestedTestSuites => RuleGroup::Complexity, Self::NoExplicitAny => RuleGroup::Suspicious, @@ -1438,6 +1441,7 @@ impl std::str::FromStr for RuleName { "noEqualsToNull" => Ok(Self::NoEqualsToNull), "noEvolvingTypes" => Ok(Self::NoEvolvingTypes), "noExcessiveCognitiveComplexity" => Ok(Self::NoExcessiveCognitiveComplexity), + "noExcessiveLinesPerFile" => Ok(Self::NoExcessiveLinesPerFile), "noExcessiveLinesPerFunction" => Ok(Self::NoExcessiveLinesPerFunction), "noExcessiveNestedTestSuites" => Ok(Self::NoExcessiveNestedTestSuites), "noExplicitAny" => Ok(Self::NoExplicitAny), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index e31cb0e680d8..daf743925acb 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -171,6 +171,7 @@ define_categories! { "lint/nursery/noDuplicatedSpreadProps": "https://biomejs.dev/linter/rules/no-duplicated-spread-props", "lint/nursery/noEmptySource": "https://biomejs.dev/linter/rules/no-empty-source", "lint/nursery/noEqualsToNull": "https://biomejs.dev/linter/rules/no-equals-to-null", + "lint/nursery/noExcessiveLinesPerFile": "https://biomejs.dev/linter/rules/no-excessive-lines-per-file", "lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises", "lint/nursery/noForIn": "https://biomejs.dev/linter/rules/no-for-in", "lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 2018734a2aa3..ce852362cb77 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -10,6 +10,7 @@ pub mod no_deprecated_imports; pub mod no_duplicated_spread_props; pub mod no_empty_source; pub mod no_equals_to_null; +pub mod no_excessive_lines_per_file; pub mod no_floating_promises; pub mod no_for_in; pub mod no_import_cycles; @@ -57,4 +58,4 @@ pub mod use_spread; pub mod use_vue_consistent_define_props_declaration; pub mod use_vue_define_macros_order; pub mod use_vue_multi_word_component_names; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } diff --git a/crates/biome_js_analyze/src/lint/nursery/no_excessive_lines_per_file.rs b/crates/biome_js_analyze/src/lint/nursery/no_excessive_lines_per_file.rs new file mode 100644 index 000000000000..02f8fe8e7254 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_excessive_lines_per_file.rs @@ -0,0 +1,173 @@ +use biome_analyze::{ + Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_js_syntax::{AnyJsRoot, JsSyntaxKind}; +use biome_rowan::AstNode; +use biome_rule_options::no_excessive_lines_per_file::NoExcessiveLinesPerFileOptions; + +declare_lint_rule! { + /// Restrict the number of lines in a file. + /// + /// This rule checks the number of lines in a file and reports a diagnostic if it exceeds a specified limit. + /// Some people consider large files a code smell. Large files tend to do many things and can make it hard to follow what's going on. + /// Many coding style guides dictate a limit of the number of lines that a file can comprise of. This rule can help enforce that style. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// The following example will show a diagnostic when `maxLines` is set to 2: + /// + /// ```json,options + /// { + /// "options": { + /// "maxLines": 2 + /// } + /// } + /// ``` + /// ```js,expect_diagnostic,use_options + /// const a = 1; + /// const b = 2; + /// const c = 3; + /// ``` + /// + /// ### Valid + /// + /// ```js + /// const a = 1; + /// const b = 2; + /// ``` + /// + /// ## Options + /// + /// The following options are available: + /// + /// ### `maxLines` + /// + /// This option sets the maximum number of lines allowed in a file. + /// If the file exceeds this limit, a diagnostic will be reported. + /// + /// Default: `300` + /// + /// When `maxLines: 2`, the following file will be considered invalid: + /// ```json,options + /// { + /// "options": { + /// "maxLines": 2 + /// } + /// } + /// ``` + /// ```js,expect_diagnostic,use_options + /// const a = 1; + /// const b = 2; + /// const c = 3; + /// ``` + /// + /// ### `skipBlankLines` + /// + /// When this option is set to `true`, blank lines are not counted towards the maximum line limit. + /// This means that only lines with actual code or comments will be counted. + /// + /// Default: `false` + /// + /// When `maxLines: 3` and `skipBlankLines: true`, the following file will be considered valid + /// even though it has 5 total lines, because only 3 lines contain code: + /// ```json,options + /// { + /// "options": { + /// "maxLines": 3, + /// "skipBlankLines": true + /// } + /// } + /// ``` + /// ```js,use_options + /// const a = 1; + /// + /// const b = 2; + /// + /// const c = 3; + /// ``` + /// + /// ## Suppressions + /// + /// If you need to exceed the line limit in a specific file, you can suppress this rule + /// at the top of the file: + /// + /// ```json,options + /// { + /// "options": { + /// "maxLines": 2 + /// } + /// } + /// ``` + /// ```js,use_options + /// // biome-ignore lint/nursery/noExcessiveLinesPerFile: generated file + /// const a = 1; + /// const b = 2; + /// const c = 3; + /// ``` + /// + pub NoExcessiveLinesPerFile { + version: "next", + name: "noExcessiveLinesPerFile", + language: "js", + recommended: false, + sources: &[RuleSource::Eslint("max-lines").inspired()], + } +} + +impl Rule for NoExcessiveLinesPerFile { + type Query = Ast; + type State = usize; + type Signals = Option; + type Options = NoExcessiveLinesPerFileOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let options = ctx.options(); + + let file_lines_count = node + .syntax() + .descendants() + .flat_map(|descendant| descendant.tokens().collect::>()) + .filter(|token| token.kind() != JsSyntaxKind::EOF) + .fold(0, |acc, token| { + if options.skip_blank_lines() { + return acc + token.has_leading_newline() as usize; + }; + + acc + token + .trim_trailing_trivia() + .leading_trivia() + .pieces() + .filter(|piece| piece.is_newline()) + .count() + }) + + 1; // Add 1 for the first line + + if file_lines_count > options.max_lines().get().into() { + return Some(file_lines_count); + } + + None + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + let node = ctx.query(); + let options = ctx.options(); + + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "This file has too many lines ("{state}"). Maximum allowed is "{options.max_lines().to_string()}"." + }, + ) + .note(markup! { + "Consider splitting this file into smaller files." + }), + ) + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/empty.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/empty.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/empty.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/empty.js.snap new file mode 100644 index 000000000000..7f079324d7a1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/empty.js.snap @@ -0,0 +1,8 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: empty.js +--- +# Input +```js + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/empty.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/empty.options.json new file mode 100644 index 000000000000..273f6e268567 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/empty.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 1 + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.js new file mode 100644 index 000000000000..a91d231bab3f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.js @@ -0,0 +1,4 @@ +const a = 1; +const b = 2; +const c = 3; +const d = 4; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.js.snap new file mode 100644 index 000000000000..90c6731ecea2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.js.snap @@ -0,0 +1,33 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js +const a = 1; +const b = 2; +const c = 3; +const d = 4; + +``` + +# Diagnostics +``` +invalid.js:1:1 lint/nursery/noExcessiveLinesPerFile ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This file has too many lines (4). Maximum allowed is 2. + + > 1 │ const a = 1; + │ ^^^^^^^^^^^^ + > 2 │ const b = 2; + > 3 │ const c = 3; + > 4 │ const d = 4; + │ ^^^^^^^^^^^^ + 5 │ + + i Consider splitting this file into smaller files. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.options.json new file mode 100644 index 000000000000..a6867a7ad8a7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalid.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 2 + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.js new file mode 100644 index 000000000000..fee0c987bc57 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.js @@ -0,0 +1,5 @@ +const a = 1; + +const b = 2; +const c = 3; + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.js.snap new file mode 100644 index 000000000000..78bbdb5ca5a8 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.js.snap @@ -0,0 +1,34 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalidSkipBlankLines.js +--- +# Input +```js +const a = 1; + +const b = 2; +const c = 3; + + +``` + +# Diagnostics +``` +invalidSkipBlankLines.js:1:1 lint/nursery/noExcessiveLinesPerFile ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This file has too many lines (3). Maximum allowed is 2. + + > 1 │ const a = 1; + │ ^^^^^^^^^^^^ + > 2 │ + > 3 │ const b = 2; + > 4 │ const c = 3; + │ ^^^^^^^^^^^^ + 5 │ + + i Consider splitting this file into smaller files. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.options.json new file mode 100644 index 000000000000..35dae41f85fb --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/invalidSkipBlankLines.options.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 2, + "skipBlankLines": true + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.js new file mode 100644 index 000000000000..b8f1493e1f45 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.js @@ -0,0 +1 @@ +const a = 1; const b = 2; const c = 3; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.js.snap new file mode 100644 index 000000000000..464483d31a50 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.js.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: multipleStatementsSingleLine.js +--- +# Input +```js +const a = 1; const b = 2; const c = 3; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.options.json new file mode 100644 index 000000000000..273f6e268567 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/multipleStatementsSingleLine.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 1 + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.js new file mode 100644 index 000000000000..54b82a09ad54 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.js @@ -0,0 +1 @@ +const a = 1; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.js.snap new file mode 100644 index 000000000000..9241ed2f50aa --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.js.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: singleLine.js +--- +# Input +```js +const a = 1; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.options.json new file mode 100644 index 000000000000..273f6e268567 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/singleLine.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 1 + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.js new file mode 100644 index 000000000000..94eee17916ac --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.js @@ -0,0 +1,4 @@ +// biome-ignore lint/nursery/noExcessiveLinesPerFile: generated file +const a = 1; +const b = 2; +const c = 3; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.js.snap new file mode 100644 index 000000000000..a1e11ba35617 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.js.snap @@ -0,0 +1,12 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: suppressed.js +--- +# Input +```js +// biome-ignore lint/nursery/noExcessiveLinesPerFile: generated file +const a = 1; +const b = 2; +const c = 3; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.options.json new file mode 100644 index 000000000000..a6867a7ad8a7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/suppressed.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 2 + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.js new file mode 100644 index 000000000000..fc7b0446eafd --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.js @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ +const a = 1; +const b = 2; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.js.snap new file mode 100644 index 000000000000..6cec41726151 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.js.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +/* should not generate diagnostics */ +const a = 1; +const b = 2; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.options.json new file mode 100644 index 000000000000..ad36c3b88f33 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/valid.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 4 + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.js new file mode 100644 index 000000000000..9ce636de454d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.js @@ -0,0 +1,5 @@ +/* should not generate diagnostics */ +const a = 1; + +const b = 2; + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.js.snap new file mode 100644 index 000000000000..ce1baa5f581c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.js.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: validSkipBlankLines.js +--- +# Input +```js +/* should not generate diagnostics */ +const a = 1; + +const b = 2; + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.options.json new file mode 100644 index 000000000000..58a164663428 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/validSkipBlankLines.options.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 4, + "skipBlankLines": true + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.js b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.js new file mode 100644 index 000000000000..139597f9cb07 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.js @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.js.snap new file mode 100644 index 000000000000..f597a07b6872 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.js.snap @@ -0,0 +1,10 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: whitespaceOnly.js +--- +# Input +```js + + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.options.json b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.options.json new file mode 100644 index 000000000000..273f6e268567 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFile/whitespaceOnly.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "noExcessiveLinesPerFile": { + "level": "error", + "options": { + "maxLines": 1 + } + } + } + } + } +} diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 6ccfa620aa6d..bd7143fd96de 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -79,6 +79,7 @@ pub mod no_enum; pub mod no_equals_to_null; pub mod no_evolving_types; pub mod no_excessive_cognitive_complexity; +pub mod no_excessive_lines_per_file; pub mod no_excessive_lines_per_function; pub mod no_excessive_nested_test_suites; pub mod no_explicit_any; diff --git a/crates/biome_rule_options/src/no_excessive_lines_per_file.rs b/crates/biome_rule_options/src/no_excessive_lines_per_file.rs new file mode 100644 index 000000000000..e16849eb177a --- /dev/null +++ b/crates/biome_rule_options/src/no_excessive_lines_per_file.rs @@ -0,0 +1,32 @@ +use biome_deserialize_macros::{Deserializable, Merge}; +use serde::{Deserialize, Serialize}; +use std::num::NonZeroU16; +#[derive(Clone, Debug, Default, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct NoExcessiveLinesPerFileOptions { + /// The maximum number of lines allowed in a file. + #[serde(skip_serializing_if = "Option::<_>::is_none")] + pub max_lines: Option, + /// When this option is set to `true`, blank lines are not counted towards the maximum line limit. + #[serde(skip_serializing_if = "Option::<_>::is_none")] + pub skip_blank_lines: Option, +} + +impl NoExcessiveLinesPerFileOptions { + pub const DEFAULT_MAX_LINES: NonZeroU16 = NonZeroU16::new(300).unwrap(); + pub const DEFAULT_SKIP_BLANK_LINES: bool = false; + + /// Returns [`Self::max_lines`] if it is set. + /// Otherwise, returns [`Self::DEFAULT_MAX_LINES`]. + pub fn max_lines(&self) -> NonZeroU16 { + self.max_lines.unwrap_or(Self::DEFAULT_MAX_LINES) + } + + /// Returns [`Self::skip_blank_lines`] if it is set. + /// Otherwise, returns [`Self::DEFAULT_SKIP_BLANK_LINES`]. + pub fn skip_blank_lines(&self) -> bool { + self.skip_blank_lines + .unwrap_or(Self::DEFAULT_SKIP_BLANK_LINES) + } +} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 157f95568e24..f006794031f3 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1899,6 +1899,11 @@ See */ noEqualsToNull?: NoEqualsToNullConfiguration; /** + * Restrict the number of lines in a file. +See + */ + noExcessiveLinesPerFile?: NoExcessiveLinesPerFileConfiguration; + /** * Require Promise-like statements to be handled appropriately. See */ @@ -3720,6 +3725,9 @@ export type NoEmptySourceConfiguration = export type NoEqualsToNullConfiguration = | RulePlainConfiguration | RuleWithNoEqualsToNullOptions; +export type NoExcessiveLinesPerFileConfiguration = + | RulePlainConfiguration + | RuleWithNoExcessiveLinesPerFileOptions; export type NoFloatingPromisesConfiguration = | RulePlainConfiguration | RuleWithNoFloatingPromisesOptions; @@ -5202,6 +5210,10 @@ export interface RuleWithNoEqualsToNullOptions { level: RulePlainConfiguration; options?: NoEqualsToNullOptions; } +export interface RuleWithNoExcessiveLinesPerFileOptions { + level: RulePlainConfiguration; + options?: NoExcessiveLinesPerFileOptions; +} export interface RuleWithNoFloatingPromisesOptions { fix?: FixKind; level: RulePlainConfiguration; @@ -6631,6 +6643,16 @@ export interface NoEmptySourceOptions { allowComments?: boolean; } export type NoEqualsToNullOptions = {}; +export interface NoExcessiveLinesPerFileOptions { + /** + * The maximum number of lines allowed in a file. + */ + maxLines?: number; + /** + * When this option is set to `true`, blank lines are not counted towards the maximum line limit. + */ + skipBlankLines?: boolean; +} export type NoFloatingPromisesOptions = {}; export type NoForInOptions = {}; export interface NoImportCyclesOptions { @@ -7546,6 +7568,7 @@ export type Category = | "lint/nursery/noDuplicatedSpreadProps" | "lint/nursery/noEmptySource" | "lint/nursery/noEqualsToNull" + | "lint/nursery/noExcessiveLinesPerFile" | "lint/nursery/noFloatingPromises" | "lint/nursery/noForIn" | "lint/nursery/noImplicitCoercion" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index b674d893ed14..67283257fca6 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -3238,6 +3238,29 @@ }, "additionalProperties": false }, + "NoExcessiveLinesPerFileConfiguration": { + "oneOf": [ + { "$ref": "#/$defs/RulePlainConfiguration" }, + { "$ref": "#/$defs/RuleWithNoExcessiveLinesPerFileOptions" } + ] + }, + "NoExcessiveLinesPerFileOptions": { + "type": "object", + "properties": { + "maxLines": { + "description": "The maximum number of lines allowed in a file.", + "type": ["integer", "null"], + "format": "uint16", + "maximum": 65535, + "minimum": 1 + }, + "skipBlankLines": { + "description": "When this option is set to `true`, blank lines are not counted towards the maximum line limit.", + "type": ["boolean", "null"] + } + }, + "additionalProperties": false + }, "NoExcessiveLinesPerFunctionConfiguration": { "oneOf": [ { "$ref": "#/$defs/RulePlainConfiguration" }, @@ -5252,6 +5275,13 @@ { "type": "null" } ] }, + "noExcessiveLinesPerFile": { + "description": "Restrict the number of lines in a file.\nSee ", + "anyOf": [ + { "$ref": "#/$defs/NoExcessiveLinesPerFileConfiguration" }, + { "type": "null" } + ] + }, "noFloatingPromises": { "description": "Require Promise-like statements to be handled appropriately.\nSee ", "anyOf": [ @@ -7013,6 +7043,15 @@ "additionalProperties": false, "required": ["level"] }, + "RuleWithNoExcessiveLinesPerFileOptions": { + "type": "object", + "properties": { + "level": { "$ref": "#/$defs/RulePlainConfiguration" }, + "options": { "$ref": "#/$defs/NoExcessiveLinesPerFileOptions" } + }, + "additionalProperties": false, + "required": ["level"] + }, "RuleWithNoExcessiveLinesPerFunctionOptions": { "type": "object", "properties": {