Skip to content

Commit bf3902f

Browse files
committed
feat(biome_js_analyze): implement noExcessiveLinesPerFunction
1 parent ff72658 commit bf3902f

27 files changed

+983
-78
lines changed

.changeset/mighty-regions-begin.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@biomejs/biome": minor
3+
---
4+
5+
Added the rule [noExcessiveLinesPerFunction](https://biomejs.dev/linter/rules/no-excessive-lines-per-function/).
6+
This rule restrict a maximum number of lines of code in a function.
7+
8+
The following code is now reported as invalid when the limit of maximum lines is set to 4:
9+
10+
```js
11+
function foo() {
12+
const x = 0;
13+
const y = 1;
14+
const z = 2;
15+
}
16+
```
17+
18+
The following code is now reported as valid when the limit of maximum lines is set to 4:
19+
20+
```jsx
21+
const bar = () => {
22+
const x = 0;
23+
const z = 2;
24+
};
25+
```

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 102 additions & 77 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ define_categories! {
154154
"lint/nursery/noDestructuredProps": "https://biomejs.dev/linter/rules/no-destructured-props",
155155
"lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback",
156156
"lint/nursery/noDuplicateAtImportRules": "https://biomejs.dev/linter/rules/no-duplicate-at-import-rules",
157+
"lint/nursery/noExcessiveLinesPerFunction": "https://biomejs.dev/linter/rules/no-excessive-lines-per-function",
157158
"lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises",
158159
"lint/nursery/noGlobalDirnameFilename": "https://biomejs.dev/linter/rules/no-global-dirname-filename",
159160
"lint/nursery/noImportCycles": "https://biomejs.dev/linter/rules/no-import-cycles",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod no_await_in_loop;
77
pub mod no_bitwise_operators;
88
pub mod no_constant_binary_expression;
99
pub mod no_destructured_props;
10+
pub mod no_excessive_lines_per_function;
1011
pub mod no_floating_promises;
1112
pub mod no_global_dirname_filename;
1213
pub mod no_import_cycles;
@@ -39,4 +40,4 @@ pub mod use_single_js_doc_asterisk;
3940
pub mod use_sorted_classes;
4041
pub mod use_symbol_description;
4142
pub mod use_unique_element_ids;
42-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_import_cycles :: NoImportCycles , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_index_of :: UseIndexOf , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
43+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_excessive_lines_per_function :: NoExcessiveLinesPerFunction , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_import_cycles :: NoImportCycles , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_index_of :: UseIndexOf , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use crate::services::semantic::Semantic;
2+
use ::serde::{Deserialize, Serialize};
3+
use biome_analyze::{
4+
Rule, RuleDiagnostic, RuleSource, RuleSourceKind, context::RuleContext, declare_lint_rule,
5+
};
6+
use biome_console::markup;
7+
use biome_deserialize_macros::Deserializable;
8+
use biome_js_syntax::{
9+
AnyFunctionLike, AnyJsFunction, JsCallExpression, JsParenthesizedExpression,
10+
};
11+
use biome_rowan::AstNode;
12+
#[cfg(feature = "schemars")]
13+
use schemars::JsonSchema;
14+
use std::num::NonZeroU8;
15+
16+
declare_lint_rule! {
17+
/// Restrict a maximum number of lines of code in a function.
18+
///
19+
/// Some people consider large functions a code smell. Large functions tend to do a lot of things and can make it hard following what’s going on. Many coding style guides dictate a limit of the number of lines that a function can comprise of. This rule can help enforce that style.
20+
///
21+
/// ## Options
22+
///
23+
/// The rule supports the following options:
24+
///
25+
/// ```json,options
26+
/// {
27+
/// "options": {
28+
/// "max": 4,
29+
/// "skipBlankLines": true,
30+
/// "iifes": true
31+
/// }
32+
/// }
33+
/// ```
34+
///
35+
/// - `max` (positive integer, default: 50): The maximum number of lines allowed in a function.
36+
/// - `skip_blank_lines` (bool, default: false): A boolean value which indicates whether blank lines are counted or not.
37+
/// - `iifes` (bool, default: false): A boolean value which indicates whether IIFEs (Immediately Invoked Function Expression) are checked or not.
38+
///
39+
/// ## Examples
40+
///
41+
/// ### Invalid
42+
///
43+
/// ```js,expect_diagnostic,use_options
44+
/// function foo () {
45+
/// const x = 0;
46+
/// const y = 1;
47+
/// const z = 2;
48+
/// };
49+
/// ```
50+
///
51+
/// ```js,expect_diagnostic,use_options
52+
/// const bar = () => {
53+
/// const x = 0;
54+
///
55+
/// const y = 1;
56+
/// const z = 2;
57+
/// };
58+
/// ```
59+
///
60+
/// ```js,expect_diagnostic,use_options
61+
/// class Baz {
62+
/// foo() {
63+
/// const x = 0;
64+
/// const y = 0;
65+
/// const z = 2;
66+
/// };
67+
/// };
68+
/// ```
69+
///
70+
/// ```js,expect_diagnostic,use_options
71+
/// (() => {
72+
/// const x = 0;
73+
/// const y = 0;
74+
/// const z = 0;
75+
/// })();
76+
/// ```
77+
///
78+
/// ### Valid
79+
///
80+
/// ```js,use_options
81+
/// function foo () {
82+
/// const x = 0;
83+
/// const y = 1;
84+
/// };
85+
///
86+
/// const bar = () => {
87+
/// const x = 0;
88+
///
89+
/// const y = 1;
90+
/// };
91+
///
92+
/// class Baz {
93+
/// foo() {
94+
/// const x = 0;
95+
/// const y = 0;
96+
/// };
97+
/// };
98+
///
99+
/// (() => {
100+
/// const x = 0;
101+
/// const y = 0;
102+
/// })();
103+
/// ```
104+
///
105+
pub NoExcessiveLinesPerFunction {
106+
version: "2.0.0",
107+
name: "noExcessiveLinesPerFunction",
108+
language: "js",
109+
recommended: false,
110+
sources: &[RuleSource::Eslint("max-lines-per-function")],
111+
source_kind: RuleSourceKind::SameLogic,
112+
}
113+
}
114+
115+
impl Rule for NoExcessiveLinesPerFunction {
116+
type Query = Semantic<AnyFunctionLike>;
117+
type State = State;
118+
type Signals = Option<Self::State>;
119+
type Options = NoExcessiveLinesPerFunctionOptions;
120+
121+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
122+
let binding = ctx.query();
123+
let options = ctx.options();
124+
125+
if let AnyFunctionLike::AnyJsFunction(func) = binding {
126+
if is_iife(func) && !options.iifes {
127+
return None;
128+
}
129+
};
130+
131+
let func_string = binding.to_string();
132+
let func_lines = func_string.trim().lines();
133+
134+
let function_line_count = if options.skip_blank_lines {
135+
func_lines
136+
.filter(|line| !line.trim().is_empty())
137+
.collect::<Vec<_>>()
138+
.len()
139+
} else {
140+
func_lines.collect::<Vec<_>>().len()
141+
};
142+
143+
if function_line_count <= options.max.get().into() {
144+
return None;
145+
}
146+
147+
Some(State {
148+
function_line_count,
149+
})
150+
}
151+
152+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
153+
let node = ctx.query();
154+
let options = ctx.options();
155+
156+
Some(
157+
RuleDiagnostic::new(
158+
rule_category!(),
159+
node.range(),
160+
markup! {
161+
"This function has too many lines ("{state.function_line_count}"). Maximum allowed is "{options.max.to_string()}"."
162+
},
163+
)
164+
.note(markup! {
165+
"Consider refactoring this function to split it into smaller functions."
166+
}),
167+
)
168+
}
169+
}
170+
171+
fn is_iife(func: &AnyJsFunction) -> bool {
172+
func.parent::<JsParenthesizedExpression>()
173+
.and_then(|expr| expr.parent::<JsCallExpression>())
174+
.is_some()
175+
}
176+
177+
pub struct State {
178+
function_line_count: usize,
179+
}
180+
181+
#[derive(Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)]
182+
#[cfg_attr(feature = "schema", derive(JsonSchema))]
183+
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
184+
pub struct NoExcessiveLinesPerFunctionOptions {
185+
pub max: NonZeroU8,
186+
pub skip_blank_lines: bool,
187+
pub iifes: bool,
188+
}
189+
190+
impl Default for NoExcessiveLinesPerFunctionOptions {
191+
fn default() -> Self {
192+
Self {
193+
max: NonZeroU8::new(50).unwrap(),
194+
skip_blank_lines: false,
195+
iifes: false,
196+
}
197+
}
198+
}

crates/biome_js_analyze/src/options.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const foo = () => {
2+
const x = 2;
3+
const y = 1;
4+
return x + y;
5+
};
6+
7+
function bar() {
8+
const x = 2;
9+
const y = 1;
10+
return x + y;
11+
}
12+
13+
function name() {
14+
var x = 5;
15+
16+
var x = 2;
17+
}
18+
19+
function foo(
20+
aaa = 1,
21+
bbb = 2,
22+
ccc = 3
23+
) {
24+
return aaa + bbb + ccc
25+
}
26+
27+
function parent() {
28+
var x = 0;
29+
function nested() {
30+
var y = 0;
31+
x = 2;
32+
}
33+
};
34+
35+
class foo {
36+
method() {
37+
let y = 10;
38+
let x = 20;
39+
return y + x;
40+
}
41+
}

0 commit comments

Comments
 (0)