From 5ee2cabc7c49d3f6e0b2265b85dfb7d56d60eb46 Mon Sep 17 00:00:00 2001 From: Brooooooklyn Date: Tue, 25 Feb 2025 09:42:16 +0000 Subject: [PATCH] feat(linter): improve no_invalid_fetch_options (#9347) --- .../rules/unicorn/no_invalid_fetch_options.rs | 103 +++++++++++++++--- .../unicorn_no_invalid_fetch_options.snap | 20 ++++ 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/crates/oxc_linter/src/rules/unicorn/no_invalid_fetch_options.rs b/crates/oxc_linter/src/rules/unicorn/no_invalid_fetch_options.rs index 13b46b004d858..e6746bacc7ff9 100644 --- a/crates/oxc_linter/src/rules/unicorn/no_invalid_fetch_options.rs +++ b/crates/oxc_linter/src/rules/unicorn/no_invalid_fetch_options.rs @@ -4,7 +4,11 @@ use cow_utils::CowUtils; use oxc_allocator::Box; use oxc_ast::{ AstKind, - ast::{Argument, Expression, ObjectExpression, ObjectPropertyKind, PropertyKey}, + ast::{ + Argument, BindingPattern, Expression, FormalParameter, ObjectExpression, + ObjectPropertyKind, PropertyKey, TSLiteral, TSLiteralType, TSType, TSTypeAnnotation, + TemplateLiteral, + }, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; @@ -83,6 +87,9 @@ impl Rule for NoInvalidFetchOptions { } } +// set to method_name to "UNKNOWN" if we can't infer the method name +const UNKNOWN_METHOD_NAME: Cow<'static, str> = Cow::Borrowed("UNKNOWN"); + fn is_invalid_fetch_options<'a>( obj_expr: &'a Box<'_, ObjectExpression<'_>>, ctx: &'a LintContext<'_>, @@ -112,8 +119,13 @@ fn is_invalid_fetch_options<'a>( body_span = key_ident.span; } } else if key_ident_name == "method" { - let method = match &obj_prop.value { - Expression::StringLiteral(value_ident) => &value_ident.value, + match &obj_prop.value { + Expression::StringLiteral(value_ident) => { + method_name = value_ident.value.cow_to_ascii_uppercase(); + } + Expression::TemplateLiteral(template_lit) => { + method_name = extract_method_name_from_template_literal(template_lit); + } Expression::Identifier(value_ident) => { let symbols = ctx.semantic().symbols(); let reference_id = value_ident.reference_id(); @@ -124,20 +136,58 @@ fn is_invalid_fetch_options<'a>( let decl = ctx.semantic().nodes().get_node(symbols.get_declaration(symbol_id)); - let AstKind::VariableDeclarator(declarator) = decl.kind() else { - continue; - }; - - let Some(Expression::StringLiteral(str_lit)) = &declarator.init else { - continue; - }; - - &str_lit.value + match decl.kind() { + AstKind::VariableDeclarator(declarator) => match &declarator.init { + Some(Expression::StringLiteral(str_lit)) => { + method_name = str_lit.value.cow_to_ascii_uppercase(); + } + Some(Expression::TemplateLiteral(template_lit)) => { + method_name = + extract_method_name_from_template_literal(template_lit); + } + _ => { + method_name = UNKNOWN_METHOD_NAME; + continue; + } + }, + AstKind::FormalParameter(FormalParameter { + pattern: BindingPattern { type_annotation: Some(annotation), .. }, + .. + }) => { + let TSTypeAnnotation { type_annotation, .. } = &**annotation; + match type_annotation { + TSType::TSUnionType(union_type) => { + if !union_type.types.iter().any(|ty| { + if let TSType::TSLiteralType(ty) = ty { + let TSLiteralType { literal, .. } = &**ty; + if let TSLiteral::StringLiteral(str_lit) = literal { + return str_lit.value.cow_to_ascii_uppercase() + == "GET" + || str_lit.value.cow_to_ascii_uppercase() + == "HEAD"; + } + } + false + }) { + method_name = UNKNOWN_METHOD_NAME; + } + } + TSType::TSLiteralType(literal_type) => { + let TSLiteralType { literal, .. } = &**literal_type; + if let TSLiteral::StringLiteral(str_lit) = literal { + method_name = str_lit.value.cow_to_ascii_uppercase(); + } + } + _ => { + method_name = UNKNOWN_METHOD_NAME; + } + } + } + _ => continue, + } } _ => continue, }; - - method_name = method.cow_to_ascii_uppercase(); } } @@ -148,6 +198,18 @@ fn is_invalid_fetch_options<'a>( } } +fn extract_method_name_from_template_literal<'a>( + template_lit: &'a TemplateLiteral<'a>, +) -> Cow<'a, str> { + if let Some(template_element_value) = template_lit.quasis.first() { + // only one template element + if template_element_value.tail { + return template_element_value.value.raw.cow_to_ascii_uppercase(); + } + } + UNKNOWN_METHOD_NAME +} + #[test] fn test() { use crate::tester::Tester; @@ -182,6 +244,14 @@ fn test() { r#"fetch('/', {body: new URLSearchParams({ data: "test" }), method: "POST"})"#, r#"const method = "post"; new Request(url, {method, body: "foo=bar"})"#, r#"const method = "post"; fetch(url, {method, body: "foo=bar"})"#, + r#"const method = `post`; fetch(url, {method, body: "foo=bar"})"#, + r#"const method = `po${"st"}`; fetch(url, {method, body: "foo=bar"})"#, + r#"function foo(method: "POST" | "PUT", body: string) { + return new Request(url, {method, body}); + }"#, + "function foo(method: string, body: string) { + return new Request(url, {method, body}); + }", ]; let fail = vec![ @@ -192,9 +262,11 @@ fn test() { r#"fetch(url, {method: "HEAD", body})"#, r#"new Request(url, {method: "HEAD", body})"#, r#"fetch(url, {method: "head", body})"#, + r#"fetch(url, {method: `head`, body: "foo=bar"})"#, r#"new Request(url, {method: "head", body})"#, r#"const method = "head"; new Request(url, {method, body: "foo=bar"})"#, r#"const method = "head"; fetch(url, {method, body: "foo=bar"})"#, + r#"const method = `head`; fetch(url, {method, body: "foo=bar"})"#, r"fetch(url, {body}, extraArgument)", r"new Request(url, {body}, extraArgument)", r#"fetch(url, {body: undefined, body: "foo=bar"});"#, @@ -202,6 +274,9 @@ fn test() { r#"fetch(url, {method: "post", body: "foo=bar", method: "HEAD"});"#, r#"new Request(url, {method: "post", body: "foo=bar", method: "HEAD"});"#, r#"fetch('/', {body: new URLSearchParams({ data: "test" })})"#, + r#"function foo(method: "HEAD" | "GET") { + return new Request(url, {method, body: ""}); + }"#, ]; Tester::new(NoInvalidFetchOptions::NAME, NoInvalidFetchOptions::PLUGIN, pass, fail) diff --git a/crates/oxc_linter/src/snapshots/unicorn_no_invalid_fetch_options.snap b/crates/oxc_linter/src/snapshots/unicorn_no_invalid_fetch_options.snap index 4af1dd184c973..9c5de0fd4a1c7 100644 --- a/crates/oxc_linter/src/snapshots/unicorn_no_invalid_fetch_options.snap +++ b/crates/oxc_linter/src/snapshots/unicorn_no_invalid_fetch_options.snap @@ -43,6 +43,12 @@ source: crates/oxc_linter/src/tester.rs · ──── ╰──── + ⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD" + ╭─[no_invalid_fetch_options.tsx:1:29] + 1 │ fetch(url, {method: `head`, body: "foo=bar"}) + · ──── + ╰──── + ⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD" ╭─[no_invalid_fetch_options.tsx:1:35] 1 │ new Request(url, {method: "head", body}) @@ -61,6 +67,12 @@ source: crates/oxc_linter/src/tester.rs · ──── ╰──── + ⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD" + ╭─[no_invalid_fetch_options.tsx:1:44] + 1 │ const method = `head`; fetch(url, {method, body: "foo=bar"}) + · ──── + ╰──── + ⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET" ╭─[no_invalid_fetch_options.tsx:1:13] 1 │ fetch(url, {body}, extraArgument) @@ -102,3 +114,11 @@ source: crates/oxc_linter/src/tester.rs 1 │ fetch('/', {body: new URLSearchParams({ data: "test" })}) · ──── ╰──── + + ⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET" + ╭─[no_invalid_fetch_options.tsx:2:46] + 1 │ function foo(method: "HEAD" | "GET") { + 2 │ return new Request(url, {method, body: ""}); + · ──── + 3 │ } + ╰────