-
-
Notifications
You must be signed in to change notification settings - Fork 836
feat(linter): add eslint/no-useless-computed-key rule
#13428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
camc314
merged 10 commits into
oxc-project:main
from
huangtiandi1999:feat/linter/no-useless-computed
Sep 12, 2025
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b165b94
implement
huangtiandi1999 7fa5ebd
fix
huangtiandi1999 d7a68d1
fix
huangtiandi1999 63df709
[autofix.ci] apply automated fixes
autofix-ci[bot] 4c482c1
Merge remote-tracking branch 'origin/main' into feat/linter/no-useles…
huangtiandi1999 932ad51
fix
huangtiandi1999 0f59675
update snapshots
huangtiandi1999 fffee39
Merge branch 'main' into feat/linter/no-useless-computed
camc314 b2d1d9c
update runner impls
camc314 7e5a2b6
fix
camc314 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
354 changes: 354 additions & 0 deletions
354
crates/oxc_linter/src/rules/eslint/no_useless_computed_key.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,354 @@ | ||
| use oxc_ast::{AstKind, ast::Expression}; | ||
| use oxc_diagnostics::OxcDiagnostic; | ||
| use oxc_macros::declare_oxc_lint; | ||
| use oxc_span::{Atom, GetSpan, Span}; | ||
| use serde_json::Value; | ||
|
|
||
| use crate::{AstNode, context::LintContext, rule::Rule}; | ||
|
|
||
| fn no_useless_computed_key_diagnostic(span: Span, raw: Option<Atom>) -> OxcDiagnostic { | ||
| // false positive, if we remove the closure, `borrowed data escapes outside of function `raw` escapes the function body here` | ||
| #[expect(clippy::redundant_closure)] | ||
| let key = raw.unwrap_or_else(|| Atom::empty()); | ||
| OxcDiagnostic::warn(format!("Unnecessarily computed property `{key}` found.")) | ||
| .with_help("Replace the computed property with a plain identifier or string literal") | ||
| .with_label(span) | ||
| } | ||
|
|
||
| #[derive(Debug, Clone)] | ||
| pub struct NoUselessComputedKey { | ||
| enforce_for_class_members: bool, | ||
| } | ||
|
|
||
| impl Default for NoUselessComputedKey { | ||
| fn default() -> Self { | ||
| Self { enforce_for_class_members: true } | ||
| } | ||
| } | ||
|
|
||
| declare_oxc_lint!( | ||
| /// ### What it does | ||
| /// | ||
| /// Disallow unnecessary computed property keys in objects and classes | ||
| /// | ||
| /// ### Why is this bad? | ||
| /// | ||
| /// It’s unnecessary to use computed properties with literals such as: | ||
| /// ```js | ||
| /// const foo = {["a"]: "b"}; | ||
| /// ``` | ||
| /// | ||
| /// The code can be rewritten as: | ||
| /// ```js | ||
| /// const foo = {"a": "b"}; | ||
| /// ``` | ||
| /// | ||
| /// ### Examples | ||
| /// | ||
| /// Examples of **incorrect** code for this rule: | ||
| /// ```js | ||
| /// const a = { ['0']: 0 }; | ||
| /// const b = { ['0+1,234']: 0 }; | ||
| /// const c = { [0]: 0 }; | ||
| /// const e = { ['x']() {} }; | ||
| /// | ||
| /// class Foo { | ||
| /// ["foo"] = "bar"; | ||
| /// [0]() {} | ||
| /// static ["foo"] = "bar"; | ||
| /// get ['b']() {} | ||
| /// set ['c'](value) {} | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// Examples of **correct** code for this rule: | ||
| /// ```js | ||
| /// const a = { 'a': 0 }; | ||
| /// const b = { 0: 0 }; | ||
| /// const c = { x() {} }; | ||
| /// const e = { '0+1,234': 0 }; | ||
| /// | ||
| /// class Foo { | ||
| /// "foo" = "bar"; | ||
| /// 0() {} | ||
| /// 'a'() {} | ||
| /// static "foo" = "bar"; | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// Examples of additional **correct** code for this rule: | ||
| /// ```js | ||
| /// | ||
| /// const c = { | ||
| /// "__proto__": foo, // defines object's prototype | ||
| /// ["__proto__"]: bar // defines a property named "__proto__" | ||
| /// }; | ||
| /// class Foo { | ||
| /// ["constructor"]; // instance field named "constructor" | ||
| /// "constructor"() {} // the constructor of this class | ||
| /// static ["constructor"]; // static field named "constructor" | ||
| /// static ["prototype"]; // runtime error, it would be a parsing error without `[]` | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// ### Options | ||
| /// | ||
| /// #### enforceForClassMembers | ||
| /// | ||
| /// `{ type: boolean, default: true }` | ||
| /// | ||
| /// The `enforceForClassMembers` option controls whether the rule applies to | ||
| /// class members (methods and properties). | ||
| /// | ||
| /// Examples of **correct** code for this rule with the `{ "enforceForClassMembers": false }` option: | ||
| /// ```js | ||
| /// class SomeClass { | ||
| /// ["foo"] = "bar"; | ||
| /// [42] = "baz"; | ||
| /// get ['b']() {} | ||
| /// set ['c'](value) {} | ||
| /// static ["foo"] = "bar"; | ||
| /// } | ||
| /// ``` | ||
| NoUselessComputedKey, | ||
| eslint, | ||
| style, | ||
| pending | ||
| ); | ||
|
|
||
| impl Rule for NoUselessComputedKey { | ||
| fn from_configuration(value: Value) -> Self { | ||
| let obj = value.get(0); | ||
| Self { | ||
| enforce_for_class_members: obj | ||
| .and_then(|v| v.get("enforceForClassMembers")) | ||
| .and_then(Value::as_bool) | ||
| .unwrap_or(true), | ||
| } | ||
| } | ||
| fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { | ||
| match node.kind() { | ||
| AstKind::ObjectProperty(property) if property.computed => { | ||
| if let Some(expr) = | ||
| property.key.as_expression().map(Expression::get_inner_expression) | ||
| { | ||
| check_computed_class_member( | ||
| ctx, | ||
| property.key.span(), | ||
| expr, | ||
| false, | ||
| &[], | ||
| &["__proto__"], | ||
| ); | ||
| } | ||
| } | ||
| AstKind::BindingProperty(binding_prop) if binding_prop.computed => { | ||
| if let Some(expr) = | ||
| binding_prop.key.as_expression().map(Expression::get_inner_expression) | ||
| { | ||
| check_computed_class_member(ctx, binding_prop.span, expr, false, &[], &[]); | ||
| } | ||
| } | ||
| AstKind::PropertyDefinition(prop_def) | ||
| if self.enforce_for_class_members && prop_def.computed => | ||
| { | ||
| if let Some(expr) = | ||
| prop_def.key.as_expression().map(Expression::get_inner_expression) | ||
| { | ||
| check_computed_class_member( | ||
| ctx, | ||
| prop_def.key.span(), | ||
| expr, | ||
| prop_def.r#static, | ||
| &["prototype", "constructor"], | ||
| &["constructor"], | ||
| ); | ||
| } | ||
| } | ||
| AstKind::MethodDefinition(method_def) | ||
| if self.enforce_for_class_members && method_def.computed => | ||
| { | ||
| if let Some(expr) = | ||
| method_def.key.as_expression().map(Expression::get_inner_expression) | ||
| { | ||
| check_computed_class_member( | ||
| ctx, | ||
| method_def.span, | ||
| expr, | ||
| method_def.r#static, | ||
| &["prototype"], | ||
| &["constructor"], | ||
| ); | ||
| } | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn check_computed_class_member( | ||
| ctx: &LintContext<'_>, | ||
| span: Span, | ||
| expr: &Expression, | ||
| is_static: bool, | ||
| allow_static: &[&str], | ||
| allow_non_static: &[&str], | ||
| ) { | ||
| match expr { | ||
| Expression::StringLiteral(lit) => { | ||
| let key_name = lit.value.as_str(); | ||
| let allowed = if is_static { | ||
| allow_static.contains(&key_name) | ||
| } else { | ||
| allow_non_static.contains(&key_name) | ||
| }; | ||
| if !allowed { | ||
| ctx.diagnostic(no_useless_computed_key_diagnostic(span, lit.raw)); | ||
| } | ||
| } | ||
| Expression::NumericLiteral(number_lit) => { | ||
| ctx.diagnostic(no_useless_computed_key_diagnostic(span, number_lit.raw)); | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test() { | ||
| use crate::tester::Tester; | ||
|
|
||
| let pass = vec![ | ||
| ("({ 'a': 0, b(){} })", None), | ||
| ("({ [x]: 0 });", None), | ||
| ("({ a: 0, [b](){} })", None), | ||
| ("({ ['__proto__']: [] })", None), | ||
| ("var { 'a': foo } = obj", None), | ||
| ("var { [a]: b } = obj;", None), | ||
| ("var { a } = obj;", None), | ||
| ("var { a: a } = obj;", None), | ||
| ("var { a: b } = obj;", None), | ||
| ("class Foo { a() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))), | ||
| ("class Foo { 'a'() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))), | ||
| ("class Foo { [x]() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))), | ||
| ( | ||
| "class Foo { ['constructor']() {} }", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": true }])), | ||
| ), | ||
| ( | ||
| "class Foo { static ['prototype']() {} }", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": true }])), | ||
| ), | ||
| ("(class { 'a'() {} })", Some(serde_json::json!([{ "enforceForClassMembers": true }]))), | ||
| ("(class { [x]() {} })", Some(serde_json::json!([{ "enforceForClassMembers": true }]))), | ||
| ( | ||
| "(class { ['constructor']() {} })", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": true }])), | ||
| ), | ||
| ( | ||
| "(class { static ['prototype']() {} })", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": true }])), | ||
| ), | ||
| ("class Foo { 'x'() {} }", None), | ||
| ("(class { [x]() {} })", None), | ||
| ("class Foo { static constructor() {} }", None), | ||
| ("class Foo { prototype() {} }", None), | ||
| ( | ||
| "class Foo { ['x']() {} }", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": false }])), | ||
| ), | ||
| ("(class { ['x']() {} })", Some(serde_json::json!([{ "enforceForClassMembers": false }]))), | ||
| ( | ||
| "class Foo { static ['constructor']() {} }", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": false }])), | ||
| ), | ||
| ( | ||
| "class Foo { ['prototype']() {} }", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": false }])), | ||
| ), | ||
| ("class Foo { a }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))), | ||
| ( | ||
| "class Foo { ['constructor'] }", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": true }])), | ||
| ), | ||
| ( | ||
| "class Foo { static ['constructor'] }", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": true }])), | ||
| ), | ||
| ( | ||
| "class Foo { static ['prototype'] }", | ||
| Some(serde_json::json!([{ "enforceForClassMembers": true }])), | ||
| ), | ||
| ("({ [99999999999999999n]: 0 })", None), // { "ecmaVersion": 2020 } | ||
| ]; | ||
|
|
||
| let fail = vec![ | ||
| ("({ ['0']: 0 })", None), | ||
| ("var { ['0']: a } = obj", None), | ||
| ("({ ['0+1,234']: 0 })", None), | ||
| ("({ [0]: 0 })", None), | ||
| ("var { [0]: a } = obj", None), | ||
| ("({ ['x']: 0 })", None), | ||
| ("var { ['x']: a } = obj", None), | ||
| ("var { ['__proto__']: a } = obj", None), | ||
| ("({ ['x']() {} })", None), | ||
| ("({ [/* this comment prevents a fix */ 'x']: 0 })", None), | ||
| ("({ ['x' /* this comment also prevents a fix */]: 0 })", None), | ||
| ("({ [('x')]: 0 })", None), | ||
| ("var { [('x')]: a } = obj", None), | ||
| ("({ *['x']() {} })", None), | ||
| ("({ async ['x']() {} })", None), // { "ecmaVersion": 8 }, | ||
| ("({ get[.2]() {} })", None), | ||
| ("({ set[.2](value) {} })", None), | ||
| ("({ async[.2]() {} })", None), // { "ecmaVersion": 8 }, | ||
| ("({ [2]() {} })", None), | ||
| ("({ get [2]() {} })", None), | ||
| ("({ set [2](value) {} })", None), | ||
| ("({ async [2]() {} })", None), // { "ecmaVersion": 8 }, | ||
| ("({ get[2]() {} })", None), | ||
| ("({ set[2](value) {} })", None), | ||
| ("({ async[2]() {} })", None), // { "ecmaVersion": 8 }, | ||
| ("({ get['foo']() {} })", None), | ||
| ("({ *[2]() {} })", None), | ||
| ("({ async*[2]() {} })", None), | ||
| ("({ ['constructor']: 1 })", None), | ||
| ("({ ['prototype']: 1 })", None), | ||
| ("class Foo { ['0']() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))), | ||
| ("class Foo { ['0+1,234']() {} }", Some(serde_json::json!([{}]))), | ||
| ("class Foo { ['x']() {} }", Some(serde_json::json!([{ "enforceForClassMembers": true }]))), | ||
| ("class Foo { [/* this comment prevents a fix */ 'x']() {} }", None), | ||
| ("class Foo { ['x' /* this comment also prevents a fix */]() {} }", None), | ||
| ("class Foo { [('x')]() {} }", None), | ||
| ("class Foo { *['x']() {} }", None), | ||
| ("class Foo { async ['x']() {} }", None), // { "ecmaVersion": 8 }, | ||
| ("class Foo { get[.2]() {} }", None), | ||
| ("class Foo { set[.2](value) {} }", None), | ||
| ("class Foo { async[.2]() {} }", None), // { "ecmaVersion": 8 }, | ||
| ("class Foo { [2]() {} }", None), | ||
| ("class Foo { get [2]() {} }", None), | ||
| ("class Foo { set [2](value) {} }", None), | ||
| ("class Foo { async [2]() {} }", None), // { "ecmaVersion": 8 }, | ||
| ("class Foo { get[2]() {} }", None), | ||
| ("class Foo { set[2](value) {} }", None), | ||
| ("class Foo { async[2]() {} }", None), // { "ecmaVersion": 8 }, | ||
| ("class Foo { get['foo']() {} }", None), | ||
| ("class Foo { *[2]() {} }", None), | ||
| ("class Foo { async*[2]() {} }", None), | ||
| ("class Foo { static ['constructor']() {} }", None), | ||
| ("class Foo { ['prototype']() {} }", None), | ||
| ("(class { ['x']() {} })", None), | ||
| ("(class { ['__proto__']() {} })", None), | ||
| ("(class { static ['__proto__']() {} })", None), | ||
| ("(class { static ['constructor']() {} })", None), | ||
| ("(class { ['prototype']() {} })", None), | ||
| ("class Foo { ['0'] }", None), | ||
| ("class Foo { ['0'] = 0 }", None), | ||
| ("class Foo { static[0] }", None), | ||
| ("class Foo { ['#foo'] }", None), | ||
| ("(class { ['__proto__'] })", None), | ||
| ("(class { static ['__proto__'] })", None), | ||
| ("(class { ['prototype'] })", None), | ||
| ]; | ||
|
|
||
| Tester::new(NoUselessComputedKey::NAME, NoUselessComputedKey::PLUGIN, pass, fail) | ||
| .test_and_snapshot(); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.