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
103 changes: 89 additions & 14 deletions crates/oxc_linter/src/rules/unicorn/no_invalid_fetch_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<'_>,
Expand Down Expand Up @@ -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();
Expand All @@ -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();
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -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![
Expand All @@ -192,16 +262,21 @@ 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"});"#,
r#"new Request(url, {body: undefined, body: "foo=bar"});"#,
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand All @@ -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)
Expand Down Expand Up @@ -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 │ }
╰────