Skip to content

Commit b8cbd83

Browse files
mehm8128ematipicoarendjr
authored
feat(biome_js_analyze): implement noExcessiveLinesPerFunction (#6166)
Co-authored-by: Emanuele Stoppa <[email protected]> Co-authored-by: Arend van Beelen jr. <[email protected]>
1 parent 72623fa commit b8cbd83

25 files changed

Lines changed: 1146 additions & 90 deletions

File tree

.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": patch
3+
---
4+
5+
Added the nursery 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 body.
7+
8+
The following code is now reported as invalid when the limit of maximum lines is set to 2:
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 3:
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: 16 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: 109 additions & 84 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;
@@ -42,4 +43,4 @@ pub mod use_single_js_doc_asterisk;
4243
pub mod use_sorted_classes;
4344
pub mod use_symbol_description;
4445
pub mod use_unique_element_ids;
45-
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_react_prop_assign :: NoReactPropAssign , 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_json_import_attribute :: UseJsonImportAttribute , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_readonly_class_properties :: UseReadonlyClassProperties , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
46+
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_react_prop_assign :: NoReactPropAssign , 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_json_import_attribute :: UseJsonImportAttribute , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_readonly_class_properties :: UseReadonlyClassProperties , 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: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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 the number of lines of code in a function.
18+
///
19+
/// This rule checks the number of lines in a function body and reports a diagnostic if it exceeds a specified limit. Remember that this rule only counts the lines of code in the function body, not the entire function declaration.
20+
/// 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.
21+
///
22+
/// ## Examples
23+
///
24+
/// ### Invalid
25+
///
26+
/// The following example will show diagnostic when you set the maxLines limit to 3, however the default value is 50.
27+
///
28+
/// ```js
29+
/// function foo () {
30+
/// const x = 0;
31+
/// const y = 1;
32+
/// const z = 2;
33+
/// return x + y + z;
34+
/// };
35+
/// ```
36+
///
37+
/// ### Valid
38+
///
39+
/// ```js
40+
/// function foo () {
41+
/// const x = 0;
42+
/// const y = 1;
43+
/// };
44+
/// ```
45+
///
46+
/// ## Options
47+
///
48+
/// The rule supports the following options:
49+
///
50+
/// ```json
51+
/// {
52+
/// "options": {
53+
/// "maxLines": 50,
54+
/// "skipBlankLines": false,
55+
/// "skipIifes": false
56+
/// }
57+
/// }
58+
/// ```
59+
///
60+
/// ### maxLines
61+
///
62+
/// This option sets the maximum number of lines allowed in a function body.
63+
/// If the function body exceeds this limit, a diagnostic will be reported.
64+
///
65+
/// Default: `50`
66+
///
67+
/// When `maxLines: 2`, the following function will be considered invalid:
68+
/// ```json,options
69+
/// {
70+
/// "options": {
71+
/// "maxLines": 2
72+
/// }
73+
/// }
74+
/// ```
75+
/// ```js,expect_diagnostic,use_options
76+
/// function example() {
77+
/// const a = 1; // 1
78+
/// const b = 2; // 2
79+
/// const c = 3; // 3
80+
/// };
81+
/// ```
82+
///
83+
/// ### skipBlankLines
84+
///
85+
/// When this options is set to `true`, blank lines in the function body are not counted towards the maximum line limit.
86+
/// This means that only lines with actual code or comments will be counted.
87+
///
88+
/// Default: `false`
89+
///
90+
/// When `maxLines: 2` and `skipBlankLines: true`, the following function will be considered valid:
91+
/// ```json,options
92+
/// {
93+
/// "options": {
94+
/// "maxLines": 2,
95+
/// "skipBlankLines": true
96+
/// }
97+
/// }
98+
/// ```
99+
/// ```js,use_options
100+
/// function example() {
101+
/// const a = 1; // 1
102+
/// // not counted
103+
/// const b = 2; // 2
104+
/// // not counted
105+
/// };
106+
/// ```
107+
///
108+
/// ### skipIifes
109+
///
110+
/// When this option is set to `true`, Immediately Invoked Function Expressions (IIFEs) are not checked for the maximum line limit.
111+
///
112+
/// Default: `false`
113+
///
114+
/// When `maxLines: 2` and `skipIifes: true`, the following IIFE will be considered valid even though its body has 3 lines:
115+
/// ```json,options
116+
/// {
117+
/// "options": {
118+
/// "maxLines": 2,
119+
/// "skipIifes": true
120+
/// }
121+
/// }
122+
/// ```
123+
/// ```js,use_options
124+
/// (() => {
125+
/// const a = 1; // 1
126+
/// const b = 2; // 2
127+
/// const c = 3; // 3
128+
/// })();
129+
/// ```
130+
///
131+
pub NoExcessiveLinesPerFunction {
132+
version: "2.0.0",
133+
name: "noExcessiveLinesPerFunction",
134+
language: "js",
135+
recommended: false,
136+
sources: &[RuleSource::Eslint("max-lines-per-function")],
137+
source_kind: RuleSourceKind::Inspired,
138+
}
139+
}
140+
141+
impl Rule for NoExcessiveLinesPerFunction {
142+
type Query = Semantic<AnyFunctionLike>;
143+
type State = State;
144+
type Signals = Option<Self::State>;
145+
type Options = NoExcessiveLinesPerFunctionOptions;
146+
147+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
148+
let binding = ctx.query();
149+
let options = ctx.options();
150+
151+
if let AnyFunctionLike::AnyJsFunction(func) = binding {
152+
if is_iife(func) && options.skip_iifes {
153+
return None;
154+
}
155+
};
156+
157+
let Ok(func_body) = binding.body() else {
158+
return None;
159+
};
160+
161+
let function_lines_count = func_body
162+
.syntax()
163+
.descendants()
164+
.flat_map(|descendant| descendant.tokens().collect::<Vec<_>>())
165+
.filter(|token| {
166+
!matches!(
167+
token.kind(),
168+
biome_js_syntax::JsSyntaxKind::L_CURLY | biome_js_syntax::JsSyntaxKind::R_CURLY
169+
)
170+
})
171+
.fold(0, |acc, token| {
172+
if options.skip_blank_lines {
173+
return acc + token.has_leading_newline() as usize;
174+
};
175+
176+
acc + token
177+
.trim_trailing_trivia()
178+
.leading_trivia()
179+
.pieces()
180+
.filter(|piece| piece.is_newline())
181+
.count()
182+
});
183+
184+
if function_lines_count > options.max_lines.get().into() {
185+
return Some(State {
186+
function_lines_count,
187+
});
188+
}
189+
190+
None
191+
}
192+
193+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
194+
let node = ctx.query();
195+
let options = ctx.options();
196+
197+
Some(
198+
RuleDiagnostic::new(
199+
rule_category!(),
200+
node.range(),
201+
markup! {
202+
"This function has too many lines ("{state.function_lines_count}"). Maximum allowed is "{options.max_lines.to_string()}"."
203+
},
204+
)
205+
.note(markup! {
206+
"Consider refactoring this function to split it into smaller functions."
207+
}),
208+
)
209+
}
210+
}
211+
212+
fn is_iife(func: &AnyJsFunction) -> bool {
213+
func.parent::<JsParenthesizedExpression>()
214+
.and_then(|expr| expr.parent::<JsCallExpression>())
215+
.is_some()
216+
}
217+
218+
pub struct State {
219+
function_lines_count: usize,
220+
}
221+
222+
#[derive(Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)]
223+
#[cfg_attr(feature = "schema", derive(JsonSchema))]
224+
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
225+
pub struct NoExcessiveLinesPerFunctionOptions {
226+
/// The maximum number of lines allowed in a function body.
227+
pub max_lines: NonZeroU8,
228+
/// When this options is set to `true`, blank lines in the function body are not counted towards the maximum line limit.
229+
pub skip_blank_lines: bool,
230+
/// When this option is set to `true`, Immediately Invoked Function Expressions (IIFEs) are not checked for the maximum line limit.
231+
pub skip_iifes: bool,
232+
}
233+
234+
impl Default for NoExcessiveLinesPerFunctionOptions {
235+
fn default() -> Self {
236+
Self {
237+
max_lines: NonZeroU8::new(50).unwrap(),
238+
skip_blank_lines: false,
239+
skip_iifes: false,
240+
}
241+
}
242+
}

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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
const x = 4;
25+
const y = 5;
26+
return aaa + bbb + ccc + x + y;
27+
}
28+
29+
function parent() {
30+
var x = 0;
31+
function nested() {
32+
var y = 0;
33+
x = 2;
34+
var z = x + y;
35+
}
36+
};
37+
38+
class foo {
39+
method() {
40+
let y = 10;
41+
let x = 20;
42+
return y + x;
43+
}
44+
constructor() {
45+
let y = 10;
46+
let x = 20;
47+
return y + x;
48+
}
49+
}
50+
51+
(function () {
52+
let x = 0;
53+
let y = 0;
54+
let z = x + y;
55+
let foo = {};
56+
return bar;
57+
})();
58+
59+
(() => {
60+
let x = 0;
61+
let y = 0;
62+
let z = x + y;
63+
let foo = {};
64+
return bar;
65+
})();

0 commit comments

Comments
 (0)