Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 107 additions & 4 deletions crates/oxc_linter/src/rules/unicorn/prefer_response_static_json.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use oxc_ast::{
AstKind,
ast::{CallExpression, Expression},
ast::{CallExpression, Expression, NewExpression},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
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,
};
Expand Down Expand Up @@ -47,7 +47,7 @@ declare_oxc_lint!(
PreferResponseStaticJson,
unicorn,
style,
pending,
suggestion
);

impl Rule for PreferResponseStaticJson {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -26,37 +29,43 @@ 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]
1 │ foo
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]
1 │ foo;
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]
1 │ foo;
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]
1 │ foo
2 │ (( new (( Response ))(JSON.stringify(data)) ))
· ──────────────
╰────
help: Replace `new Response(JSON.stringify(...))` with `Response.json(...)`
Loading