Skip to content

Commit 5a184d1

Browse files
Netailematipico
authored andcommitted
feat(js_analyze): implement useFind (biomejs#8100)
1 parent 0bb1d63 commit 5a184d1

File tree

16 files changed

+414
-31
lines changed

16 files changed

+414
-31
lines changed

.changeset/all-kiwis-poke.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`useFind`](https://biomejs.dev/linter/rules/use-find/). Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result.
6+
7+
**Invalid:**
8+
9+
```js
10+
[1, 2, 3].filter(x => x > 1)[0];
11+
12+
[1, 2, 3].filter(x => x > 1).at(0);
13+
```

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 51 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/domain_selector.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ define_categories! {
203203
"lint/nursery/useExhaustiveSwitchCases": "https://biomejs.dev/linter/rules/use-exhaustive-switch-cases",
204204
"lint/nursery/useExplicitFunctionReturnType": "https://biomejs.dev/linter/rules/use-explicit-type",
205205
"lint/nursery/useExplicitType": "https://biomejs.dev/linter/rules/use-explicit-type",
206+
"lint/nursery/useFind": "https://biomejs.dev/linter/rules/use-find",
206207
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
207208
"lint/nursery/useJsxCurlyBraceConvention": "https://biomejs.dev/linter/rules/use-jsx-curly-brace-convention",
208209
"lint/nursery/useMaxParams": "https://biomejs.dev/linter/rules/use-max-params",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ pub mod use_array_sort_compare;
3030
pub mod use_consistent_arrow_return;
3131
pub mod use_exhaustive_switch_cases;
3232
pub mod use_explicit_type;
33+
pub mod use_find;
3334
pub mod use_max_params;
3435
pub mod use_qwik_method_usage;
3536
pub mod use_qwik_valid_lexical_scope;
3637
pub mod use_sorted_classes;
3738
pub mod use_spread;
3839
pub mod use_vue_define_macros_order;
3940
pub mod use_vue_multi_word_component_names;
40-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
41+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use biome_analyze::{
2+
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_js_syntax::{
6+
AnyJsExpression, JsCallExpression, JsComputedMemberExpression, JsStaticMemberExpression,
7+
};
8+
use biome_rowan::{AstNode, AstSeparatedList, TextRange};
9+
use biome_rule_options::use_find::UseFindOptions;
10+
11+
use crate::services::typed::Typed;
12+
13+
declare_lint_rule! {
14+
/// Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result.
15+
///
16+
/// When searching for the first item in an array matching a condition, it may be tempting to use code like `arr.filter(x => x > 0)[0]`.
17+
/// However, it is simpler to use `Array.prototype.find()` instead, `arr.find(x => x > 0)`, which also returns the first entry matching a condition.
18+
/// Because the `.find()` only needs to execute the callback until it finds a match, it's also more efficient.
19+
///
20+
/// ## Examples
21+
///
22+
/// ### Invalid
23+
///
24+
/// ```ts,expect_diagnostic,file=invalid.ts
25+
/// [1, 2, 3].filter(x => x > 1)[0];
26+
/// ```
27+
///
28+
/// ```ts,expect_diagnostic,file=invalid2.ts
29+
/// [1, 2, 3].filter(x => x > 1).at(0);
30+
/// ```
31+
///
32+
/// ### Valid
33+
///
34+
/// ```ts,file=valid.ts
35+
/// [1, 2, 3].find(x => x > 1);
36+
/// ```
37+
///
38+
pub UseFind {
39+
version: "next",
40+
name: "useFind",
41+
language: "js",
42+
recommended: false,
43+
sources: &[RuleSource::EslintTypeScript("prefer-find").same()],
44+
domains: &[RuleDomain::Project],
45+
}
46+
}
47+
48+
fn is_first_position(ctx: &RuleContext<UseFind>, express: &AnyJsExpression) -> bool {
49+
ctx.type_of_expression(express).is_number_literal(0.)
50+
|| ctx.type_of_expression(express).is_bigint_literal(0)
51+
}
52+
53+
impl Rule for UseFind {
54+
type Query = Typed<JsCallExpression>;
55+
type State = TextRange;
56+
type Signals = Option<Self::State>;
57+
type Options = UseFindOptions;
58+
59+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
60+
let binding = ctx.query();
61+
let binding_callee = binding.callee().ok()?;
62+
let binding_callee_static = binding_callee.as_js_static_member_expression()?;
63+
64+
let member = binding_callee_static.member().ok()?;
65+
let member_name = member.as_js_name()?;
66+
let member_value = member_name.value_token().ok()?;
67+
if member_value.text_trimmed() != "filter" {
68+
return None;
69+
}
70+
71+
let parent = binding.syntax().parent()?;
72+
73+
// Handle .filter()[0]
74+
if JsComputedMemberExpression::can_cast(parent.kind()) {
75+
let express = JsComputedMemberExpression::cast(parent)?;
76+
let member = express.member().ok()?;
77+
78+
if is_first_position(ctx, &member) {
79+
return Some(express.range());
80+
}
81+
82+
// Handle .filter().at(0)
83+
} else if JsStaticMemberExpression::can_cast(parent.kind()) {
84+
let express = JsStaticMemberExpression::cast(parent)?;
85+
86+
let member = express.member().ok()?;
87+
let value_token = member.value_token().ok()?;
88+
if value_token.text_trimmed() != "at" {
89+
return None;
90+
}
91+
92+
let call_parent = express.syntax().parent()?;
93+
let call_parent_express = JsCallExpression::cast(call_parent)?;
94+
let arguments = call_parent_express.arguments().ok()?;
95+
let first_arg = arguments.args().first()?.ok()?;
96+
let first_arg_express = first_arg.as_any_js_expression()?;
97+
98+
if is_first_position(ctx, first_arg_express) {
99+
return Some(call_parent_express.range());
100+
}
101+
}
102+
103+
None
104+
}
105+
106+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
107+
Some(
108+
RuleDiagnostic::new(
109+
rule_category!(),
110+
state,
111+
markup! {
112+
"Prefer using Array#find() over Array#filter[0]."
113+
},
114+
)
115+
.note(markup! {
116+
"Use Array#find() instead of Array#filter[0] to improve readability."
117+
}),
118+
)
119+
}
120+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[1, 2, 3].filter(x => x > 1)[0];
2+
const found1 = [1, 2, 3].filter(x => x > 1)[0];
3+
4+
[1, 2, 3].filter(x => x > 1).at(0);
5+
const found2 = [1, 2, 3].filter(x => x > 1).at(0);
6+
7+
[1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1)[0].toString();
8+
[1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1).at(0)?.toString();
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.ts
4+
---
5+
# Input
6+
```ts
7+
[1, 2, 3].filter(x => x > 1)[0];
8+
const found1 = [1, 2, 3].filter(x => x > 1)[0];
9+
10+
[1, 2, 3].filter(x => x > 1).at(0);
11+
const found2 = [1, 2, 3].filter(x => x > 1).at(0);
12+
13+
[1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1)[0].toString();
14+
[1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1).at(0)?.toString();
15+
16+
```
17+
18+
# Diagnostics
19+
```
20+
invalid.ts:1:1 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21+
22+
i Prefer using Array#find() over Array#filter[0].
23+
24+
> 1 │ [1, 2, 3].filter(x => x > 1)[0];
25+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26+
2 │ const found1 = [1, 2, 3].filter(x => x > 1)[0];
27+
3 │
28+
29+
i Use Array#find() instead of Array#filter[0] to improve readability.
30+
31+
32+
```
33+
34+
```
35+
invalid.ts:2:16 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36+
37+
i Prefer using Array#find() over Array#filter[0].
38+
39+
1 │ [1, 2, 3].filter(x => x > 1)[0];
40+
> 2 │ const found1 = [1, 2, 3].filter(x => x > 1)[0];
41+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42+
3 │
43+
4 │ [1, 2, 3].filter(x => x > 1).at(0);
44+
45+
i Use Array#find() instead of Array#filter[0] to improve readability.
46+
47+
48+
```
49+
50+
```
51+
invalid.ts:4:1 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52+
53+
i Prefer using Array#find() over Array#filter[0].
54+
55+
2 │ const found1 = [1, 2, 3].filter(x => x > 1)[0];
56+
3 │
57+
> 4 │ [1, 2, 3].filter(x => x > 1).at(0);
58+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59+
5 │ const found2 = [1, 2, 3].filter(x => x > 1).at(0);
60+
6 │
61+
62+
i Use Array#find() instead of Array#filter[0] to improve readability.
63+
64+
65+
```
66+
67+
```
68+
invalid.ts:5:16 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
69+
70+
i Prefer using Array#find() over Array#filter[0].
71+
72+
4 │ [1, 2, 3].filter(x => x > 1).at(0);
73+
> 5 │ const found2 = [1, 2, 3].filter(x => x > 1).at(0);
74+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
75+
6 │
76+
7 │ [1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1)[0].toString();
77+
78+
i Use Array#find() instead of Array#filter[0] to improve readability.
79+
80+
81+
```
82+
83+
```
84+
invalid.ts:7:1 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85+
86+
i Prefer using Array#find() over Array#filter[0].
87+
88+
5 │ const found2 = [1, 2, 3].filter(x => x > 1).at(0);
89+
6 │
90+
> 7 │ [1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1)[0].toString();
91+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
92+
8 │ [1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1).at(0)?.toString();
93+
9 │
94+
95+
i Use Array#find() instead of Array#filter[0] to improve readability.
96+
97+
98+
```
99+
100+
```
101+
invalid.ts:8:1 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102+
103+
i Prefer using Array#find() over Array#filter[0].
104+
105+
7 │ [1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1)[0].toString();
106+
> 8 │ [1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1).at(0)?.toString();
107+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
108+
9 │
109+
110+
i Use Array#find() instead of Array#filter[0] to improve readability.
111+
112+
113+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* should not generate diagnostics */
2+
[1, 2, 3].find(x => x > 1);
3+
[1, 2, 3].filter(x => x > 1)[1];
4+
const foundArray = [1, 2, 3].filter(x => x > 1)[1];
5+
6+
[1, 2, 3].filter(x => x > 1).concat([5, 6, 7])[0];
7+
8+
const obj = {
9+
find: () => {
10+
return [1, 2, 3]
11+
}
12+
}
13+
obj.find()[0];

0 commit comments

Comments
 (0)