-
-
Notifications
You must be signed in to change notification settings - Fork 794
feat(lint): implement useDestructuring
#8335
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
+1,188
−45
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6e9bf60
feat(lint): implement
dibashthapa 4c66b2c
chores(lint): added changeset
dibashthapa 80661e5
chores: fixed merge conflicts
dibashthapa c4a938c
chores: minor changes in changeset
dibashthapa d622468
chores: minor changes and restored quick test
dibashthapa 7718191
chores(lint): fixed clippy errors
dibashthapa 06a176c
chores(lint): fixed merge conflicts
dibashthapa 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| '@biomejs/biome': patch | ||
| --- | ||
|
|
||
| Added the new nursery rule [`useDestructuring`](https://biomejs.dev/linter/rules/use-destructuring). This rule helps to encourage destructuring from arrays and objects. | ||
|
|
||
| For example, the following code triggers because the variable name `x` matches the property `foo.x`, making it ideal for object destructuring syntax. | ||
|
|
||
| ```js | ||
| var x = foo.x; | ||
| ``` |
12 changes: 12 additions & 0 deletions
12
crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
109 changes: 65 additions & 44 deletions
109
crates/biome_configuration/src/analyzer/linter/rules.rs
Large diffs are not rendered by default.
Oops, something went wrong.
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
195 changes: 195 additions & 0 deletions
195
crates/biome_js_analyze/src/lint/nursery/use_destructuring.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,195 @@ | ||
| use biome_analyze::{ | ||
| Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, | ||
| }; | ||
| use biome_console::markup; | ||
| use biome_js_syntax::{ | ||
| AnyJsAssignment, AnyJsAssignmentPattern, AnyJsBinding, AnyJsBindingPattern, AnyJsExpression, | ||
| AnyJsLiteralExpression, AnyJsName, JsAssignmentExpression, JsAssignmentOperator, | ||
| JsVariableDeclaration, JsVariableDeclarator, | ||
| }; | ||
| use biome_rowan::{AstNode, declare_node_union}; | ||
| use biome_rule_options::use_destructuring::UseDestructuringOptions; | ||
|
|
||
| declare_lint_rule! { | ||
| /// Require destructuring from arrays and/or objects | ||
| /// | ||
| /// With JavaScript ES6, a new syntax was added for creating variables from an array index or object property, | ||
| /// called destructuring. This rule enforces usage of destructuring instead of accessing a property through a member expression. | ||
| /// | ||
| /// ## Examples | ||
| /// | ||
| /// ### Invalid | ||
| /// | ||
| /// ```js,expect_diagnostic | ||
| /// var foo = array[0]; | ||
| /// ``` | ||
| /// | ||
| /// ```js,expect_diagnostic | ||
| /// var bar = foo.bar; | ||
| /// ``` | ||
| /// | ||
| /// | ||
| /// ### Valid | ||
| /// | ||
| /// ```js | ||
| /// var [foo] = array; | ||
| /// ``` | ||
| /// | ||
| /// ```js | ||
| /// var { bar } = foo; | ||
| /// ``` | ||
| /// | ||
| pub UseDestructuring { | ||
| version: "next", | ||
| name: "useDestructuring", | ||
| language: "js", | ||
| recommended: false, | ||
| sources: &[RuleSource::Eslint("prefer-destructuring").same()], | ||
| } | ||
| } | ||
|
|
||
| impl Rule for UseDestructuring { | ||
| type Query = Ast<UseDestructuringQuery>; | ||
| type State = UseDestructuringState; | ||
| type Signals = Option<Self::State>; | ||
| type Options = UseDestructuringOptions; | ||
|
|
||
| fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
| let query = ctx.query(); | ||
|
|
||
| match query { | ||
| UseDestructuringQuery::JsAssignmentExpression(node) => { | ||
| let op = node.operator().ok()?; | ||
| if op != JsAssignmentOperator::Assign { | ||
| return None; | ||
| } | ||
| let left = node.left().ok()?; | ||
| let right = node.right().ok()?; | ||
|
|
||
| if let AnyJsAssignmentPattern::AnyJsAssignment( | ||
| AnyJsAssignment::JsIdentifierAssignment(expr), | ||
| ) = left | ||
| { | ||
| let ident = expr.name_token().ok()?; | ||
| return should_suggest_destructuring(ident.text_trimmed(), &right); | ||
| } | ||
|
|
||
| None | ||
| } | ||
| UseDestructuringQuery::JsVariableDeclarator(node) => { | ||
| let initializer = node.initializer()?; | ||
| let declaration = JsVariableDeclaration::cast(node.syntax().parent()?.parent()?)?; | ||
| let has_await_using = declaration.await_token().is_some(); | ||
| if declaration.kind().ok()?.text_trimmed() == "using" || has_await_using { | ||
| return None; | ||
| } | ||
|
|
||
| let left = node.id().ok()?; | ||
| let right = initializer.expression().ok()?; | ||
|
|
||
| if let AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding(expr)) = | ||
| left | ||
| { | ||
| let ident = expr.name_token().ok()?; | ||
| return should_suggest_destructuring(ident.text_trimmed(), &right); | ||
| } | ||
|
|
||
| None | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { | ||
| let node = ctx.query(); | ||
| match state { | ||
| UseDestructuringState::Array => { | ||
| Some( | ||
| RuleDiagnostic::new( | ||
| rule_category!(), | ||
| node.range(), | ||
| markup! { | ||
| "Use array destructuring instead of accessing array elements by index." | ||
| }, | ||
| ) | ||
| .note(markup! { | ||
| "Array destructuring is more readable and expressive than accessing individual elements by index." | ||
| }) | ||
| .note(markup! { | ||
| "Replace the array index access with array destructuring syntax." | ||
| }), | ||
| ) | ||
| } | ||
| UseDestructuringState::Object => { | ||
| Some( | ||
| RuleDiagnostic::new( | ||
| rule_category!(), | ||
| node.range(), | ||
| markup! { | ||
| "Use object destructuring instead of accessing object properties." | ||
| }, | ||
| ) | ||
| .note(markup! { | ||
| "Object destructuring is more readable and expressive than accessing individual properties." | ||
| }) | ||
| .note(markup! { | ||
| "Replace the property access with object destructuring syntax." | ||
| }), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| declare_node_union! { | ||
| pub UseDestructuringQuery = JsVariableDeclarator | JsAssignmentExpression | ||
| } | ||
|
|
||
| fn should_suggest_destructuring( | ||
| left: &str, | ||
| right: &AnyJsExpression, | ||
| ) -> Option<UseDestructuringState> { | ||
| match right { | ||
| AnyJsExpression::JsComputedMemberExpression(expr) => { | ||
| if expr.is_optional_chain() { | ||
| return None; | ||
| } | ||
|
|
||
| let member = expr.member().ok()?; | ||
| if let AnyJsExpression::AnyJsLiteralExpression(expr) = member { | ||
| if matches!(expr, AnyJsLiteralExpression::JsNumberLiteralExpression(_)) { | ||
| return Some(UseDestructuringState::Array); | ||
| } | ||
|
|
||
| let value = expr.value_token().ok()?; | ||
|
|
||
| if left == value.text_trimmed() { | ||
| return Some(UseDestructuringState::Object); | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
| AnyJsExpression::JsStaticMemberExpression(expr) => { | ||
| if matches!(expr.member().ok()?, AnyJsName::JsPrivateName(_)) | ||
| || matches!(expr.object().ok()?, AnyJsExpression::JsSuperExpression(_)) | ||
| { | ||
| return None; | ||
| } | ||
|
|
||
| if expr.is_optional_chain() { | ||
| return None; | ||
| } | ||
| let member = expr.member().ok()?.value_token().ok()?; | ||
| if left == member.text_trimmed() { | ||
| return Some(UseDestructuringState::Object); | ||
| } | ||
| None | ||
| } | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
| pub enum UseDestructuringState { | ||
| Object, | ||
| Array, | ||
| } | ||
87 changes: 87 additions & 0 deletions
87
crates/biome_js_analyze/tests/specs/nursery/useDestructuring/invalid.js
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,87 @@ | ||
| /* should generate diagnostics */ | ||
| { | ||
| var foo = array[0]; | ||
| } | ||
| { | ||
| foo = array[0]; | ||
| } | ||
| { | ||
| var foo = object.foo; | ||
| } | ||
| { | ||
| var foo = (a, b).foo; | ||
| } | ||
| { | ||
| var length = (() => {}).length; | ||
| } | ||
| { | ||
| var foo = (a = b).foo; | ||
| } | ||
| { | ||
| var foo = (a || b).foo; | ||
| } | ||
| { | ||
| var foo = f().foo; | ||
| } | ||
| { | ||
| var foo = object.bar.foo; | ||
| } | ||
| { | ||
| var foo = object['foo']; | ||
| } | ||
| { | ||
| foo = object.foo; | ||
| } | ||
| { | ||
| foo = object['foo']; | ||
| } | ||
| { | ||
| class Foo extends Bar { | ||
| static foo() { | ||
| var bar = super.foo.bar; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| var /* comment */ foo = object.foo; | ||
| } | ||
| { | ||
| var a, | ||
| /* comment */ foo = object.foo; | ||
| } | ||
| { | ||
| var foo /* comment */ = object.foo; | ||
| } | ||
| { | ||
| var a, | ||
| foo /* comment */ = object.foo; | ||
| } | ||
| { | ||
| var foo /* comment */ = object.foo, | ||
| a; | ||
| } | ||
| { | ||
| var foo = object.foo; /* comment */ | ||
| } | ||
| { | ||
| var foo = object.foo, | ||
| /* comment */ a; | ||
| } | ||
| { | ||
| var foo = bar(/* comment */).foo; | ||
| } | ||
| { | ||
| var foo = bar /* comment */.baz.foo; | ||
| } | ||
| { | ||
| var foo = bar[baz].foo; | ||
| } | ||
| { | ||
| var foo = object.foo /* comment */, | ||
| a; | ||
| } | ||
| { | ||
| var foo = object.foo, | ||
| /* comment */ a; | ||
| } |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: biomejs/biome
Length of output: 170
🏁 Script executed:
Repository: biomejs/biome
Length of output: 170
🏁 Script executed:
Repository: biomejs/biome
Length of output: 170
🏁 Script executed:
Repository: biomejs/biome
Length of output: 170
🏁 Script executed:
Repository: biomejs/biome
Length of output: 170
🏁 Script executed:
Repository: biomejs/biome
Length of output: 170
String literal property access won't trigger object destructuring due to quote mismatch in comparison.
For
var x = foo["x"], the code callsvalue_token().text_trimmed()on a string literal token, which returns the raw token including quotes (e.g.,"x"), not the unquoted valuex. Sinceleftis the unquoted variable name, the comparisonleft == value.text_trimmed()will never match for string literals, breaking this destructuring suggestion pattern.Use
inner_string_text()onJsStringLiteralExpressionto extract the unquoted string value:🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you implying that, we should handle checks for dynamic key ?
??