Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/all-kiwis-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@biomejs/biome": patch
---

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.

**Invalid:**

```js
[1, 2, 3].filter(x => x > 1)[0];

[1, 2, 3].filter(x => x > 1).at(0);
```
12 changes: 12 additions & 0 deletions 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.

81 changes: 51 additions & 30 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ define_categories! {
"lint/nursery/useExhaustiveSwitchCases": "https://biomejs.dev/linter/rules/use-exhaustive-switch-cases",
"lint/nursery/useExplicitFunctionReturnType": "https://biomejs.dev/linter/rules/use-explicit-type",
"lint/nursery/useExplicitType": "https://biomejs.dev/linter/rules/use-explicit-type",
"lint/nursery/useFind": "https://biomejs.dev/linter/rules/use-find",
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
"lint/nursery/useJsxCurlyBraceConvention": "https://biomejs.dev/linter/rules/use-jsx-curly-brace-convention",
"lint/nursery/useMaxParams": "https://biomejs.dev/linter/rules/use-max-params",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ pub mod use_array_sort_compare;
pub mod use_consistent_arrow_return;
pub mod use_exhaustive_switch_cases;
pub mod use_explicit_type;
pub mod use_find;
pub mod use_max_params;
pub mod use_qwik_method_usage;
pub mod use_qwik_valid_lexical_scope;
pub mod use_sorted_classes;
pub mod use_spread;
pub mod use_vue_define_macros_order;
pub mod use_vue_multi_word_component_names;
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 ,] } }
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 ,] } }
120 changes: 120 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_find.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use biome_analyze::{
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_js_syntax::{
AnyJsExpression, JsCallExpression, JsComputedMemberExpression, JsStaticMemberExpression,
};
use biome_rowan::{AstNode, AstSeparatedList, TextRange};
use biome_rule_options::use_find::UseFindOptions;

use crate::services::typed::Typed;

declare_lint_rule! {
/// Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result.
///
/// 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]`.
/// However, it is simpler to use `Array.prototype.find()` instead, `arr.find(x => x > 0)`, which also returns the first entry matching a condition.
/// Because the `.find()` only needs to execute the callback until it finds a match, it's also more efficient.
///
/// ## Examples
///
/// ### Invalid
///
/// ```ts,expect_diagnostic,file=invalid.ts
/// [1, 2, 3].filter(x => x > 1)[0];
/// ```
///
/// ```ts,expect_diagnostic,file=invalid2.ts
/// [1, 2, 3].filter(x => x > 1).at(0);
/// ```
///
/// ### Valid
///
/// ```ts,file=valid.ts
/// [1, 2, 3].find(x => x > 1);
/// ```
///
pub UseFind {
version: "next",
name: "useFind",
language: "js",
recommended: false,
sources: &[RuleSource::EslintTypeScript("prefer-find").same()],
domains: &[RuleDomain::Project],
}
}

fn is_first_position(ctx: &RuleContext<UseFind>, express: &AnyJsExpression) -> bool {
ctx.type_of_expression(express).is_number_literal(0.)
|| ctx.type_of_expression(express).is_bigint_literal(0)
}

impl Rule for UseFind {
type Query = Typed<JsCallExpression>;
type State = TextRange;
type Signals = Option<Self::State>;
type Options = UseFindOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let binding = ctx.query();
let binding_callee = binding.callee().ok()?;
let binding_callee_static = binding_callee.as_js_static_member_expression()?;

let member = binding_callee_static.member().ok()?;
let member_name = member.as_js_name()?;
let member_value = member_name.value_token().ok()?;
if member_value.text_trimmed() != "filter" {
return None;
}

let parent = binding.syntax().parent()?;

// Handle .filter()[0]
if JsComputedMemberExpression::can_cast(parent.kind()) {
let express = JsComputedMemberExpression::cast(parent)?;
let member = express.member().ok()?;

if is_first_position(ctx, &member) {
return Some(express.range());
}

// Handle .filter().at(0)
} else if JsStaticMemberExpression::can_cast(parent.kind()) {
let express = JsStaticMemberExpression::cast(parent)?;

let member = express.member().ok()?;
let value_token = member.value_token().ok()?;
if value_token.text_trimmed() != "at" {
return None;
}

let call_parent = express.syntax().parent()?;
let call_parent_express = JsCallExpression::cast(call_parent)?;
let arguments = call_parent_express.arguments().ok()?;
let first_arg = arguments.args().first()?.ok()?;
let first_arg_express = first_arg.as_any_js_expression()?;

if is_first_position(ctx, first_arg_express) {
return Some(call_parent_express.range());
}
}

None
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
state,
markup! {
"Prefer using Array#find() over Array#filter[0]."
},
)
.note(markup! {
"Use Array#find() instead of Array#filter[0] to improve readability."
}),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[1, 2, 3].filter(x => x > 1)[0];
const found1 = [1, 2, 3].filter(x => x > 1)[0];

[1, 2, 3].filter(x => x > 1).at(0);
const found2 = [1, 2, 3].filter(x => x > 1).at(0);

[1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1)[0].toString();
[1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1).at(0)?.toString();
113 changes: 113 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/useFind/invalid.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.ts
---
# Input
```ts
[1, 2, 3].filter(x => x > 1)[0];
const found1 = [1, 2, 3].filter(x => x > 1)[0];

[1, 2, 3].filter(x => x > 1).at(0);
const found2 = [1, 2, 3].filter(x => x > 1).at(0);

[1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1)[0].toString();
[1, 2, 3].concat([56, 76, 4543]).filter(x => x > 1).at(0)?.toString();

```

# Diagnostics
```
invalid.ts:1:1 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Prefer using Array#find() over Array#filter[0].

> 1 │ [1, 2, 3].filter(x => x > 1)[0];
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 │ const found1 = [1, 2, 3].filter(x => x > 1)[0];
3 │

i Use Array#find() instead of Array#filter[0] to improve readability.


```

```
invalid.ts:2:16 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Prefer using Array#find() over Array#filter[0].

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);

i Use Array#find() instead of Array#filter[0] to improve readability.


```

```
invalid.ts:4:1 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Prefer using Array#find() over Array#filter[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 │

i Use Array#find() instead of Array#filter[0] to improve readability.


```

```
invalid.ts:5:16 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Prefer using Array#find() over Array#filter[0].

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();

i Use Array#find() instead of Array#filter[0] to improve readability.


```

```
invalid.ts:7:1 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Prefer using Array#find() over Array#filter[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();
9 │

i Use Array#find() instead of Array#filter[0] to improve readability.


```

```
invalid.ts:8:1 lint/nursery/useFind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Prefer using Array#find() over Array#filter[0].

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();
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 │

i Use Array#find() instead of Array#filter[0] to improve readability.


```
13 changes: 13 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/useFind/valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* should not generate diagnostics */
[1, 2, 3].find(x => x > 1);
[1, 2, 3].filter(x => x > 1)[1];
const foundArray = [1, 2, 3].filter(x => x > 1)[1];

[1, 2, 3].filter(x => x > 1).concat([5, 6, 7])[0];

const obj = {
find: () => {
return [1, 2, 3]
}
}
obj.find()[0];
21 changes: 21 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/useFind/valid.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.ts
---
# Input
```ts
/* should not generate diagnostics */
[1, 2, 3].find(x => x > 1);
[1, 2, 3].filter(x => x > 1)[1];
const foundArray = [1, 2, 3].filter(x => x > 1)[1];
[1, 2, 3].filter(x => x > 1).concat([5, 6, 7])[0];
const obj = {
find: () => {
return [1, 2, 3]
}
}
obj.find()[0];
```
15 changes: 15 additions & 0 deletions crates/biome_js_type_info/src/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,21 @@ impl Type {
})
}

/// Returns whether this type is a bigint with the given `value`.
pub fn is_bigint_literal(&self, value: i64) -> bool {
self.as_raw_data().is_some_and(|ty| match ty {
TypeData::Literal(literal) => match literal.as_ref() {
Literal::BigInt(literal) => literal
.trim_end_matches('n')
.parse::<i64>()
.ok()
.is_some_and(|literal_value| literal_value == value),
_ => false,
},
_ => false,
})
}

/// Returns whether this type is the `Promise` class.
pub fn is_promise(&self) -> bool {
self.id == GLOBAL_PROMISE_ID
Expand Down
1 change: 1 addition & 0 deletions crates/biome_rule_options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ pub mod use_exponentiation_operator;
pub mod use_export_type;
pub mod use_exports_last;
pub mod use_filenaming_convention;
pub mod use_find;
pub mod use_flat_map;
pub mod use_focusable_interactive;
pub mod use_for_of;
Expand Down
6 changes: 6 additions & 0 deletions crates/biome_rule_options/src/use_find.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseFindOptions {}
Loading