Skip to content

Commit adb8ebd

Browse files
keita-hinoSysix
andauthoredJan 31, 2025··
1 parent 3bc05fa commit adb8ebd

File tree

3 files changed

+393
-0
lines changed

3 files changed

+393
-0
lines changed
 

Diff for: ‎crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ mod eslint {
135135
pub mod no_unused_labels;
136136
pub mod no_unused_private_class_members;
137137
pub mod no_unused_vars;
138+
pub mod no_useless_call;
138139
pub mod no_useless_catch;
139140
pub mod no_useless_concat;
140141
pub mod no_useless_constructor;
@@ -546,6 +547,7 @@ oxc_macros::declare_all_lint_rules! {
546547
eslint::max_lines,
547548
eslint::max_params,
548549
eslint::new_cap,
550+
eslint::no_useless_call,
549551
eslint::no_extra_label,
550552
eslint::no_multi_assign,
551553
eslint::no_nested_ternary,
+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use oxc_ast::{
2+
ast::{CallExpression, ChainElement, Expression, MemberExpression},
3+
match_member_expression, AstKind,
4+
};
5+
use oxc_diagnostics::OxcDiagnostic;
6+
use oxc_macros::declare_oxc_lint;
7+
use oxc_span::cmp::ContentEq;
8+
use oxc_span::Span;
9+
10+
use crate::{context::LintContext, rule::Rule, AstNode};
11+
12+
fn no_useless_call_diagnostic(name: &str, span: Span) -> OxcDiagnostic {
13+
OxcDiagnostic::warn(format!("Avoid unnecessary use of .{name}()"))
14+
.with_help("Replace with a normal function invocation")
15+
.with_label(span)
16+
}
17+
18+
#[derive(Debug, Default, Clone)]
19+
pub struct NoUselessCall;
20+
21+
declare_oxc_lint!(
22+
/// ### What it does
23+
/// Disallow unnecessary calls to `.call()` and `.apply()`
24+
///
25+
/// ### Why is this bad?
26+
/// `Function.prototype.call()` and `Function.prototype.apply()` are slower than the normal function invocation.
27+
///
28+
/// This rule compares code statically to check whether or not thisArg is changed.
29+
/// So if the code about thisArg is a dynamic expression, this rule cannot judge correctly.
30+
///
31+
/// ### Examples
32+
///
33+
/// Examples of **incorrect** code for this rule:
34+
/// ```js
35+
/// // These are same as `foo(1, 2, 3);`
36+
/// foo.call(undefined, 1, 2, 3);
37+
/// foo.apply(undefined, [1, 2, 3]);
38+
/// foo.call(null, 1, 2, 3);
39+
/// foo.apply(null, [1, 2, 3]);
40+
///
41+
/// // These are same as `obj.foo(1, 2, 3);`
42+
/// obj.foo.call(obj, 1, 2, 3);
43+
/// obj.foo.apply(obj, [1, 2, 3]);
44+
/// ```
45+
///
46+
/// Examples of **correct** code for this rule:
47+
/// ```js
48+
/// // The `this` binding is different.
49+
/// foo.call(obj, 1, 2, 3);
50+
/// foo.apply(obj, [1, 2, 3]);
51+
/// obj.foo.call(null, 1, 2, 3);
52+
/// obj.foo.apply(null, [1, 2, 3]);
53+
/// obj.foo.call(otherObj, 1, 2, 3);
54+
/// obj.foo.apply(otherObj, [1, 2, 3]);
55+
///
56+
/// // The argument list is variadic.
57+
/// // Those are warned by the `prefer-spread` rule.
58+
/// foo.apply(undefined, args);
59+
/// foo.apply(null, args);
60+
/// obj.foo.apply(obj, args);
61+
/// ```
62+
NoUselessCall,
63+
eslint,
64+
perf
65+
);
66+
67+
impl Rule for NoUselessCall {
68+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
69+
let AstKind::CallExpression(call_expr) = node.kind() else { return };
70+
71+
let Some(callee) =
72+
as_member_expression_without_chain_expression(call_expr.callee.without_parentheses())
73+
else {
74+
return;
75+
};
76+
77+
if !is_call_or_non_variadic_apply(call_expr, callee) {
78+
return;
79+
}
80+
81+
let applied = callee.object().without_parentheses();
82+
let Some(first_arg) = call_expr.arguments.first() else { return };
83+
let Some(this_arg) = first_arg.as_expression() else { return };
84+
85+
if validate_this_argument(this_arg, applied) {
86+
ctx.diagnostic(no_useless_call_diagnostic(
87+
callee.static_property_name().unwrap(),
88+
call_expr.span,
89+
));
90+
}
91+
}
92+
}
93+
94+
fn validate_this_argument(this_arg: &Expression, applied: &Expression) -> bool {
95+
if this_arg.is_null_or_undefined() {
96+
return matches!(applied, Expression::Identifier(_));
97+
}
98+
99+
let Some(applied_member) = as_member_expression_without_chain_expression(applied) else {
100+
return false;
101+
};
102+
103+
validate_member_expression(this_arg, applied_member)
104+
}
105+
106+
fn validate_member_expression(this_arg: &Expression, applied_member: &MemberExpression) -> bool {
107+
let applied_object = applied_member.object().without_parentheses();
108+
109+
if let Some(this_arg_member) = as_member_expression_without_chain_expression(this_arg) {
110+
return as_member_expression_without_chain_expression(applied_object).is_some_and(
111+
|applied_object_member| applied_object_member.content_eq(this_arg_member),
112+
);
113+
}
114+
115+
applied_object.content_eq(this_arg)
116+
}
117+
118+
fn is_call_function(call_expr: &CallExpression, callee: &MemberExpression) -> bool {
119+
callee.static_property_name().is_some_and(|name| name == "call")
120+
&& !call_expr.arguments.is_empty()
121+
}
122+
123+
fn is_apply_function(call_expr: &CallExpression, callee: &MemberExpression) -> bool {
124+
callee.static_property_name().is_some_and(|name| name == "apply")
125+
&& call_expr.arguments.len() == 2
126+
&& call_expr
127+
.arguments
128+
.get(1)
129+
.and_then(|arg| arg.as_expression())
130+
.is_some_and(|expr| matches!(expr, Expression::ArrayExpression(_)))
131+
}
132+
133+
fn is_call_or_non_variadic_apply(call_expr: &CallExpression, callee: &MemberExpression) -> bool {
134+
!callee.is_computed()
135+
&& (is_call_function(call_expr, callee) || is_apply_function(call_expr, callee))
136+
}
137+
138+
fn as_member_expression_without_chain_expression<'a>(
139+
expr: &'a Expression,
140+
) -> Option<&'a MemberExpression<'a>> {
141+
match expr {
142+
Expression::ChainExpression(chain_expr) => {
143+
if let match_member_expression!(ChainElement) = chain_expr.expression {
144+
chain_expr.expression.as_member_expression()
145+
} else {
146+
None
147+
}
148+
}
149+
match_member_expression!(Expression) => expr.as_member_expression(),
150+
_ => None,
151+
}
152+
}
153+
154+
#[test]
155+
fn test() {
156+
use crate::tester::Tester;
157+
158+
let pass = vec![
159+
"foo.apply(obj, 1, 2);",
160+
"obj.foo.apply(null, 1, 2);",
161+
"obj.foo.apply(otherObj, 1, 2);",
162+
"a.b(x, y).c.foo.apply(a.b(x, z).c, 1, 2);",
163+
"foo.apply(obj, [1, 2]);",
164+
"obj.foo.apply(null, [1, 2]);",
165+
"obj.foo.apply(otherObj, [1, 2]);",
166+
"a.b(x, y).c.foo.apply(a.b(x, z).c, [1, 2]);",
167+
"a.b.foo.apply(a.b.c, [1, 2]);",
168+
"foo.apply(null, args);",
169+
"obj.foo.apply(obj, args);",
170+
"var call; foo[call](null, 1, 2);",
171+
"var apply; foo[apply](null, [1, 2]);",
172+
"foo.call();",
173+
"obj.foo.call();",
174+
"foo.apply();",
175+
"obj.foo.apply();",
176+
"obj?.foo.bar.call(obj.foo, 1, 2);", // { "ecmaVersion": 2020 },
177+
"class C { #call; wrap(foo) { foo.#call(undefined, 1, 2); } }", // { "ecmaVersion": 2022 }
178+
];
179+
180+
let fail = vec![
181+
"foo.call(undefined, 1, 2);",
182+
"foo.call(void 0, 1, 2);",
183+
"foo.call(null, 1, 2);",
184+
"obj.foo.call(obj, 1, 2);",
185+
"a.b.c.foo.call(a.b.c, 1, 2);",
186+
"a.b(x, y).c.foo.call(a.b(x, y).c, 1, 2);",
187+
"foo.apply(undefined, [1, 2]);",
188+
"foo.apply(void 0, [1, 2]);",
189+
"foo.apply(null, [1, 2]);",
190+
"obj.foo.apply(obj, [1, 2]);",
191+
"a.b.c.foo.apply(a.b.c, [1, 2]);",
192+
"a.b(x, y).c.foo.apply(a.b(x, y).c, [1, 2]);",
193+
"[].concat.apply([ ], [1, 2]);",
194+
"[].concat.apply([
195+
/*empty*/
196+
], [1, 2]);
197+
",
198+
r#"abc.get("foo", 0).concat.apply(abc . get("foo", 0 ), [1, 2]);"#,
199+
"foo.call?.(undefined, 1, 2);", // { "ecmaVersion": 2020 },
200+
"foo?.call(undefined, 1, 2);", // { "ecmaVersion": 2020 },
201+
"(foo?.call)(undefined, 1, 2);", // { "ecmaVersion": 2020 },
202+
"obj.foo.call?.(obj, 1, 2);", // { "ecmaVersion": 2020 },
203+
"obj?.foo.call(obj, 1, 2);", // { "ecmaVersion": 2020 },
204+
"(obj?.foo).call(obj, 1, 2);", // { "ecmaVersion": 2020 },
205+
"(obj?.foo.call)(obj, 1, 2);", // { "ecmaVersion": 2020 },
206+
"obj?.foo.bar.call(obj?.foo, 1, 2);", // { "ecmaVersion": 2020 },
207+
"(obj?.foo).bar.call(obj?.foo, 1, 2);", // { "ecmaVersion": 2020 },
208+
"obj.foo?.bar.call(obj.foo, 1, 2);", // { "ecmaVersion": 2020 }
209+
];
210+
211+
Tester::new(NoUselessCall::NAME, NoUselessCall::PLUGIN, pass, fail).test_and_snapshot();
212+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint(no-useless-call): Avoid unnecessary use of .call()
5+
╭─[no_useless_call.tsx:1:1]
6+
1foo.call(undefined, 1, 2);
7+
· ─────────────────────────
8+
╰────
9+
help: Replace with a normal function invocation
10+
11+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
12+
╭─[no_useless_call.tsx:1:1]
13+
1 │ foo.call(void 0, 1, 2);
14+
· ──────────────────────
15+
╰────
16+
help: Replace with a normal function invocation
17+
18+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
19+
╭─[no_useless_call.tsx:1:1]
20+
1 │ foo.call(null, 1, 2);
21+
· ────────────────────
22+
╰────
23+
help: Replace with a normal function invocation
24+
25+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
26+
╭─[no_useless_call.tsx:1:1]
27+
1 │ obj.foo.call(obj, 1, 2);
28+
· ───────────────────────
29+
╰────
30+
help: Replace with a normal function invocation
31+
32+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
33+
╭─[no_useless_call.tsx:1:1]
34+
1 │ a.b.c.foo.call(a.b.c, 1, 2);
35+
· ───────────────────────────
36+
╰────
37+
help: Replace with a normal function invocation
38+
39+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
40+
╭─[no_useless_call.tsx:1:1]
41+
1 │ a.b(x, y).c.foo.call(a.b(x, y).c, 1, 2);
42+
· ───────────────────────────────────────
43+
╰────
44+
help: Replace with a normal function invocation
45+
46+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
47+
╭─[no_useless_call.tsx:1:1]
48+
1 │ foo.apply(undefined, [1, 2]);
49+
· ────────────────────────────
50+
╰────
51+
help: Replace with a normal function invocation
52+
53+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
54+
╭─[no_useless_call.tsx:1:1]
55+
1 │ foo.apply(void 0, [1, 2]);
56+
· ─────────────────────────
57+
╰────
58+
help: Replace with a normal function invocation
59+
60+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
61+
╭─[no_useless_call.tsx:1:1]
62+
1 │ foo.apply(null, [1, 2]);
63+
· ───────────────────────
64+
╰────
65+
help: Replace with a normal function invocation
66+
67+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
68+
╭─[no_useless_call.tsx:1:1]
69+
1 │ obj.foo.apply(obj, [1, 2]);
70+
· ──────────────────────────
71+
╰────
72+
help: Replace with a normal function invocation
73+
74+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
75+
╭─[no_useless_call.tsx:1:1]
76+
1 │ a.b.c.foo.apply(a.b.c, [1, 2]);
77+
· ──────────────────────────────
78+
╰────
79+
help: Replace with a normal function invocation
80+
81+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
82+
╭─[no_useless_call.tsx:1:1]
83+
1 │ a.b(x, y).c.foo.apply(a.b(x, y).c, [1, 2]);
84+
· ──────────────────────────────────────────
85+
╰────
86+
help: Replace with a normal function invocation
87+
88+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
89+
╭─[no_useless_call.tsx:1:1]
90+
1 │ [].concat.apply([ ], [1, 2]);
91+
· ────────────────────────────
92+
╰────
93+
help: Replace with a normal function invocation
94+
95+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
96+
╭─[no_useless_call.tsx:1:1]
97+
1 │ ╭─▶ [].concat.apply([
98+
2 │ │ /*empty*/
99+
3 │ ╰─▶ ], [1, 2]);
100+
4
101+
╰────
102+
help: Replace with a normal function invocation
103+
104+
⚠ eslint(no-useless-call): Avoid unnecessary use of .apply()
105+
╭─[no_useless_call.tsx:1:1]
106+
1 │ abc.get("foo", 0).concat.apply(abc . get("foo", 0 ), [1, 2]);
107+
· ─────────────────────────────────────────────────────────────
108+
╰────
109+
help: Replace with a normal function invocation
110+
111+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
112+
╭─[no_useless_call.tsx:1:1]
113+
1 │ foo.call?.(undefined, 1, 2);
114+
· ───────────────────────────
115+
╰────
116+
help: Replace with a normal function invocation
117+
118+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
119+
╭─[no_useless_call.tsx:1:1]
120+
1 │ foo?.call(undefined, 1, 2);
121+
· ──────────────────────────
122+
╰────
123+
help: Replace with a normal function invocation
124+
125+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
126+
╭─[no_useless_call.tsx:1:1]
127+
1 │ (foo?.call)(undefined, 1, 2);
128+
· ────────────────────────────
129+
╰────
130+
help: Replace with a normal function invocation
131+
132+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
133+
╭─[no_useless_call.tsx:1:1]
134+
1 │ obj.foo.call?.(obj, 1, 2);
135+
· ─────────────────────────
136+
╰────
137+
help: Replace with a normal function invocation
138+
139+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
140+
╭─[no_useless_call.tsx:1:1]
141+
1 │ obj?.foo.call(obj, 1, 2);
142+
· ────────────────────────
143+
╰────
144+
help: Replace with a normal function invocation
145+
146+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
147+
╭─[no_useless_call.tsx:1:1]
148+
1 │ (obj?.foo).call(obj, 1, 2);
149+
· ──────────────────────────
150+
╰────
151+
help: Replace with a normal function invocation
152+
153+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
154+
╭─[no_useless_call.tsx:1:1]
155+
1 │ (obj?.foo.call)(obj, 1, 2);
156+
· ──────────────────────────
157+
╰────
158+
help: Replace with a normal function invocation
159+
160+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
161+
╭─[no_useless_call.tsx:1:1]
162+
1 │ obj?.foo.bar.call(obj?.foo, 1, 2);
163+
· ─────────────────────────────────
164+
╰────
165+
help: Replace with a normal function invocation
166+
167+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
168+
╭─[no_useless_call.tsx:1:1]
169+
1 │ (obj?.foo).bar.call(obj?.foo, 1, 2);
170+
· ───────────────────────────────────
171+
╰────
172+
help: Replace with a normal function invocation
173+
174+
⚠ eslint(no-useless-call): Avoid unnecessary use of .call()
175+
╭─[no_useless_call.tsx:1:1]
176+
1 │ obj.foo?.bar.call(obj.foo, 1, 2);
177+
· ────────────────────────────────
178+
╰────
179+
help: Replace with a normal function invocation

0 commit comments

Comments
 (0)
Please sign in to comment.