Skip to content

Commit bbf4755

Browse files
committed
feat(linter): improve no_invalid_fetch_options
1 parent 17acece commit bbf4755

File tree

2 files changed

+109
-14
lines changed

2 files changed

+109
-14
lines changed

crates/oxc_linter/src/rules/unicorn/no_invalid_fetch_options.rs

+89-14
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ use cow_utils::CowUtils;
44
use oxc_allocator::Box;
55
use oxc_ast::{
66
AstKind,
7-
ast::{Argument, Expression, ObjectExpression, ObjectPropertyKind, PropertyKey},
7+
ast::{
8+
Argument, BindingPattern, Expression, FormalParameter, ObjectExpression,
9+
ObjectPropertyKind, PropertyKey, TSLiteral, TSLiteralType, TSType, TSTypeAnnotation,
10+
TemplateLiteral,
11+
},
812
};
913
use oxc_diagnostics::OxcDiagnostic;
1014
use oxc_macros::declare_oxc_lint;
@@ -83,6 +87,9 @@ impl Rule for NoInvalidFetchOptions {
8387
}
8488
}
8589

90+
// set to method_name to "UNKNOWN" if we can't infer the method name
91+
const UNKNOWN_METHOD_NAME: Cow<'static, str> = Cow::Borrowed("UNKNOWN");
92+
8693
fn is_invalid_fetch_options<'a>(
8794
obj_expr: &'a Box<'_, ObjectExpression<'_>>,
8895
ctx: &'a LintContext<'_>,
@@ -112,8 +119,13 @@ fn is_invalid_fetch_options<'a>(
112119
body_span = key_ident.span;
113120
}
114121
} else if key_ident_name == "method" {
115-
let method = match &obj_prop.value {
116-
Expression::StringLiteral(value_ident) => &value_ident.value,
122+
match &obj_prop.value {
123+
Expression::StringLiteral(value_ident) => {
124+
method_name = value_ident.value.cow_to_ascii_uppercase();
125+
}
126+
Expression::TemplateLiteral(template_lit) => {
127+
method_name = extract_method_name_from_template_literal(template_lit);
128+
}
117129
Expression::Identifier(value_ident) => {
118130
let symbols = ctx.semantic().symbols();
119131
let reference_id = value_ident.reference_id();
@@ -124,20 +136,58 @@ fn is_invalid_fetch_options<'a>(
124136

125137
let decl = ctx.semantic().nodes().get_node(symbols.get_declaration(symbol_id));
126138

127-
let AstKind::VariableDeclarator(declarator) = decl.kind() else {
128-
continue;
129-
};
130-
131-
let Some(Expression::StringLiteral(str_lit)) = &declarator.init else {
132-
continue;
133-
};
134-
135-
&str_lit.value
139+
match decl.kind() {
140+
AstKind::VariableDeclarator(declarator) => match &declarator.init {
141+
Some(Expression::StringLiteral(str_lit)) => {
142+
method_name = str_lit.value.cow_to_ascii_uppercase();
143+
}
144+
Some(Expression::TemplateLiteral(template_lit)) => {
145+
method_name =
146+
extract_method_name_from_template_literal(template_lit);
147+
}
148+
_ => {
149+
method_name = UNKNOWN_METHOD_NAME;
150+
continue;
151+
}
152+
},
153+
AstKind::FormalParameter(FormalParameter {
154+
pattern: BindingPattern { type_annotation: Some(annotation), .. },
155+
..
156+
}) => {
157+
let TSTypeAnnotation { type_annotation, .. } = &**annotation;
158+
match type_annotation {
159+
TSType::TSUnionType(union_type) => {
160+
if !union_type.types.iter().any(|ty| {
161+
if let TSType::TSLiteralType(ty) = ty {
162+
let TSLiteralType { literal, .. } = &**ty;
163+
if let TSLiteral::StringLiteral(str_lit) = literal {
164+
return str_lit.value.cow_to_ascii_uppercase()
165+
== "GET"
166+
|| str_lit.value.cow_to_ascii_uppercase()
167+
== "HEAD";
168+
}
169+
}
170+
false
171+
}) {
172+
method_name = UNKNOWN_METHOD_NAME;
173+
}
174+
}
175+
TSType::TSLiteralType(literal_type) => {
176+
let TSLiteralType { literal, .. } = &**literal_type;
177+
if let TSLiteral::StringLiteral(str_lit) = literal {
178+
method_name = str_lit.value.cow_to_ascii_uppercase();
179+
}
180+
}
181+
_ => {
182+
method_name = UNKNOWN_METHOD_NAME;
183+
}
184+
}
185+
}
186+
_ => continue,
187+
}
136188
}
137189
_ => continue,
138190
};
139-
140-
method_name = method.cow_to_ascii_uppercase();
141191
}
142192
}
143193

@@ -148,6 +198,18 @@ fn is_invalid_fetch_options<'a>(
148198
}
149199
}
150200

201+
fn extract_method_name_from_template_literal<'a>(
202+
template_lit: &'a TemplateLiteral<'a>,
203+
) -> Cow<'a, str> {
204+
if let Some(template_element_value) = template_lit.quasis.first() {
205+
// only one template element
206+
if template_element_value.tail {
207+
return template_element_value.value.raw.cow_to_ascii_uppercase();
208+
}
209+
}
210+
UNKNOWN_METHOD_NAME
211+
}
212+
151213
#[test]
152214
fn test() {
153215
use crate::tester::Tester;
@@ -182,6 +244,14 @@ fn test() {
182244
r#"fetch('/', {body: new URLSearchParams({ data: "test" }), method: "POST"})"#,
183245
r#"const method = "post"; new Request(url, {method, body: "foo=bar"})"#,
184246
r#"const method = "post"; fetch(url, {method, body: "foo=bar"})"#,
247+
r#"const method = `post`; fetch(url, {method, body: "foo=bar"})"#,
248+
r#"const method = `po${"st"}`; fetch(url, {method, body: "foo=bar"})"#,
249+
r#"function foo(method: "POST" | "PUT", body: string) {
250+
return new Request(url, {method, body});
251+
}"#,
252+
"function foo(method: string, body: string) {
253+
return new Request(url, {method, body});
254+
}",
185255
];
186256

187257
let fail = vec![
@@ -192,16 +262,21 @@ fn test() {
192262
r#"fetch(url, {method: "HEAD", body})"#,
193263
r#"new Request(url, {method: "HEAD", body})"#,
194264
r#"fetch(url, {method: "head", body})"#,
265+
r#"fetch(url, {method: `head`, body: "foo=bar"})"#,
195266
r#"new Request(url, {method: "head", body})"#,
196267
r#"const method = "head"; new Request(url, {method, body: "foo=bar"})"#,
197268
r#"const method = "head"; fetch(url, {method, body: "foo=bar"})"#,
269+
r#"const method = `head`; fetch(url, {method, body: "foo=bar"})"#,
198270
r"fetch(url, {body}, extraArgument)",
199271
r"new Request(url, {body}, extraArgument)",
200272
r#"fetch(url, {body: undefined, body: "foo=bar"});"#,
201273
r#"new Request(url, {body: undefined, body: "foo=bar"});"#,
202274
r#"fetch(url, {method: "post", body: "foo=bar", method: "HEAD"});"#,
203275
r#"new Request(url, {method: "post", body: "foo=bar", method: "HEAD"});"#,
204276
r#"fetch('/', {body: new URLSearchParams({ data: "test" })})"#,
277+
r#"function foo(method: "HEAD" | "GET") {
278+
return new Request(url, {method, body: ""});
279+
}"#,
205280
];
206281

207282
Tester::new(NoInvalidFetchOptions::NAME, NoInvalidFetchOptions::PLUGIN, pass, fail)

crates/oxc_linter/src/snapshots/unicorn_no_invalid_fetch_options.snap

+20
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ source: crates/oxc_linter/src/tester.rs
4343
· ────
4444
╰────
4545

46+
eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
47+
╭─[no_invalid_fetch_options.tsx:1:29]
48+
1fetch(url, {method: `head`, body: "foo=bar"})
49+
· ────
50+
╰────
51+
4652
eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
4753
╭─[no_invalid_fetch_options.tsx:1:35]
4854
1new Request(url, {method: "head", body})
@@ -61,6 +67,12 @@ source: crates/oxc_linter/src/tester.rs
6167
· ────
6268
╰────
6369

70+
eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
71+
╭─[no_invalid_fetch_options.tsx:1:44]
72+
1const method = `head`; fetch(url, {method, body: "foo=bar"})
73+
· ────
74+
╰────
75+
6476
eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
6577
╭─[no_invalid_fetch_options.tsx:1:13]
6678
1fetch(url, {body}, extraArgument)
@@ -102,3 +114,11 @@ source: crates/oxc_linter/src/tester.rs
102114
1fetch('/', {body: new URLSearchParams({ data: "test" })})
103115
· ────
104116
╰────
117+
118+
eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
119+
╭─[no_invalid_fetch_options.tsx:2:46]
120+
1function foo(method: "HEAD" | "GET") {
121+
2return new Request(url, {method, body: ""});
122+
· ────
123+
3 │ }
124+
╰────

0 commit comments

Comments
 (0)