Skip to content

Commit 2751193

Browse files
huangtiandi1999autofix-ci[bot]camc314
authored
feat(linter): add eslint/no-useless-computed-key rule (#13428)
Rule detail: https://eslint.org/docs/latest/rules/no-useless-computed-key#rule-details --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Cameron Clark <cameron.clark@hey.com>
1 parent d13b5b2 commit 2751193

File tree

4 files changed

+817
-0
lines changed

4 files changed

+817
-0
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,10 @@ impl RuleRunner for crate::rules::eslint::no_useless_catch::NoUselessCatch {
546546
Some(&AstTypesBitset::from_types(&[AstType::TryStatement]));
547547
}
548548

549+
impl RuleRunner for crate::rules::eslint::no_useless_computed_key::NoUselessComputedKey {
550+
const NODE_TYPES: Option<&AstTypesBitset> = None;
551+
}
552+
549553
impl RuleRunner for crate::rules::eslint::no_useless_concat::NoUselessConcat {
550554
const NODE_TYPES: Option<&AstTypesBitset> =
551555
Some(&AstTypesBitset::from_types(&[AstType::BinaryExpression]));

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ pub(crate) mod eslint {
159159
pub mod no_useless_backreference;
160160
pub mod no_useless_call;
161161
pub mod no_useless_catch;
162+
pub mod no_useless_computed_key;
162163
pub mod no_useless_concat;
163164
pub mod no_useless_constructor;
164165
pub mod no_useless_escape;
@@ -655,6 +656,7 @@ oxc_macros::declare_all_lint_rules! {
655656
eslint::max_nested_callbacks,
656657
eslint::max_params,
657658
eslint::new_cap,
659+
eslint::no_useless_computed_key,
658660
eslint::no_unassigned_vars,
659661
eslint::no_extra_bind,
660662
eslint::no_alert,
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
use oxc_ast::{AstKind, ast::Expression};
2+
use oxc_diagnostics::OxcDiagnostic;
3+
use oxc_macros::declare_oxc_lint;
4+
use oxc_span::{Atom, GetSpan, Span};
5+
use serde_json::Value;
6+
7+
use crate::{AstNode, context::LintContext, rule::Rule};
8+
9+
fn no_useless_computed_key_diagnostic(span: Span, raw: Option<Atom>) -> OxcDiagnostic {
10+
// false positive, if we remove the closure, `borrowed data escapes outside of function `raw` escapes the function body here`
11+
#[expect(clippy::redundant_closure)]
12+
let key = raw.unwrap_or_else(|| Atom::empty());
13+
OxcDiagnostic::warn(format!("Unnecessarily computed property `{key}` found."))
14+
.with_help("Replace the computed property with a plain identifier or string literal")
15+
.with_label(span)
16+
}
17+
18+
#[derive(Debug, Clone)]
19+
pub struct NoUselessComputedKey {
20+
enforce_for_class_members: bool,
21+
}
22+
23+
impl Default for NoUselessComputedKey {
24+
fn default() -> Self {
25+
Self { enforce_for_class_members: true }
26+
}
27+
}
28+
29+
declare_oxc_lint!(
30+
/// ### What it does
31+
///
32+
/// Disallow unnecessary computed property keys in objects and classes
33+
///
34+
/// ### Why is this bad?
35+
///
36+
/// It’s unnecessary to use computed properties with literals such as:
37+
/// ```js
38+
/// const foo = {["a"]: "b"};
39+
/// ```
40+
///
41+
/// The code can be rewritten as:
42+
/// ```js
43+
/// const foo = {"a": "b"};
44+
/// ```
45+
///
46+
/// ### Examples
47+
///
48+
/// Examples of **incorrect** code for this rule:
49+
/// ```js
50+
/// const a = { ['0']: 0 };
51+
/// const b = { ['0+1,234']: 0 };
52+
/// const c = { [0]: 0 };
53+
/// const e = { ['x']() {} };
54+
///
55+
/// class Foo {
56+
/// ["foo"] = "bar";
57+
/// [0]() {}
58+
/// static ["foo"] = "bar";
59+
/// get ['b']() {}
60+
/// set ['c'](value) {}
61+
/// }
62+
/// ```
63+
///
64+
/// Examples of **correct** code for this rule:
65+
/// ```js
66+
/// const a = { 'a': 0 };
67+
/// const b = { 0: 0 };
68+
/// const c = { x() {} };
69+
/// const e = { '0+1,234': 0 };
70+
///
71+
/// class Foo {
72+
/// "foo" = "bar";
73+
/// 0() {}
74+
/// 'a'() {}
75+
/// static "foo" = "bar";
76+
/// }
77+
/// ```
78+
///
79+
/// Examples of additional **correct** code for this rule:
80+
/// ```js
81+
///
82+
/// const c = {
83+
/// "__proto__": foo, // defines object's prototype
84+
/// ["__proto__"]: bar // defines a property named "__proto__"
85+
/// };
86+
/// class Foo {
87+
/// ["constructor"]; // instance field named "constructor"
88+
/// "constructor"() {} // the constructor of this class
89+
/// static ["constructor"]; // static field named "constructor"
90+
/// static ["prototype"]; // runtime error, it would be a parsing error without `[]`
91+
/// }
92+
/// ```
93+
///
94+
/// ### Options
95+
///
96+
/// #### enforceForClassMembers
97+
///
98+
/// `{ type: boolean, default: true }`
99+
///
100+
/// The `enforceForClassMembers` option controls whether the rule applies to
101+
/// class members (methods and properties).
102+
///
103+
/// Examples of **correct** code for this rule with the `{ "enforceForClassMembers": false }` option:
104+
/// ```js
105+
/// class SomeClass {
106+
/// ["foo"] = "bar";
107+
/// [42] = "baz";
108+
/// get ['b']() {}
109+
/// set ['c'](value) {}
110+
/// static ["foo"] = "bar";
111+
/// }
112+
/// ```
113+
NoUselessComputedKey,
114+
eslint,
115+
style,
116+
pending
117+
);
118+
119+
impl Rule for NoUselessComputedKey {
120+
fn from_configuration(value: Value) -> Self {
121+
let obj = value.get(0);
122+
Self {
123+
enforce_for_class_members: obj
124+
.and_then(|v| v.get("enforceForClassMembers"))
125+
.and_then(Value::as_bool)
126+
.unwrap_or(true),
127+
}
128+
}
129+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
130+
match node.kind() {
131+
AstKind::ObjectProperty(property) if property.computed => {
132+
if let Some(expr) =
133+
property.key.as_expression().map(Expression::get_inner_expression)
134+
{
135+
check_computed_class_member(
136+
ctx,
137+
property.key.span(),
138+
expr,
139+
false,
140+
&[],
141+
&["__proto__"],
142+
);
143+
}
144+
}
145+
AstKind::BindingProperty(binding_prop) if binding_prop.computed => {
146+
if let Some(expr) =
147+
binding_prop.key.as_expression().map(Expression::get_inner_expression)
148+
{
149+
check_computed_class_member(ctx, binding_prop.span, expr, false, &[], &[]);
150+
}
151+
}
152+
AstKind::PropertyDefinition(prop_def)
153+
if self.enforce_for_class_members && prop_def.computed =>
154+
{
155+
if let Some(expr) =
156+
prop_def.key.as_expression().map(Expression::get_inner_expression)
157+
{
158+
check_computed_class_member(
159+
ctx,
160+
prop_def.key.span(),
161+
expr,
162+
prop_def.r#static,
163+
&["prototype", "constructor"],
164+
&["constructor"],
165+
);
166+
}
167+
}
168+
AstKind::MethodDefinition(method_def)
169+
if self.enforce_for_class_members && method_def.computed =>
170+
{
171+
if let Some(expr) =
172+
method_def.key.as_expression().map(Expression::get_inner_expression)
173+
{
174+
check_computed_class_member(
175+
ctx,
176+
method_def.span,
177+
expr,
178+
method_def.r#static,
179+
&["prototype"],
180+
&["constructor"],
181+
);
182+
}
183+
}
184+
_ => {}
185+
}
186+
}
187+
}
188+
189+
fn check_computed_class_member(
190+
ctx: &LintContext<'_>,
191+
span: Span,
192+
expr: &Expression,
193+
is_static: bool,
194+
allow_static: &[&str],
195+
allow_non_static: &[&str],
196+
) {
197+
match expr {
198+
Expression::StringLiteral(lit) => {
199+
let key_name = lit.value.as_str();
200+
let allowed = if is_static {
201+
allow_static.contains(&key_name)
202+
} else {
203+
allow_non_static.contains(&key_name)
204+
};
205+
if !allowed {
206+
ctx.diagnostic(no_useless_computed_key_diagnostic(span, lit.raw));
207+
}
208+
}
209+
Expression::NumericLiteral(number_lit) => {
210+
ctx.diagnostic(no_useless_computed_key_diagnostic(span, number_lit.raw));
211+
}
212+
_ => {}
213+
}
214+
}
215+
216+
#[test]
217+
fn test() {
218+
use crate::tester::Tester;
219+
220+
let pass = vec![
221+
("({ 'a': 0, b(){} })", None),
222+
("({ [x]: 0 });", None),
223+
("({ a: 0, [b](){} })", None),
224+
("({ ['__proto__']: [] })", None),
225+
("var { 'a': foo } = obj", None),
226+
("var { [a]: b } = obj;", None),
227+
("var { a } = obj;", None),
228+
("var { a: a } = obj;", None),
229+
("var { a: b } = obj;", None),
230+
("class Foo { a() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
231+
("class Foo { 'a'() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
232+
("class Foo { [x]() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
233+
(
234+
"class Foo { ['constructor']() {} }",
235+
Some(serde_json::json!([{ "enforceForClassMembers": true }])),
236+
),
237+
(
238+
"class Foo { static ['prototype']() {} }",
239+
Some(serde_json::json!([{ "enforceForClassMembers": true }])),
240+
),
241+
("(class { 'a'() {} })", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
242+
("(class { [x]() {} })", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
243+
(
244+
"(class { ['constructor']() {} })",
245+
Some(serde_json::json!([{ "enforceForClassMembers": true }])),
246+
),
247+
(
248+
"(class { static ['prototype']() {} })",
249+
Some(serde_json::json!([{ "enforceForClassMembers": true }])),
250+
),
251+
("class Foo { 'x'() {} }", None),
252+
("(class { [x]() {} })", None),
253+
("class Foo { static constructor() {} }", None),
254+
("class Foo { prototype() {} }", None),
255+
(
256+
"class Foo { ['x']() {} }",
257+
Some(serde_json::json!([{ "enforceForClassMembers": false }])),
258+
),
259+
("(class { ['x']() {} })", Some(serde_json::json!([{ "enforceForClassMembers": false }]))),
260+
(
261+
"class Foo { static ['constructor']() {} }",
262+
Some(serde_json::json!([{ "enforceForClassMembers": false }])),
263+
),
264+
(
265+
"class Foo { ['prototype']() {} }",
266+
Some(serde_json::json!([{ "enforceForClassMembers": false }])),
267+
),
268+
("class Foo { a }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
269+
(
270+
"class Foo { ['constructor'] }",
271+
Some(serde_json::json!([{ "enforceForClassMembers": true }])),
272+
),
273+
(
274+
"class Foo { static ['constructor'] }",
275+
Some(serde_json::json!([{ "enforceForClassMembers": true }])),
276+
),
277+
(
278+
"class Foo { static ['prototype'] }",
279+
Some(serde_json::json!([{ "enforceForClassMembers": true }])),
280+
),
281+
("({ [99999999999999999n]: 0 })", None), // { "ecmaVersion": 2020 }
282+
];
283+
284+
let fail = vec![
285+
("({ ['0']: 0 })", None),
286+
("var { ['0']: a } = obj", None),
287+
("({ ['0+1,234']: 0 })", None),
288+
("({ [0]: 0 })", None),
289+
("var { [0]: a } = obj", None),
290+
("({ ['x']: 0 })", None),
291+
("var { ['x']: a } = obj", None),
292+
("var { ['__proto__']: a } = obj", None),
293+
("({ ['x']() {} })", None),
294+
("({ [/* this comment prevents a fix */ 'x']: 0 })", None),
295+
("({ ['x' /* this comment also prevents a fix */]: 0 })", None),
296+
("({ [('x')]: 0 })", None),
297+
("var { [('x')]: a } = obj", None),
298+
("({ *['x']() {} })", None),
299+
("({ async ['x']() {} })", None), // { "ecmaVersion": 8 },
300+
("({ get[.2]() {} })", None),
301+
("({ set[.2](value) {} })", None),
302+
("({ async[.2]() {} })", None), // { "ecmaVersion": 8 },
303+
("({ [2]() {} })", None),
304+
("({ get [2]() {} })", None),
305+
("({ set [2](value) {} })", None),
306+
("({ async [2]() {} })", None), // { "ecmaVersion": 8 },
307+
("({ get[2]() {} })", None),
308+
("({ set[2](value) {} })", None),
309+
("({ async[2]() {} })", None), // { "ecmaVersion": 8 },
310+
("({ get['foo']() {} })", None),
311+
("({ *[2]() {} })", None),
312+
("({ async*[2]() {} })", None),
313+
("({ ['constructor']: 1 })", None),
314+
("({ ['prototype']: 1 })", None),
315+
("class Foo { ['0']() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
316+
("class Foo { ['0+1,234']() {} }", Some(serde_json::json!([{}]))),
317+
("class Foo { ['x']() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))),
318+
("class Foo { [/* this comment prevents a fix */ 'x']() {} }", None),
319+
("class Foo { ['x' /* this comment also prevents a fix */]() {} }", None),
320+
("class Foo { [('x')]() {} }", None),
321+
("class Foo { *['x']() {} }", None),
322+
("class Foo { async ['x']() {} }", None), // { "ecmaVersion": 8 },
323+
("class Foo { get[.2]() {} }", None),
324+
("class Foo { set[.2](value) {} }", None),
325+
("class Foo { async[.2]() {} }", None), // { "ecmaVersion": 8 },
326+
("class Foo { [2]() {} }", None),
327+
("class Foo { get [2]() {} }", None),
328+
("class Foo { set [2](value) {} }", None),
329+
("class Foo { async [2]() {} }", None), // { "ecmaVersion": 8 },
330+
("class Foo { get[2]() {} }", None),
331+
("class Foo { set[2](value) {} }", None),
332+
("class Foo { async[2]() {} }", None), // { "ecmaVersion": 8 },
333+
("class Foo { get['foo']() {} }", None),
334+
("class Foo { *[2]() {} }", None),
335+
("class Foo { async*[2]() {} }", None),
336+
("class Foo { static ['constructor']() {} }", None),
337+
("class Foo { ['prototype']() {} }", None),
338+
("(class { ['x']() {} })", None),
339+
("(class { ['__proto__']() {} })", None),
340+
("(class { static ['__proto__']() {} })", None),
341+
("(class { static ['constructor']() {} })", None),
342+
("(class { ['prototype']() {} })", None),
343+
("class Foo { ['0'] }", None),
344+
("class Foo { ['0'] = 0 }", None),
345+
("class Foo { static[0] }", None),
346+
("class Foo { ['#foo'] }", None),
347+
("(class { ['__proto__'] })", None),
348+
("(class { static ['__proto__'] })", None),
349+
("(class { ['prototype'] })", None),
350+
];
351+
352+
Tester::new(NoUselessComputedKey::NAME, NoUselessComputedKey::PLUGIN, pass, fail)
353+
.test_and_snapshot();
354+
}

0 commit comments

Comments
 (0)