Skip to content

Commit fad34b9

Browse files
kedevkedautofix-ci[bot]arendjrsiketyan
authored
feat(biome_js_analyze): add UseConsistentArrowReturn rule (#7245)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Arend van Beelen jr. <[email protected]> Co-authored-by: Naoki Ikeguchi <[email protected]>
1 parent b1f8cbd commit fad34b9

File tree

14 files changed

+522
-20
lines changed

14 files changed

+522
-20
lines changed

.changeset/early-books-jam.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new lint rule `useConsistentArrowReturn`.
6+
7+
This rule enforces a consistent return style for arrow functions.
8+
9+
### Invalid
10+
11+
```js
12+
const f = () => {
13+
return 1;
14+
}
15+
```
16+
17+
This rule is a port of ESLint's [arrow-body-style](https://eslint.org/docs/latest/rules/arrow-body-style) rule.

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

Lines changed: 12 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: 40 additions & 19 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
@@ -183,6 +183,7 @@ define_categories! {
183183
"lint/nursery/noVueReservedProps": "https://biomejs.dev/linter/rules/no-vue-reserved-props",
184184
"lint/nursery/useAnchorHref": "https://biomejs.dev/linter/rules/use-anchor-href",
185185
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
186+
"lint/nursery/useConsistentArrowReturn": "https://biomejs.dev/linter/rules/use-consistent-arrow-return",
186187
"lint/nursery/useConsistentObjectDefinition": "https://biomejs.dev/linter/rules/use-consistent-object-definition",
187188
"lint/nursery/useConsistentTypeDefinitions": "https://biomejs.dev/linter/rules/use-consistent-type-definitions",
188189
"lint/nursery/useExhaustiveSwitchCases": "https://biomejs.dev/linter/rules/use-exhaustive-switch-cases",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub mod no_vue_data_object_declaration;
1919
pub mod no_vue_reserved_keys;
2020
pub mod no_vue_reserved_props;
2121
pub mod use_anchor_href;
22+
pub mod use_consistent_arrow_return;
2223
pub mod use_consistent_type_definitions;
2324
pub mod use_exhaustive_switch_cases;
2425
pub mod use_explicit_type;
@@ -28,4 +29,4 @@ pub mod use_qwik_classlist;
2829
pub mod use_react_function_components;
2930
pub mod use_sorted_classes;
3031
pub mod use_vue_multi_word_component_names;
31-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
32+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use crate::JsRuleAction;
2+
use biome_analyze::{
3+
Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
4+
};
5+
use biome_console::markup;
6+
use biome_diagnostics::Severity;
7+
use biome_js_factory::make;
8+
use biome_js_syntax::{
9+
AnyJsExpression, AnyJsFunctionBody, JsArrowFunctionExpression, JsFunctionBody,
10+
JsReturnStatement,
11+
};
12+
use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};
13+
use biome_rule_options::use_consistent_arrow_return::UseConsistentArrowReturnOptions;
14+
15+
declare_lint_rule! {
16+
/// Enforce consistent arrow function bodies.
17+
///
18+
/// This rule enforces the use of arrow functions with no body block when the function body consists of a single return statement.
19+
/// This rule does not report when:
20+
/// - the function body contains directives (e.g. `"use strict"`), or
21+
/// - the body (or its descendants) contain comments, or
22+
/// - the single `return` has no argument (`return;`).
23+
///
24+
/// The fix wraps expressions in parentheses when required for correctness (e.g. object literals and sequence expressions).
25+
///
26+
/// ## Examples
27+
///
28+
/// ### Invalid
29+
///
30+
///```js,expect_diagnostic
31+
/// const bar = () => {
32+
/// return {
33+
/// bar: {
34+
/// foo: 1,
35+
/// bar: 2,
36+
/// }
37+
/// };
38+
/// };
39+
/// ```
40+
///
41+
/// ### Valid
42+
///
43+
/// ```js
44+
/// const foo = () => 0;
45+
/// const bar = () => { "use strict"; return 1 }
46+
/// const baz = () => { /* intentional */ return x }
47+
/// const qux = () => ({ a: 1 }) // already concise with parens
48+
/// ```
49+
///
50+
pub UseConsistentArrowReturn {
51+
version: "next",
52+
name: "useConsistentArrowReturn",
53+
language: "js",
54+
sources: &[RuleSource::Eslint("arrow-body-style").same()],
55+
recommended: false,
56+
severity: Severity::Warning,
57+
fix_kind: FixKind::Safe,
58+
}
59+
}
60+
61+
impl Rule for UseConsistentArrowReturn {
62+
type Query = Ast<JsArrowFunctionExpression>;
63+
type State = JsFunctionBody;
64+
type Signals = Option<Self::State>;
65+
type Options = UseConsistentArrowReturnOptions;
66+
67+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
68+
let arrow = ctx.query();
69+
let body = JsFunctionBody::cast(arrow.body().ok()?.into_syntax())?;
70+
71+
if !body.directives().is_empty() || body.syntax().has_comments_descendants() {
72+
return None;
73+
}
74+
75+
if body.statements().len() == 1 {
76+
let first_statement = body.statements().iter().next()?;
77+
if let Some(return_statement) = JsReturnStatement::cast(first_statement.into_syntax())
78+
&& return_statement.argument().is_some()
79+
{
80+
return Some(body);
81+
}
82+
}
83+
84+
None
85+
}
86+
87+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
88+
let node = ctx.query();
89+
Some(RuleDiagnostic::new(
90+
rule_category!(),
91+
node.range(),
92+
markup! {
93+
"The body of this arrow function contains a single "<Emphasis>"return"</Emphasis> " statement."
94+
},
95+
))
96+
}
97+
98+
fn action(ctx: &RuleContext<Self>, body: &Self::State) -> Option<JsRuleAction> {
99+
let mut mutation = ctx.root().begin();
100+
101+
let return_statement = body.statements().iter().next()?;
102+
let return_statement = JsReturnStatement::cast(return_statement.into_syntax())?;
103+
let return_argument = return_statement.argument()?;
104+
105+
let new_body = if needs_parens_in_concise_body(&return_argument) {
106+
AnyJsExpression::from(make::parenthesized(return_argument))
107+
} else {
108+
return_argument
109+
};
110+
111+
mutation.replace_node(
112+
AnyJsFunctionBody::from(body.clone()),
113+
AnyJsFunctionBody::AnyJsExpression(new_body),
114+
);
115+
116+
Some(JsRuleAction::new(
117+
ctx.metadata().action_category(ctx.category(), ctx.group()),
118+
ctx.metadata().applicability(),
119+
markup! { "Remove the return statement." }.to_owned(),
120+
mutation,
121+
))
122+
}
123+
}
124+
125+
fn needs_parens_in_concise_body(expr: &AnyJsExpression) -> bool {
126+
use AnyJsExpression::*;
127+
matches!(
128+
expr,
129+
JsObjectExpression(_)
130+
| JsSequenceExpression(_)
131+
| TsAsExpression(_)
132+
| TsSatisfiesExpression(_)
133+
| TsTypeAssertionExpression(_)
134+
)
135+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const returnsObjectArrow = () => {
2+
return { a: 1, b: 2 };
3+
}
4+
5+
const returnsSequenceArrow = () => {
6+
return (a, b);
7+
}
8+
9+
10+
const returnsAwaitArrow = async () => {
11+
return await fetchData();
12+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
assertion_line: 152
4+
expression: invalid.js
5+
---
6+
# Input
7+
```js
8+
const returnsObjectArrow = () => {
9+
return { a: 1, b: 2 };
10+
}
11+
12+
const returnsSequenceArrow = () => {
13+
return (a, b);
14+
}
15+
16+
17+
const returnsAwaitArrow = async () => {
18+
return await fetchData();
19+
}
20+
21+
```
22+
23+
# Diagnostics
24+
```
25+
invalid.js:1:28 lint/nursery/useConsistentArrowReturn FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26+
27+
! The body of this arrow function contains a single return statement.
28+
29+
> 1 │ const returnsObjectArrow = () => {
30+
^^^^^^^
31+
> 2return { a: 1, b: 2 };
32+
> 3}
33+
│ ^
34+
4 │
35+
5 │ const returnsSequenceArrow = () => {
36+
37+
i Safe fix: Remove the return statement.
38+
39+
1- const·returnsObjectArrow·=·()·=>·{
40+
2 │ - ····return·{·a:·1,·b:·2·};
41+
3- }
42+
1 │ + const·returnsObjectArrow·=·()·=>·({·a1b2·})
43+
4 2 │
44+
5 3 │ const returnsSequenceArrow = () => {
45+
46+
47+
```
48+
49+
```
50+
invalid.js:5:30 lint/nursery/useConsistentArrowReturn FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51+
52+
! The body of this arrow function contains a single return statement.
53+
54+
3 │ }
55+
4 │
56+
> 5 │ const returnsSequenceArrow = () => {
57+
^^^^^^^
58+
> 6return (a, b);
59+
> 7}
60+
│ ^
61+
8 │
62+
63+
i Safe fix: Remove the return statement.
64+
65+
3 3 │ }
66+
4 4 │
67+
5 │ - const·returnsSequenceArrow·=·()·=>·{
68+
6- ····return·(ab);
69+
7- }
70+
5 │ + const·returnsSequenceArrow·=·()·=>·(a,·b)
71+
8 6 │
72+
9 7 │
73+
74+
75+
```
76+
77+
```
78+
invalid.js:10:27 lint/nursery/useConsistentArrowReturn FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79+
80+
! The body of this arrow function contains a single return statement.
81+
82+
> 10 │ const returnsAwaitArrow = async () => {
83+
^^^^^^^^^^^^^
84+
> 11return await fetchData();
85+
> 12}
86+
│ ^
87+
13 │
88+
89+
i Safe fix: Remove the return statement.
90+
91+
8 8 │
92+
9 9 │
93+
10 │ - const·returnsAwaitArrow·=·async·()·=>·{
94+
11- ····return·await·fetchData();
95+
12- }
96+
10 │ + const·returnsAwaitArrow·=·async·()·=>·await·fetchData()
97+
13 11 │
98+
99+
100+
```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* should not generate diagnostics */
2+
const a = () => 0;
3+
const b = () => "hello";
4+
const c = (a, b) => a + b;
5+
const d = () => ({ a: 1 });
6+
7+
function multiStatement() {
8+
const x = 1;
9+
return x;
10+
}
11+
12+
const multiStatement2 = () => {
13+
console.log("hello");
14+
};
15+
16+
const multiStatement3 = () => {
17+
let y = 2;
18+
y++;
19+
return y;
20+
}
21+
22+
function noReturn() {
23+
// I do nothing
24+
}
25+
26+
function foo() {
27+
return 0;
28+
}
29+
30+
const noReturnArrow = () => {};
31+
32+
function conditionalReturn(a) {
33+
if (a) {
34+
return 1;
35+
}
36+
return 0;
37+
}
38+
39+
40+
const withComment = () => {
41+
// intentional comment
42+
return 1;
43+
};
44+
45+
46+
const emptyReturn = () => {
47+
return;
48+
};
49+
50+
const withDirective = () => {
51+
"use strict";
52+
return 1;
53+
};
54+
55+
const emptyReturnWithComment = () => {
56+
return; // explicitly empty
57+
};
58+
59+
const withInlineComment = () => { return 1; /* inline */ };
60+
const withBlockCommentBefore = () => { /* leading */ return 1; };
61+
const withCommentBetweenReturnAndExpr = () => { return /* comment */ 1; };
62+
63+
const returnsSequenceArrow = () => (a, b)
64+
const returnsAwaitArrow = async () => await fetchData()

0 commit comments

Comments
 (0)