Skip to content

Commit eeb4edf

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

File tree

3 files changed

+233
-14
lines changed

3 files changed

+233
-14
lines changed

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

+88-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,57 @@ 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.to_ascii_uppercase() == "GET"
165+
|| str_lit.value.to_ascii_uppercase()
166+
== "HEAD";
167+
}
168+
}
169+
false
170+
}) {
171+
method_name = UNKNOWN_METHOD_NAME;
172+
}
173+
}
174+
TSType::TSLiteralType(literal_type) => {
175+
let TSLiteralType { literal, .. } = &**literal_type;
176+
if let TSLiteral::StringLiteral(str_lit) = literal {
177+
method_name = str_lit.value.cow_to_ascii_uppercase();
178+
}
179+
}
180+
_ => {
181+
method_name = UNKNOWN_METHOD_NAME;
182+
}
183+
}
184+
}
185+
_ => continue,
186+
}
136187
}
137188
_ => continue,
138189
};
139-
140-
method_name = method.cow_to_ascii_uppercase();
141190
}
142191
}
143192

@@ -148,6 +197,18 @@ fn is_invalid_fetch_options<'a>(
148197
}
149198
}
150199

200+
fn extract_method_name_from_template_literal<'a>(
201+
template_lit: &'a TemplateLiteral<'a>,
202+
) -> Cow<'a, str> {
203+
if let Some(template_element_value) = template_lit.quasis.get(0) {
204+
// only one template element
205+
if template_element_value.tail {
206+
return template_element_value.value.raw.cow_to_ascii_uppercase();
207+
}
208+
}
209+
UNKNOWN_METHOD_NAME
210+
}
211+
151212
#[test]
152213
fn test() {
153214
use crate::tester::Tester;
@@ -182,6 +243,14 @@ fn test() {
182243
r#"fetch('/', {body: new URLSearchParams({ data: "test" }), method: "POST"})"#,
183244
r#"const method = "post"; new Request(url, {method, body: "foo=bar"})"#,
184245
r#"const method = "post"; fetch(url, {method, body: "foo=bar"})"#,
246+
r#"const method = `post`; fetch(url, {method, body: "foo=bar"})"#,
247+
r#"const method = `po${"st"}`; fetch(url, {method, body: "foo=bar"})"#,
248+
r#"function foo(method: "POST" | "PUT", body: string) {
249+
return new Request(url, {method, body});
250+
}"#,
251+
r#"function foo(method: string, body: string) {
252+
return new Request(url, {method, body});
253+
}"#,
185254
];
186255

187256
let fail = vec![
@@ -192,16 +261,21 @@ fn test() {
192261
r#"fetch(url, {method: "HEAD", body})"#,
193262
r#"new Request(url, {method: "HEAD", body})"#,
194263
r#"fetch(url, {method: "head", body})"#,
264+
r#"fetch(url, {method: `head`, body: "foo=bar"})"#,
195265
r#"new Request(url, {method: "head", body})"#,
196266
r#"const method = "head"; new Request(url, {method, body: "foo=bar"})"#,
197267
r#"const method = "head"; fetch(url, {method, body: "foo=bar"})"#,
268+
r#"const method = `head`; fetch(url, {method, body: "foo=bar"})"#,
198269
r"fetch(url, {body}, extraArgument)",
199270
r"new Request(url, {body}, extraArgument)",
200271
r#"fetch(url, {body: undefined, body: "foo=bar"});"#,
201272
r#"new Request(url, {body: undefined, body: "foo=bar"});"#,
202273
r#"fetch(url, {method: "post", body: "foo=bar", method: "HEAD"});"#,
203274
r#"new Request(url, {method: "post", body: "foo=bar", method: "HEAD"});"#,
204275
r#"fetch('/', {body: new URLSearchParams({ data: "test" })})"#,
276+
r#"function foo(method: "HEAD" | "GET") {
277+
return new Request(url, {method, body: ""});
278+
}"#,
205279
];
206280

207281
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+
╰────
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
assertion_line: 357
4+
---
5+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
6+
╭─[no_invalid_fetch_options.tsx:1:13]
7+
1 │ fetch(url, {body})
8+
· ────
9+
╰────
10+
11+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
12+
╭─[no_invalid_fetch_options.tsx:1:19]
13+
1 │ new Request(url, {body})
14+
· ────
15+
╰────
16+
17+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
18+
╭─[no_invalid_fetch_options.tsx:1:28]
19+
1 │ fetch(url, {method: "GET", body})
20+
· ────
21+
╰────
22+
23+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
24+
╭─[no_invalid_fetch_options.tsx:1:34]
25+
1 │ new Request(url, {method: "GET", body})
26+
· ────
27+
╰────
28+
29+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
30+
╭─[no_invalid_fetch_options.tsx:1:29]
31+
1 │ fetch(url, {method: "HEAD", body})
32+
· ────
33+
╰────
34+
35+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
36+
╭─[no_invalid_fetch_options.tsx:1:35]
37+
1 │ new Request(url, {method: "HEAD", body})
38+
· ────
39+
╰────
40+
41+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
42+
╭─[no_invalid_fetch_options.tsx:1:29]
43+
1 │ fetch(url, {method: "head", body})
44+
· ────
45+
╰────
46+
47+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
48+
╭─[no_invalid_fetch_options.tsx:1:29]
49+
1 │ fetch(url, {method: `head`, body: "foo=bar"})
50+
· ────
51+
╰────
52+
53+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
54+
╭─[no_invalid_fetch_options.tsx:1:35]
55+
1 │ new Request(url, {method: "head", body})
56+
· ────
57+
╰────
58+
59+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
60+
╭─[no_invalid_fetch_options.tsx:1:50]
61+
1 │ const method = "head"; new Request(url, {method, body: "foo=bar"})
62+
· ────
63+
╰────
64+
65+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
66+
╭─[no_invalid_fetch_options.tsx:1:44]
67+
1 │ const method = "head"; fetch(url, {method, body: "foo=bar"})
68+
· ────
69+
╰────
70+
71+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
72+
╭─[no_invalid_fetch_options.tsx:1:44]
73+
1 │ const method = `head`; fetch(url, {method, body: "foo=bar"})
74+
· ────
75+
╰────
76+
77+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
78+
╭─[no_invalid_fetch_options.tsx:1:13]
79+
1 │ fetch(url, {body}, extraArgument)
80+
· ────
81+
╰────
82+
83+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
84+
╭─[no_invalid_fetch_options.tsx:1:19]
85+
1 │ new Request(url, {body}, extraArgument)
86+
· ────
87+
╰────
88+
89+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
90+
╭─[no_invalid_fetch_options.tsx:1:30]
91+
1 │ fetch(url, {body: undefined, body: "foo=bar"});
92+
· ────
93+
╰────
94+
95+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
96+
╭─[no_invalid_fetch_options.tsx:1:36]
97+
1 │ new Request(url, {body: undefined, body: "foo=bar"});
98+
· ────
99+
╰────
100+
101+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
102+
╭─[no_invalid_fetch_options.tsx:1:29]
103+
1 │ fetch(url, {method: "post", body: "foo=bar", method: "HEAD"});
104+
· ────
105+
╰────
106+
107+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "HEAD"
108+
╭─[no_invalid_fetch_options.tsx:1:35]
109+
1 │ new Request(url, {method: "post", body: "foo=bar", method: "HEAD"});
110+
· ────
111+
╰────
112+
113+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
114+
╭─[no_invalid_fetch_options.tsx:1:13]
115+
1 │ fetch('/', {body: new URLSearchParams({ data: "test" })})
116+
· ────
117+
╰────
118+
119+
⚠ eslint-plugin-unicorn(no-invalid-fetch-options): "body" is not allowed when method is "GET"
120+
╭─[no_invalid_fetch_options.tsx:2:46]
121+
1 │ function foo(method: "HEAD" | "GET") {
122+
2 │ return new Request(url, {method, body: ""});
123+
· ────
124+
3 │ }
125+
╰────

0 commit comments

Comments
 (0)