Skip to content
255 changes: 156 additions & 99 deletions crates/oxc_linter/src/rules/eslint/arrow_body_style.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
use serde_json::Value;

use oxc_allocator::Box as OxcBox;
use oxc_ast::{
AstKind,
ast::{ArrowFunctionExpression, FunctionBody, ReturnStatement},
ast::{Expression, Statement},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use serde_json::Value;

use crate::{AstNode, context::LintContext, rule::Rule};

fn arrow_body_style_diagnostic(span: Span, msg: &str) -> OxcDiagnostic {
OxcDiagnostic::warn(msg.to_string()).with_label(span)
}

fn diagnostic_expected_block(ctx: &LintContext, span: Span) {
ctx.diagnostic(arrow_body_style_diagnostic(
span,
"Expected block statement surrounding arrow body.",
));
}

#[derive(Debug, Default, PartialEq, Clone)]
enum Mode {
#[default]
Expand All @@ -29,18 +39,6 @@ impl Mode {
_ => Self::AsNeeded,
}
}

pub fn is_always(&self) -> bool {
matches!(self, Self::Always)
}

pub fn is_never(&self) -> bool {
matches!(self, Self::Never)
}

pub fn is_as_needed(&self) -> bool {
matches!(self, Self::AsNeeded)
}
}

#[derive(Debug, Default, Clone)]
Expand All @@ -53,89 +51,132 @@ declare_oxc_lint!(
/// ### What it does
///
/// This rule can enforce or disallow the use of braces around arrow function body.
/// Arrow functions can use either:
/// - a block body `() => { ... }`
/// - or a concise body `() => expression` with an implicit return.
///
/// ### Why is this bad?
///
/// Arrow functions have two syntactic forms for their function bodies.
/// They may be defined with a block body (denoted by curly braces) () => { ... }
/// or with a single expression () => ..., whose value is implicitly returned.
/// Inconsistent use of block vs. concise bodies makes code harder to read.
/// Concise bodies are limited to a single expression, whose value is implicitly returned.
///
/// ### Options
///
/// First option:
/// - Type: `string`
/// - Enum: `"always"`, `"as-needed"`, `"never"`
/// - Default: `"never"`
///
/// Possible values:
/// * `never` enforces no braces where they can be omitted (default)
/// * `always` enforces braces around the function body
/// * `as-needed` enforces no braces around the function body (constrains arrow functions to the role of returning an expression)
///
/// Second option:
/// - Type: `object`
/// - Properties:
/// - `requireReturnForObjectLiteral`: `boolean` (default: `false`) - requires braces and an explicit return for object literals.
///
/// Note: This option only applies when the first option is `"as-needed"`.
///
/// Example configuration:
/// ```json
/// {
/// "arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": true }]
/// }
/// ```
///
/// ### Examples
///
/// #### `"never"` (default)
///
/// Examples of **incorrect** code for this rule with the `never` option:
/// ```js
/// /* arrow-body-style: ["error", "never"] */
///
/// /* ✘ Bad: */
/// const foo = () => {
/// return 0;
/// };
/// ```
///
/// Examples of **correct** code for this rule with the `never` option:
/// ```js
/// /* arrow-body-style: ["error", "never"] */
///
/// /* ✔ Good: */
/// const foo = () => 0;
/// const bar = () => ({ foo: 0 });
/// ```
///
/// #### `"always"`
///
/// Examples of **incorrect** code for this rule with the `always` option:
/// ```js
/// /* arrow-body-style: ["error", "always"] */
///
/// /* ✘ Bad: */
/// const foo = () => 0;
/// ```
///
/// Examples of **correct** code for this rule with the `always` option:
/// ```js
/// /* arrow-body-style: ["error", "always"] */
///
/// /* ✔ Good: */
/// const foo = () => {
/// return 0;
/// };
/// ```
///
/// #### `"as-needed"`
///
/// Examples of **incorrect** code for this rule with the `as-needed` option:
/// ```js
/// /* arrow-body-style: ["error", "as-needed"] */
///
/// /* ✘ Bad: */
/// const foo = () => {
/// return 0;
/// };
/// ```
///
/// Examples of **correct** code for this rule with the `as-needed` option:
/// ```js
/// /* arrow-body-style: ["error", "as-needed"] */
///
/// /* ✔ Good: */
/// const foo1 = () => 0;
///
/// const foo2 = (retv, name) => {
/// retv[name] = true;
/// return retv;
/// };
///
/// const foo3 = () => {
/// bar();
/// };
/// ```
///
/// Examples of **incorrect** code for this rule with the { "requireReturnForObjectLiteral": true } option:
/// #### `"as-needed"` with `requireReturnForObjectLiteral`
///
/// Examples of **incorrect** code for this rule with the `{ "requireReturnForObjectLiteral": true }` option:
/// ```js
/// /* arrow-body-style: ["error", "as-needed", { "requireReturnForObjectLiteral": true }]*/
///
/// /* ✘ Bad: */
/// const foo = () => ({});
/// const bar = () => ({ bar: 0 });
/// ```
///
/// Examples of **correct** code for this rule with the { "requireReturnForObjectLiteral": true } option:
/// Examples of **correct** code for this rule with the `{ "requireReturnForObjectLiteral": true }` option:
/// ```js
/// /* arrow-body-style: ["error", "as-needed", { "requireReturnForObjectLiteral": true }]*/
///
/// /* ✔ Good: */
/// const foo = () => {};
/// const bar = () => { return { bar: 0 }; };
/// ```
///
/// Examples of **incorrect** code for this rule with the `never` option:
/// ```js
/// const foo = () => {
/// return 0;
/// };
/// ```
///
/// Examples of **correct** code for this rule with the `never` option:
/// ```js
/// const foo = () => 0;
/// const bar = () => ({ foo: 0 });
/// ```
///
/// ### Options
///
/// The rule takes one or two options. The first is a string, which can be:
///
/// * `always` enforces braces around the function body
/// * `never` enforces no braces where they can be omitted (default)
/// * `as-needed` enforces no braces around the function body (constrains arrow functions to the role of returning an expression)
///
/// The second one is an object for more fine-grained configuration
/// when the first option is "as-needed". Currently,
/// the only available option is requireReturnForObjectLiteral, a boolean property.
/// It’s false by default. If set to true, it requires braces and an explicit return for object literals.
///
/// ```json
/// {
/// "arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": true }]
/// }
/// ```
ArrowBodyStyle,
eslint,
style,
Expand All @@ -144,69 +185,85 @@ declare_oxc_lint!(

impl Rule for ArrowBodyStyle {
fn from_configuration(value: Value) -> Self {
let obj1 = value.get(0);
let obj2 = value.get(1);
let mode = value.get(0).and_then(Value::as_str).map(Mode::from).unwrap_or_default();

Self {
mode: obj1.and_then(Value::as_str).map(Mode::from).unwrap_or_default(),
require_return_for_object_literal: obj2
.and_then(|v| v.get("requireReturnForObjectLiteral"))
.and_then(Value::as_bool)
.unwrap_or(false),
}
let require_return_for_object_literal = value
.get(1)
.and_then(|v| v.get("requireReturnForObjectLiteral"))
.and_then(Value::as_bool)
.unwrap_or(false);

Self { mode, require_return_for_object_literal }
}
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {

fn run(&self, node: &AstNode, ctx: &LintContext) {
let AstKind::ArrowFunctionExpression(arrow_func_expr) = node.kind() else {
return;
};
let body = &arrow_func_expr.body;
let statements = &body.statements;

if arrow_func_expr.expression {
if self.mode.is_always() {
ctx.diagnostic(arrow_body_style_diagnostic(
body.span,
"Expected block statement surrounding arrow body.",
));
}
if self.mode.is_as_needed() && self.require_return_for_object_literal {
if let Some(Expression::ObjectExpression(_)) =
arrow_func_expr.get_expression().map(Expression::get_inner_expression)
{
ctx.diagnostic(arrow_body_style_diagnostic(
body.span,
"Expected block statement surrounding arrow body.",
));
}
}
self.run_for_arrow_expression(arrow_func_expr, ctx);
} else {
if self.mode.is_never() {
let msg = if statements.is_empty() {
self.run_for_arrow_block(&arrow_func_expr.body, ctx);
}
}
}

impl ArrowBodyStyle {
fn run_for_arrow_expression(
&self,
arrow_func_expr: &ArrowFunctionExpression,
ctx: &LintContext,
) {
let body = &arrow_func_expr.body;

match (
&self.mode,
&self.require_return_for_object_literal,
arrow_func_expr.get_expression().map(Expression::get_inner_expression),
) {
(Mode::Always, _, _) => diagnostic_expected_block(ctx, body.span),
(Mode::AsNeeded, true, Some(Expression::ObjectExpression(_))) => {
diagnostic_expected_block(ctx, body.span);
}
_ => {}
}
}

fn run_for_arrow_block_return_statement(
&self,
return_statement: &OxcBox<ReturnStatement>,
body: &FunctionBody,
ctx: &LintContext,
) {
if self.require_return_for_object_literal
&& matches!(return_statement.argument, Some(Expression::ObjectExpression(_)))
{
return;
}

ctx.diagnostic(arrow_body_style_diagnostic(
body.span,
"Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.",
));
}

fn run_for_arrow_block(&self, body: &FunctionBody, ctx: &LintContext) {
match self.mode {
Mode::Never => {
let msg = if body.statements.is_empty() {
"Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`."
} else {
"Unexpected block statement surrounding arrow body."
};
ctx.diagnostic(arrow_body_style_diagnostic(body.span, msg));
}
if self.mode.is_as_needed() {
// check is there only one `ReturnStatement`
if statements.len() != 1 {
return;
}
let inner_statement = &statements[0];

if let Statement::ReturnStatement(return_statement) = inner_statement {
let return_val = &return_statement.argument;
if self.require_return_for_object_literal
&& return_val
.as_ref()
.is_some_and(|v| matches!(v, Expression::ObjectExpression(_)))
{
return;
}
ctx.diagnostic(arrow_body_style_diagnostic(body.span, "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`."));
Mode::AsNeeded if body.statements.len() == 1 => {
if let Statement::ReturnStatement(return_statement) = &body.statements[0] {
self.run_for_arrow_block_return_statement(return_statement, body, ctx);
}
}
_ => {}
}
}
}
Expand All @@ -222,7 +279,7 @@ fn test() {
("var foo = () => { /* do nothing */ };", None),
(
"var foo = () => {
/* do nothing */
/* do nothing */
};",
None,
),
Expand Down Expand Up @@ -260,7 +317,7 @@ fn test() {
),
(
"var foo = () => {
/* do nothing */
/* do nothing */
};",
Some(serde_json::json!(["as-needed", { "requireReturnForObjectLiteral": true }])),
),
Expand Down
Loading