Skip to content

Commit 741eacf

Browse files
committed
fix(biome_js_analyze): change the way to calculate the number of lines in a function
1 parent 7558d0c commit 741eacf

19 files changed

+226
-122
lines changed

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

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/src/lint/nursery/no_excessive_lines_per_function.rs

Lines changed: 125 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,22 @@ use schemars::JsonSchema;
1414
use std::num::NonZeroU8;
1515

1616
declare_lint_rule! {
17-
/// Restrict a maximum number of lines of code in a function.
17+
/// Restrict the number of lines of code in a function.
1818
///
19+
/// This rule checks the number of lines in a function body and reports a diagnostic if it exceeds a specified limit.
1920
/// 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.
2021
///
21-
/// ## Options
22-
///
23-
/// The rule supports the following options:
22+
/// ## Examples
2423
///
24+
/// When maximum number of lines is set to `2` with the following option, the following code will be considered invalid:
2525
/// ```json,options
2626
/// {
2727
/// "options": {
28-
/// "max": 4,
29-
/// "skipBlankLines": true,
30-
/// "iifes": true
28+
/// "maxLines": 2
3129
/// }
3230
/// }
3331
/// ```
3432
///
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-
///
4133
/// ### Invalid
4234
///
4335
/// ```js,expect_diagnostic,use_options
@@ -51,7 +43,6 @@ declare_lint_rule! {
5143
/// ```js,expect_diagnostic,use_options
5244
/// const bar = () => {
5345
/// const x = 0;
54-
///
5546
/// const y = 1;
5647
/// const z = 2;
5748
/// };
@@ -85,7 +76,6 @@ declare_lint_rule! {
8576
///
8677
/// const bar = () => {
8778
/// const x = 0;
88-
///
8979
/// const y = 1;
9080
/// };
9181
///
@@ -102,6 +92,89 @@ declare_lint_rule! {
10292
/// })();
10393
/// ```
10494
///
95+
/// ## Options
96+
///
97+
/// The rule supports the following options:
98+
///
99+
/// ```json
100+
/// {
101+
/// "options": {
102+
/// "maxLines": 50,
103+
/// "skipBlankLines": false,
104+
/// "skipIifes": false
105+
/// }
106+
/// }
107+
/// ```
108+
///
109+
/// ### maxLines
110+
///
111+
/// This option sets the maximum number of lines allowed in a function body.
112+
/// If the function body exceeds this limit, a diagnostic will be reported.
113+
///
114+
/// Default: `50`
115+
///
116+
/// When `maxLines: 2`, the following function will be considered invalid:
117+
/// ```json,options
118+
/// {
119+
/// "options": {
120+
/// "maxLines": 2
121+
/// }
122+
/// }
123+
/// ```
124+
/// ```js,expect_diagnostic,use_options
125+
/// function example() {
126+
/// const a = 1; // 1
127+
/// const b = 2; // 2
128+
/// const c = 3; // 3
129+
/// };
130+
/// ```
131+
///
132+
/// ### skipBlankLines
133+
/// When this options is set to `true`, blank lines in the function body are not counted towards the maximum line limit.
134+
/// This means that only lines with actual code or comments will be counted.
135+
///
136+
/// Default: `false`
137+
///
138+
/// When `maxLines: 2` and `skipBlankLines: true`, the following function will be considered valid:
139+
/// ```json,options
140+
/// {
141+
/// "options": {
142+
/// "maxLines": 2,
143+
/// "skipBlankLines": true
144+
/// }
145+
/// }
146+
/// ```
147+
/// ```js,use_options
148+
/// function example() {
149+
/// const a = 1; // 1
150+
/// // not counted
151+
/// const b = 2; // 2
152+
/// // not counted
153+
/// };
154+
/// ```
155+
///
156+
/// ### skipIifes
157+
/// When this option is set to `true`, Immediately Invoked Function Expressions (IIFEs) are not checked for the maximum line limit.
158+
///
159+
/// Default: `false`
160+
///
161+
/// When `maxLines: 2` and `skipIifes: true`, the following IIFE will be considered valid even though its body has 3 lines:
162+
/// ```json,options
163+
/// {
164+
/// "options": {
165+
/// "maxLines": 2,
166+
/// "skipIifes": true
167+
/// }
168+
/// }
169+
/// ```
170+
/// ```js,use_options
171+
/// (() => {
172+
/// const a = 1; // 1
173+
/// const b = 2; // 2
174+
/// const c = 3; // 3
175+
/// })();
176+
/// ```
177+
///
105178
pub NoExcessiveLinesPerFunction {
106179
version: "2.0.0",
107180
name: "noExcessiveLinesPerFunction",
@@ -123,30 +196,45 @@ impl Rule for NoExcessiveLinesPerFunction {
123196
let options = ctx.options();
124197

125198
if let AnyFunctionLike::AnyJsFunction(func) = binding {
126-
if is_iife(func) && !options.iifes {
199+
if is_iife(func) && options.skip_iifes {
127200
return None;
128201
}
129202
};
130203

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()
204+
let Ok(func_body) = binding.body() else {
205+
return None;
141206
};
142207

143-
if function_line_count <= options.max.get().into() {
144-
return None;
208+
let function_lines_count = func_body
209+
.syntax()
210+
.descendants()
211+
.flat_map(|descendant| descendant.tokens().collect::<Vec<_>>())
212+
.filter(|token| {
213+
!matches!(
214+
token.kind(),
215+
biome_js_syntax::JsSyntaxKind::L_CURLY | biome_js_syntax::JsSyntaxKind::R_CURLY
216+
)
217+
})
218+
.fold(0, |acc, token| {
219+
if options.skip_blank_lines {
220+
return acc + token.has_leading_newline() as usize;
221+
};
222+
223+
acc + token
224+
.trim_trailing_trivia()
225+
.leading_trivia()
226+
.pieces()
227+
.filter(|piece| piece.is_newline())
228+
.count()
229+
});
230+
231+
if function_lines_count > options.max_lines.get().into() {
232+
return Some(State {
233+
function_lines_count,
234+
});
145235
}
146236

147-
Some(State {
148-
function_line_count,
149-
})
237+
None
150238
}
151239

152240
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
@@ -158,7 +246,7 @@ impl Rule for NoExcessiveLinesPerFunction {
158246
rule_category!(),
159247
node.range(),
160248
markup! {
161-
"This function has too many lines ("{state.function_line_count}"). Maximum allowed is "{options.max.to_string()}"."
249+
"This function has too many lines ("{state.function_lines_count}"). Maximum allowed is "{options.max_lines.to_string()}"."
162250
},
163251
)
164252
.note(markup! {
@@ -175,24 +263,24 @@ fn is_iife(func: &AnyJsFunction) -> bool {
175263
}
176264

177265
pub struct State {
178-
function_line_count: usize,
266+
function_lines_count: usize,
179267
}
180268

181269
#[derive(Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)]
182270
#[cfg_attr(feature = "schema", derive(JsonSchema))]
183271
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
184272
pub struct NoExcessiveLinesPerFunctionOptions {
185-
pub max: NonZeroU8,
273+
pub max_lines: NonZeroU8,
186274
pub skip_blank_lines: bool,
187-
pub iifes: bool,
275+
pub skip_iifes: bool,
188276
}
189277

190278
impl Default for NoExcessiveLinesPerFunctionOptions {
191279
fn default() -> Self {
192280
Self {
193-
max: NonZeroU8::new(50).unwrap(),
281+
max_lines: NonZeroU8::new(50).unwrap(),
194282
skip_blank_lines: false,
195-
iifes: false,
283+
skip_iifes: false,
196284
}
197285
}
198286
}

crates/biome_js_analyze/tests/specs/nursery/noExcessiveLinesPerFunction/invalid.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@ function foo(
2121
bbb = 2,
2222
ccc = 3
2323
) {
24-
return aaa + bbb + ccc
24+
const x = 4;
25+
const y = 5;
26+
return aaa + bbb + ccc + x + y;
2527
}
2628

2729
function parent() {
2830
var x = 0;
2931
function nested() {
3032
var y = 0;
3133
x = 2;
34+
var z = x + y;
3235
}
3336
};
3437

0 commit comments

Comments
 (0)