mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(linter/arrow-body-style): keep type assertions when adding a block body (#27978)
Closes #27977 With `arrow-body-style: ["error", "always"]`, the fix unwrapped the concise body with `get_inner_expression`, which strips TS wrappers together with the parentheses, so the type assertion was lost. Now only the parentheses around the leading object literal are removed (the same tokens ESLint removes) and the rest of the expression is kept. | Input | Before | After | | --- | --- | --- | | `() => <T>{ id }` | `() => {return { id }}` | `() => {return <T>{ id }}` | | `() => <T>({ id })` | `() => {return { id }}` | `() => {return <T>({ id })}` | | `() => ({ id }) as T` | `() => {return { id }}` | `() => {return { id } as T}` | | `() => ({ id }) satisfies T` | `() => {return { id }}` | `() => {return { id } satisfies T}` | Plain `() => ({ id })` still becomes `() => {return { id }}`. Tests: added a `.ts` test (`test_typescript`) with pass/fail/fix cases (also `({ id })!`, `(({ id }) as T)` and `requireReturnForObjectLiteral`) and its snapshot. `cargo test --all-features -p oxc_linter` and `just lint -p oxc_linter` pass. AI disclosure: this PR was prepared with the help of an AI assistant (Claude).
This commit is contained in:
1 parent
853fca45b1
commit
0298062de2
2 files changed
+161
-11
No files matched your search
@@ -4,7 +4,9 @@ use serde_json::Value;
|
||||
|
||||
use oxc_ast::{
|
||||
AstKind,
|
||||
ast::{ArrowFunctionExpression, Expression, ReturnStatement, Statement},
|
||||
ast::{
|
||||
ArrowFunctionExpression, Expression, ParenthesizedExpression, ReturnStatement, Statement,
|
||||
},
|
||||
};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
@@ -318,21 +320,37 @@ impl ArrowBodyStyle {
|
||||
return fixer.noop();
|
||||
};
|
||||
|
||||
let expr_text = ctx.source_range(expr.span());
|
||||
|
||||
// Check if the expression is a parenthesized object literal: `() => ({ ... })`
|
||||
// In that case, we need to remove the outer parens when converting to block:
|
||||
// `() => ({ ... })` → `() => { return { ... } }`
|
||||
let inner_expr = expr.get_inner_expression();
|
||||
if matches!(inner_expr, Expression::ObjectExpression(_)) {
|
||||
let inner_text = ctx.source_range(inner_expr.span());
|
||||
return fixer.replace(body.span(), format!("{{return {inner_text}}}"));
|
||||
// Only the parens around the object literal can be dropped; a type assertion wrapping it
|
||||
// must be kept: `() => ({ ... }) as T` → `() => { return { ... } as T }`
|
||||
if let Some(paren) = Self::leading_parenthesized_object(expr) {
|
||||
let object_text = ctx.source_range(paren.expression.without_parentheses().span());
|
||||
let rest_text = ctx.source_range(Span::new(paren.span.end, expr.span().end));
|
||||
return fixer.replace(body.span(), format!("{{return {object_text}{rest_text}}}"));
|
||||
}
|
||||
|
||||
// For all other expressions, just wrap in `{ return ... }`
|
||||
let expr_text = ctx.source_range(expr.span());
|
||||
fixer.replace(body.span(), format!("{{return {expr_text}}}"))
|
||||
}
|
||||
|
||||
/// Find the parenthesized object literal a concise body starts with, looking through
|
||||
/// postfix TS wrappers: `({ ... })`, `({ ... }) as T`, `({ ... }) satisfies T`, `({ ... })!`
|
||||
fn leading_parenthesized_object<'b, 'a>(
|
||||
expr: &'b Expression<'a>,
|
||||
) -> Option<&'b ParenthesizedExpression<'a>> {
|
||||
match expr {
|
||||
Expression::ParenthesizedExpression(paren) => {
|
||||
matches!(paren.expression.without_parentheses(), Expression::ObjectExpression(_))
|
||||
.then_some(&**paren)
|
||||
}
|
||||
Expression::TSAsExpression(e) => Self::leading_parenthesized_object(&e.expression),
|
||||
Expression::TSSatisfiesExpression(e) => {
|
||||
Self::leading_parenthesized_object(&e.expression)
|
||||
}
|
||||
Expression::TSNonNullExpression(e) => Self::leading_parenthesized_object(&e.expression),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fix: Convert block body to concise body
|
||||
/// `() => { return expr }` → `() => expr`
|
||||
fn fix_block_to_concise<'a>(
|
||||
@@ -1104,3 +1122,76 @@ var foo = () =>
|
||||
.expect_fix(fix)
|
||||
.test_and_snapshot();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_typescript() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
("var foo = () => { return <Foo>{ id }; };", Some(serde_json::json!(["always"]))),
|
||||
("var foo = () => { return { id } as Foo; };", Some(serde_json::json!(["always"]))),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
("var foo = () => <Foo>{ id };", Some(serde_json::json!(["always"]))),
|
||||
("var foo = () => <Foo>({ id });", Some(serde_json::json!(["always"]))),
|
||||
("var foo = () => ({ id }) as Foo;", Some(serde_json::json!(["always"]))),
|
||||
("var foo = () => ({ id }) satisfies Foo;", Some(serde_json::json!(["always"]))),
|
||||
("var foo = () => ({ id })!;", Some(serde_json::json!(["always"]))),
|
||||
("var foo = () => (({ id }) as Foo);", Some(serde_json::json!(["always"]))),
|
||||
("var foo = () => <Foo>JSON.parse(id);", Some(serde_json::json!(["always"]))),
|
||||
(
|
||||
"var foo = () => ({ id }) as Foo;",
|
||||
Some(serde_json::json!(["as-needed", { "requireReturnForObjectLiteral": true }])),
|
||||
),
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
(
|
||||
"var foo = () => <Foo>{ id };",
|
||||
"var foo = () => {return <Foo>{ id }};",
|
||||
Some(serde_json::json!(["always"])),
|
||||
),
|
||||
(
|
||||
"var foo = () => <Foo>({ id });",
|
||||
"var foo = () => {return <Foo>({ id })};",
|
||||
Some(serde_json::json!(["always"])),
|
||||
),
|
||||
(
|
||||
"var foo = () => ({ id }) as Foo;",
|
||||
"var foo = () => {return { id } as Foo};",
|
||||
Some(serde_json::json!(["always"])),
|
||||
),
|
||||
(
|
||||
"var foo = () => ({ id }) satisfies Foo;",
|
||||
"var foo = () => {return { id } satisfies Foo};",
|
||||
Some(serde_json::json!(["always"])),
|
||||
),
|
||||
(
|
||||
"var foo = () => ({ id })!;",
|
||||
"var foo = () => {return { id }!};",
|
||||
Some(serde_json::json!(["always"])),
|
||||
),
|
||||
(
|
||||
"var foo = () => (({ id }) as Foo);",
|
||||
"var foo = () => {return (({ id }) as Foo)};",
|
||||
Some(serde_json::json!(["always"])),
|
||||
),
|
||||
(
|
||||
"var foo = () => <Foo>JSON.parse(id);",
|
||||
"var foo = () => {return <Foo>JSON.parse(id)};",
|
||||
Some(serde_json::json!(["always"])),
|
||||
),
|
||||
(
|
||||
"var foo = () => ({ id }) as Foo;",
|
||||
"var foo = () => {return { id } as Foo};",
|
||||
Some(serde_json::json!(["as-needed", { "requireReturnForObjectLiteral": true }])),
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(ArrowBodyStyle::NAME, ArrowBodyStyle::PLUGIN, pass, fail)
|
||||
.change_rule_path_extension("ts")
|
||||
.with_snapshot_suffix("ts")
|
||||
.expect_fix(fix)
|
||||
.test_and_snapshot();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
|
||||
⚠ eslint(arrow-body-style): Expected block statement surrounding arrow body.
|
||||
╭─[arrow_body_style.ts:1:17]
|
||||
1 │ var foo = () => <Foo>{ id };
|
||||
· ───────────
|
||||
╰────
|
||||
help: Surround the arrow body with braces and use a return statement.
|
||||
|
||||
⚠ eslint(arrow-body-style): Expected block statement surrounding arrow body.
|
||||
╭─[arrow_body_style.ts:1:17]
|
||||
1 │ var foo = () => <Foo>({ id });
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Surround the arrow body with braces and use a return statement.
|
||||
|
||||
⚠ eslint(arrow-body-style): Expected block statement surrounding arrow body.
|
||||
╭─[arrow_body_style.ts:1:17]
|
||||
1 │ var foo = () => ({ id }) as Foo;
|
||||
· ───────────────
|
||||
╰────
|
||||
help: Surround the arrow body with braces and use a return statement.
|
||||
|
||||
⚠ eslint(arrow-body-style): Expected block statement surrounding arrow body.
|
||||
╭─[arrow_body_style.ts:1:17]
|
||||
1 │ var foo = () => ({ id }) satisfies Foo;
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Surround the arrow body with braces and use a return statement.
|
||||
|
||||
⚠ eslint(arrow-body-style): Expected block statement surrounding arrow body.
|
||||
╭─[arrow_body_style.ts:1:17]
|
||||
1 │ var foo = () => ({ id })!;
|
||||
· ─────────
|
||||
╰────
|
||||
help: Surround the arrow body with braces and use a return statement.
|
||||
|
||||
⚠ eslint(arrow-body-style): Expected block statement surrounding arrow body.
|
||||
╭─[arrow_body_style.ts:1:17]
|
||||
1 │ var foo = () => (({ id }) as Foo);
|
||||
· ─────────────────
|
||||
╰────
|
||||
help: Surround the arrow body with braces and use a return statement.
|
||||
|
||||
⚠ eslint(arrow-body-style): Expected block statement surrounding arrow body.
|
||||
╭─[arrow_body_style.ts:1:17]
|
||||
1 │ var foo = () => <Foo>JSON.parse(id);
|
||||
· ───────────────────
|
||||
╰────
|
||||
help: Surround the arrow body with braces and use a return statement.
|
||||
|
||||
⚠ eslint(arrow-body-style): Expected block statement surrounding arrow body.
|
||||
╭─[arrow_body_style.ts:1:17]
|
||||
1 │ var foo = () => ({ id }) as Foo;
|
||||
· ───────────────
|
||||
╰────
|
||||
help: Surround the arrow body with braces and use a return statement.
|
||||
Reference in new issue
Block a user