Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/wet-squids-agree.md
Original file line number Diff line number Diff line change
@@ -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;
```
16 changes: 16 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ,] } }
Original file line number Diff line number Diff line change
@@ -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<AnyJsRoot>;
type State = usize;
type Signals = Option<Self::State>;
type Options = NoExcessiveLinesPerFileOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let options = ctx.options();

let file_lines_count = node
.syntax()
.descendants()
.flat_map(|descendant| descendant.tokens().collect::<Vec<_>>())
.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<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
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."
}),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: empty.js
---
# Input
```js

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"rules": {
"nursery": {
"noExcessiveLinesPerFile": {
"level": "error",
"options": {
"maxLines": 1
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const a = 1;
const b = 2;
const c = 3;
const d = 4;
Original file line number Diff line number Diff line change
@@ -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.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"rules": {
"nursery": {
"noExcessiveLinesPerFile": {
"level": "error",
"options": {
"maxLines": 2
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const a = 1;

const b = 2;
const c = 3;

Original file line number Diff line number Diff line change
@@ -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.


```
Loading
Loading