diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_response_static_json.rs b/crates/oxc_linter/src/rules/unicorn/prefer_response_static_json.rs index 005f4e0c0c397..963264b6c1c37 100644 --- a/crates/oxc_linter/src/rules/unicorn/prefer_response_static_json.rs +++ b/crates/oxc_linter/src/rules/unicorn/prefer_response_static_json.rs @@ -1,6 +1,6 @@ use oxc_ast::{ AstKind, - ast::{CallExpression, Expression}, + ast::{CallExpression, Expression, NewExpression}, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; @@ -8,7 +8,7 @@ use oxc_span::{GetSpan, Span}; use crate::{ AstNode, - ast_util::{is_method_call, is_new_expression}, + ast_util::{could_be_asi_hazard, is_method_call, is_new_expression}, context::LintContext, rule::Rule, }; @@ -47,7 +47,7 @@ declare_oxc_lint!( PreferResponseStaticJson, unicorn, style, - pending, + suggestion ); impl Rule for PreferResponseStaticJson { @@ -80,10 +80,68 @@ impl Rule for PreferResponseStaticJson { return; } - ctx.diagnostic(prefer_response_static_json_diagnostic(call_expr.callee.span())); + ctx.diagnostic_with_suggestion( + prefer_response_static_json_diagnostic(call_expr.callee.span()), + |fixer| { + let fixer = fixer.for_multifix(); + let mut fix = fixer.new_fix_with_capacity(7); + + let inner_callee = new_expr.callee.get_inner_expression(); + + fix.push(fixer.insert_text_after(inner_callee, ".json")); + + let new_keyword_end = new_expr.span.start + 3; // "new" is 3 chars + let callee_start = new_expr.callee.span().start; + + if ctx.has_comments_between(Span::new(new_keyword_end, callee_start)) { + fix.push(fixer.insert_text_before_range(new_expr.span, "( ")); + fix.push( + fixer.delete_range(Span::new(new_expr.span.start, new_keyword_end + 1)), + ); + fix.push(fixer.insert_text_after_range(new_expr.span, ")")); + } else { + let new_keyword_span = + Span::new(new_expr.span.start, new_expr.callee.span().start); + fix.push(fixer.delete_range(new_keyword_span)); + } + + let Some(data_arg) = call_expr.arguments.first() else { + return fixer.noop(); + }; + let Some(data_expr) = data_arg.as_expression() else { + return fixer.noop(); + }; + + let data_span = data_expr.span(); + let stringify_call_span = argument_expr.span(); + + let before_data_span = Span::new(stringify_call_span.start, data_span.start); + fix.push(fixer.delete_range(before_data_span)); + + let after_data_span = Span::new(data_span.end, stringify_call_span.end); + fix.push(fixer.delete_range(after_data_span)); + + if should_add_semicolon(node, new_expr, ctx) { + fix.push(fixer.insert_text_before_range(new_expr.span, ";")); + } + + fix.with_message( + "Replace `new Response(JSON.stringify(...))` with `Response.json(...)`", + ) + }, + ); } } +fn should_add_semicolon(node: &AstNode, new_expr: &NewExpression, ctx: &LintContext) -> bool { + let parent = ctx.nodes().parent_node(node.id()); + let new_expr_is_parenthesized = matches!(parent.kind(), AstKind::ParenthesizedExpression(_)); + + let callee_is_parenthesized = !matches!(new_expr.callee, Expression::Identifier(_)); + + !new_expr_is_parenthesized && callee_is_parenthesized && could_be_asi_hazard(node, ctx) +} + fn stringify_has_spread_arguments(call_expr: &CallExpression) -> bool { call_expr.arguments.iter().any(oxc_ast::ast::Argument::is_spread) } @@ -128,6 +186,51 @@ fn test() { (( new (( Response ))(JSON.stringify(data)) ))", ]; + let fix = vec![ + ("new Response(JSON.stringify(data))", "Response.json(data)"), + ("new Response(JSON.stringify(data), extraArgument)", "Response.json(data, extraArgument)"), + ( + "new Response( (( JSON.stringify( (( 0, data )), ) )), )", + "Response.json( (( 0, data )), )", + ), + ( + "function foo() { + return new // comment + Response(JSON.stringify(data)) + }", + "function foo() { + return ( // comment + Response.json(data)) + }", + ), + ("new Response(JSON.stringify(data), {status: 200})", "Response.json(data, {status: 200})"), + ( + "foo + new (( Response ))(JSON.stringify(data))", + "foo + ;(( Response.json ))(data)", + ), + ( + "foo; + new (( Response ))(JSON.stringify(data))", + "foo; + (( Response.json ))(data)", + ), + ( + "foo; + (( new (( Response ))(JSON.stringify(data)) ))", + "foo; + (( (( Response.json ))(data) ))", + ), + ( + "foo + (( new (( Response ))(JSON.stringify(data)) ))", + "foo + (( (( Response.json ))(data) ))", + ), + ]; + Tester::new(PreferResponseStaticJson::NAME, PreferResponseStaticJson::PLUGIN, pass, fail) + .expect_fix(fix) .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/unicorn_prefer_response_static_json.snap b/crates/oxc_linter/src/snapshots/unicorn_prefer_response_static_json.snap index ad791672bd2ce..c8d544c572fc4 100644 --- a/crates/oxc_linter/src/snapshots/unicorn_prefer_response_static_json.snap +++ b/crates/oxc_linter/src/snapshots/unicorn_prefer_response_static_json.snap @@ -6,18 +6,21 @@ source: crates/oxc_linter/src/tester.rs 1 │ new Response(JSON.stringify(data)) · ────────────── ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)` ⚠ eslint-plugin-unicorn(prefer-response-static-json): Prefer using `Response.json(…)` over `JSON.stringify()`. ╭─[prefer_response_static_json.tsx:1:14] 1 │ new Response(JSON.stringify(data), extraArgument) · ────────────── ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)` ⚠ eslint-plugin-unicorn(prefer-response-static-json): Prefer using `Response.json(…)` over `JSON.stringify()`. ╭─[prefer_response_static_json.tsx:1:18] 1 │ new Response( (( JSON.stringify( (( 0, data )), ) )), ) · ────────────── ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)` ⚠ eslint-plugin-unicorn(prefer-response-static-json): Prefer using `Response.json(…)` over `JSON.stringify()`. ╭─[prefer_response_static_json.tsx:3:15] @@ -26,12 +29,14 @@ source: crates/oxc_linter/src/tester.rs · ────────────── 4 │ } ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)` ⚠ eslint-plugin-unicorn(prefer-response-static-json): Prefer using `Response.json(…)` over `JSON.stringify()`. ╭─[prefer_response_static_json.tsx:1:14] 1 │ new Response(JSON.stringify(data), {status: 200}) · ────────────── ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)` ⚠ eslint-plugin-unicorn(prefer-response-static-json): Prefer using `Response.json(…)` over `JSON.stringify()`. ╭─[prefer_response_static_json.tsx:2:23] @@ -39,6 +44,7 @@ source: crates/oxc_linter/src/tester.rs 2 │ new (( Response ))(JSON.stringify(data)) · ────────────── ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)` ⚠ eslint-plugin-unicorn(prefer-response-static-json): Prefer using `Response.json(…)` over `JSON.stringify()`. ╭─[prefer_response_static_json.tsx:2:23] @@ -46,6 +52,7 @@ source: crates/oxc_linter/src/tester.rs 2 │ new (( Response ))(JSON.stringify(data)) · ────────────── ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)` ⚠ eslint-plugin-unicorn(prefer-response-static-json): Prefer using `Response.json(…)` over `JSON.stringify()`. ╭─[prefer_response_static_json.tsx:2:26] @@ -53,6 +60,7 @@ source: crates/oxc_linter/src/tester.rs 2 │ (( new (( Response ))(JSON.stringify(data)) )) · ────────────── ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)` ⚠ eslint-plugin-unicorn(prefer-response-static-json): Prefer using `Response.json(…)` over `JSON.stringify()`. ╭─[prefer_response_static_json.tsx:2:26] @@ -60,3 +68,4 @@ source: crates/oxc_linter/src/tester.rs 2 │ (( new (( Response ))(JSON.stringify(data)) )) · ────────────── ╰──── + help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)`