Skip to content

Commit 1a2d1af

Browse files
authored
feat(biome_js_analyze): implement useArraySortCompare (#8065)
1 parent 3ff9d45 commit 1a2d1af

File tree

15 files changed

+406
-22
lines changed

15 files changed

+406
-22
lines changed

.changeset/sad-adults-enjoy.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`useArraySortCompare`](https://biomejs.dev/linter/rules/use-array-sort-compare/). Require Array#sort and Array#toSorted calls to always provide a compareFunction.
6+
7+
**Invalid:**
8+
```js
9+
const array = [];
10+
array.sort();
11+
```
12+
13+
**Valid:**
14+
```js
15+
const array = [];
16+
array.sort((a, b) => a - b);
17+
```

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: 42 additions & 21 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
@@ -193,6 +193,7 @@ define_categories! {
193193
"lint/nursery/noVueReservedKeys": "https://biomejs.dev/linter/rules/no-vue-reserved-keys",
194194
"lint/nursery/noVueReservedProps": "https://biomejs.dev/linter/rules/no-vue-reserved-props",
195195
"lint/nursery/useAnchorHref": "https://biomejs.dev/linter/rules/use-anchor-href",
196+
"lint/nursery/useArraySortCompare": "https://biomejs.dev/linter/rules/use-array-sort-compare",
196197
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
197198
"lint/nursery/useConsistentArrowReturn": "https://biomejs.dev/linter/rules/use-consistent-arrow-return",
198199
"lint/nursery/useConsistentObjectDefinition": "https://biomejs.dev/linter/rules/use-consistent-object-definition",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod no_vue_data_object_declaration;
2525
pub mod no_vue_duplicate_keys;
2626
pub mod no_vue_reserved_keys;
2727
pub mod no_vue_reserved_props;
28+
pub mod use_array_sort_compare;
2829
pub mod use_consistent_arrow_return;
2930
pub mod use_exhaustive_switch_cases;
3031
pub mod use_explicit_type;
@@ -34,4 +35,4 @@ pub mod use_qwik_valid_lexical_scope;
3435
pub mod use_sorted_classes;
3536
pub mod use_vue_define_macros_order;
3637
pub mod use_vue_multi_word_component_names;
37-
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_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_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_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
38+
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_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_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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::JsCallExpression;
6+
use biome_rowan::{AstNode, AstSeparatedList};
7+
use biome_rule_options::use_array_sort_compare::UseArraySortCompareOptions;
8+
9+
use crate::services::typed::Typed;
10+
11+
declare_lint_rule! {
12+
/// Require Array#sort and Array#toSorted calls to always provide a compareFunction.
13+
///
14+
/// When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units [ECMA specification](https://262.ecma-international.org/9.0/#sec-sortcompare).
15+
///
16+
/// The result is that elements are sorted alphabetically, regardless of their type. For example, when sorting numbers, this results in a "10 before 2" order:
17+
///
18+
/// ```ts,file=example.ts,ignore
19+
/// [1, 2, 3, 10, 20, 30].sort(); //→ [1, 10, 2, 20, 3, 30]
20+
/// ```
21+
///
22+
/// This rule reports on any call to the sort methods that do not provide a compare argument.
23+
///
24+
/// ## Examples
25+
///
26+
/// ### Invalid
27+
///
28+
/// ```ts,file=invalid.ts,expect_diagnostic
29+
/// const array: any[] = [];
30+
/// array.sort();
31+
/// ```
32+
///
33+
/// ### Valid
34+
///
35+
/// ```ts,file=valid.ts
36+
/// const array: any[] = [];
37+
/// array.sort((a, b) => a - b);
38+
/// ```
39+
///
40+
pub UseArraySortCompare {
41+
version: "next",
42+
name: "useArraySortCompare",
43+
language: "js",
44+
recommended: false,
45+
sources: &[RuleSource::EslintTypeScript("require-array-sort-compare").same()],
46+
domains: &[RuleDomain::Project],
47+
}
48+
}
49+
50+
impl Rule for UseArraySortCompare {
51+
type Query = Typed<JsCallExpression>;
52+
type State = ();
53+
type Signals = Option<Self::State>;
54+
type Options = UseArraySortCompareOptions;
55+
56+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
57+
let node = ctx.query();
58+
59+
let binding = node.callee().ok()?.omit_parentheses();
60+
let callee = binding.as_js_static_member_expression()?;
61+
62+
let call_object = callee.object().ok()?;
63+
let ty = ctx.type_of_expression(&call_object);
64+
if !ty.is_array_of(|_ty| true) {
65+
return None;
66+
}
67+
68+
let call_name = callee.member().ok()?.as_js_name()?.to_trimmed_text();
69+
if call_name != "sort" && call_name != "toSorted" {
70+
return None;
71+
}
72+
73+
let arguments = node.arguments().ok()?.args();
74+
if arguments.is_empty() {
75+
return Some(());
76+
}
77+
78+
let binding = arguments.first()?.ok()?;
79+
let first_arg = binding.as_any_js_expression()?;
80+
let ty = ctx.type_of_expression(first_arg);
81+
if ty.is_undefined() || ty.is_null() {
82+
return Some(());
83+
}
84+
85+
None
86+
}
87+
88+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
89+
let node = ctx.query();
90+
Some(
91+
RuleDiagnostic::new(
92+
rule_category!(),
93+
node.range(),
94+
markup! {
95+
"Compare function missing."
96+
},
97+
)
98+
.note(markup! {
99+
"When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units."
100+
})
101+
.note(markup! {
102+
"Add a compare function to prevent unexpected sorting."
103+
}),
104+
)
105+
}
106+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const anyArray: any[] = [];
2+
anyArray.sort();
3+
anyArray.toSorted();
4+
5+
anyArray.sort(undefined);
6+
anyArray.toSorted(undefined);
7+
8+
const stringArray: string[] = [];
9+
stringArray.sort();
10+
stringArray.toSorted();
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.ts
4+
---
5+
# Input
6+
```ts
7+
const anyArray: any[] = [];
8+
anyArray.sort();
9+
anyArray.toSorted();
10+
11+
anyArray.sort(undefined);
12+
anyArray.toSorted(undefined);
13+
14+
const stringArray: string[] = [];
15+
stringArray.sort();
16+
stringArray.toSorted();
17+
18+
```
19+
20+
# Diagnostics
21+
```
22+
invalid.ts:2:1 lint/nursery/useArraySortCompare ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23+
24+
i Compare function missing.
25+
26+
1 │ const anyArray: any[] = [];
27+
> 2 │ anyArray.sort();
28+
│ ^^^^^^^^^^^^^^^
29+
3 │ anyArray.toSorted();
30+
4 │
31+
32+
i When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units.
33+
34+
i Add a compare function to prevent unexpected sorting.
35+
36+
37+
```
38+
39+
```
40+
invalid.ts:3:1 lint/nursery/useArraySortCompare ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41+
42+
i Compare function missing.
43+
44+
1 │ const anyArray: any[] = [];
45+
2 │ anyArray.sort();
46+
> 3 │ anyArray.toSorted();
47+
│ ^^^^^^^^^^^^^^^^^^^
48+
4 │
49+
5 │ anyArray.sort(undefined);
50+
51+
i When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units.
52+
53+
i Add a compare function to prevent unexpected sorting.
54+
55+
56+
```
57+
58+
```
59+
invalid.ts:5:1 lint/nursery/useArraySortCompare ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
60+
61+
i Compare function missing.
62+
63+
3 │ anyArray.toSorted();
64+
4 │
65+
> 5 │ anyArray.sort(undefined);
66+
│ ^^^^^^^^^^^^^^^^^^^^^^^^
67+
6 │ anyArray.toSorted(undefined);
68+
7 │
69+
70+
i When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units.
71+
72+
i Add a compare function to prevent unexpected sorting.
73+
74+
75+
```
76+
77+
```
78+
invalid.ts:6:1 lint/nursery/useArraySortCompare ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79+
80+
i Compare function missing.
81+
82+
5 │ anyArray.sort(undefined);
83+
> 6 │ anyArray.toSorted(undefined);
84+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
85+
7 │
86+
8 │ const stringArray: string[] = [];
87+
88+
i When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units.
89+
90+
i Add a compare function to prevent unexpected sorting.
91+
92+
93+
```
94+
95+
```
96+
invalid.ts:9:1 lint/nursery/useArraySortCompare ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97+
98+
i Compare function missing.
99+
100+
8 │ const stringArray: string[] = [];
101+
> 9 │ stringArray.sort();
102+
│ ^^^^^^^^^^^^^^^^^^
103+
10 │ stringArray.toSorted();
104+
11 │
105+
106+
i When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units.
107+
108+
i Add a compare function to prevent unexpected sorting.
109+
110+
111+
```
112+
113+
```
114+
invalid.ts:10:1 lint/nursery/useArraySortCompare ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
115+
116+
i Compare function missing.
117+
118+
8 │ const stringArray: string[] = [];
119+
9 │ stringArray.sort();
120+
> 10 │ stringArray.toSorted();
121+
│ ^^^^^^^^^^^^^^^^^^^^^^
122+
11 │
123+
124+
i When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units.
125+
126+
i Add a compare function to prevent unexpected sorting.
127+
128+
129+
```
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* should not generate diagnostics */
2+
const array: any[] = [];
3+
array.sort((a, b) => a - b);
4+
array.sort((a, b) => a.localeCompare(b));
5+
array.toSorted((a, b) => a - b);
6+
7+
const userDefinedType = {
8+
sort: () => { }
9+
};
10+
userDefinedType.sort();

0 commit comments

Comments
 (0)