Skip to content

Commit 2a4aaaa

Browse files
mysticateabradzacher
authored andcommitted
feat(eslint-plugin): add require-array-sort-compare rule (#261)
Fixes #247
1 parent 7f47236 commit 2a4aaaa

File tree

3 files changed

+234
-0
lines changed

3 files changed

+234
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Enforce giving `compare` argument to `Array#sort` (require-array-sort-compare)
2+
3+
This rule prevents to invoke `Array#sort()` method without `compare` argument.
4+
5+
`Array#sort()` method sorts that element by the alphabet order.
6+
7+
```ts
8+
[1, 2, 3, 10, 20, 30].sort(); //→ [1, 10, 2, 20, 3, 30]
9+
```
10+
11+
The language specification also noted this trap.
12+
13+
> NOTE 2: Method calls performed by the ToString abstract operations in steps 5 and 7 have the potential to cause SortCompare to not behave as a consistent comparison function.<br> > https://www.ecma-international.org/ecma-262/9.0/#sec-sortcompare
14+
15+
## Rule Details
16+
17+
This rule is aimed at preventing the calls of `Array#sort` method.
18+
This rule ignores the `sort` methods of user-defined types.
19+
20+
Examples of **incorrect** code for this rule:
21+
22+
```ts
23+
const array: any[];
24+
const stringArray: string[];
25+
26+
array.sort();
27+
28+
// Even if a string array, warns it in favor of `String#localeCompare` method.
29+
stringArray.sort();
30+
```
31+
32+
Examples of **correct** code for this rule:
33+
34+
```ts
35+
const array: any[];
36+
const userDefinedType: { sort(): void };
37+
38+
array.sort((a, b) => a - b);
39+
array.sort((a, b) => a.localeCompare(b));
40+
41+
userDefinedType.sort();
42+
```
43+
44+
### Options
45+
46+
There is no option.
47+
48+
## When Not To Use It
49+
50+
If you understand the language specification enough, you can turn this rule off safely.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @fileoverview Enforce giving `compare` argument to `Array#sort`
3+
* @author Toru Nagashima <https://github.com/mysticatea>
4+
*/
5+
6+
import * as ts from 'typescript';
7+
import { TSESTree } from '@typescript-eslint/typescript-estree';
8+
import * as util from '../util';
9+
10+
export default util.createRule({
11+
name: 'require-array-sort-compare',
12+
defaultOptions: [],
13+
14+
meta: {
15+
type: 'problem',
16+
docs: {
17+
description: 'Enforce giving `compare` argument to `Array#sort`',
18+
category: 'Best Practices',
19+
recommended: false
20+
},
21+
messages: {
22+
requireCompare: "Require 'compare' argument."
23+
},
24+
schema: []
25+
},
26+
27+
create(context) {
28+
const service = util.getParserServices(context);
29+
const checker = service.program.getTypeChecker();
30+
31+
return {
32+
"CallExpression[arguments.length=0] > MemberExpression[property.name='sort'][computed=false]"(
33+
node: TSESTree.MemberExpression
34+
) {
35+
// Get the symbol of the `sort` method.
36+
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
37+
const sortSymbol = checker.getSymbolAtLocation(tsNode);
38+
if (sortSymbol == null) {
39+
return;
40+
}
41+
42+
// Check the owner type of the `sort` method.
43+
for (const methodDecl of sortSymbol.declarations) {
44+
const typeDecl = methodDecl.parent;
45+
if (
46+
ts.isInterfaceDeclaration(typeDecl) &&
47+
ts.isSourceFile(typeDecl.parent) &&
48+
typeDecl.name.escapedText === 'Array'
49+
) {
50+
context.report({ node: node.parent!, messageId: 'requireCompare' });
51+
return;
52+
}
53+
}
54+
}
55+
};
56+
}
57+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import path from 'path';
2+
import rule from '../../src/rules/require-array-sort-compare';
3+
import { RuleTester } from '../RuleTester';
4+
5+
const rootPath = path.join(process.cwd(), 'tests/fixtures/');
6+
7+
const ruleTester = new RuleTester({
8+
parser: '@typescript-eslint/parser',
9+
parserOptions: {
10+
tsconfigRootDir: rootPath,
11+
project: './tsconfig.json'
12+
}
13+
});
14+
15+
ruleTester.run('require-array-sort-compare', rule, {
16+
valid: [
17+
`
18+
function f(a: any[]) {
19+
a.sort(undefined)
20+
}
21+
`,
22+
`
23+
function f(a: any[]) {
24+
a.sort((a, b) => a - b)
25+
}
26+
`,
27+
`
28+
function f(a: Array<string>) {
29+
a.sort(undefined)
30+
}
31+
`,
32+
`
33+
function f(a: Array<number>) {
34+
a.sort((a, b) => a - b)
35+
}
36+
`,
37+
`
38+
function f(a: { sort(): void }) {
39+
a.sort()
40+
}
41+
`,
42+
`
43+
class A { sort(): void {} }
44+
function f(a: A) {
45+
a.sort()
46+
}
47+
`,
48+
`
49+
interface A { sort(): void }
50+
function f(a: A) {
51+
a.sort()
52+
}
53+
`,
54+
`
55+
interface A { sort(): void }
56+
function f<T extends A>(a: T) {
57+
a.sort()
58+
}
59+
`,
60+
`
61+
function f(a: any) {
62+
a.sort()
63+
}
64+
`,
65+
`
66+
namespace UserDefined {
67+
interface Array {
68+
sort(): void
69+
}
70+
function f(a: Array) {
71+
a.sort()
72+
}
73+
}
74+
`
75+
],
76+
invalid: [
77+
{
78+
code: `
79+
function f(a: Array<any>) {
80+
a.sort()
81+
}
82+
`,
83+
errors: [{ messageId: 'requireCompare' }]
84+
},
85+
{
86+
code: `
87+
function f(a: string[]) {
88+
a.sort()
89+
}
90+
`,
91+
errors: [{ messageId: 'requireCompare' }]
92+
},
93+
{
94+
code: `
95+
function f(a: string | string[]) {
96+
if (Array.isArray(a))
97+
a.sort()
98+
}
99+
`,
100+
errors: [{ messageId: 'requireCompare' }]
101+
},
102+
{
103+
code: `
104+
function f(a: number[] | string[]) {
105+
a.sort()
106+
}
107+
`,
108+
errors: [{ messageId: 'requireCompare' }]
109+
},
110+
{
111+
code: `
112+
function f<T extends number[]>(a: T) {
113+
a.sort()
114+
}
115+
`,
116+
errors: [{ messageId: 'requireCompare' }]
117+
},
118+
{
119+
code: `
120+
function f<T, U extends T[]>(a: U) {
121+
a.sort()
122+
}
123+
`,
124+
errors: [{ messageId: 'requireCompare' }]
125+
}
126+
]
127+
});

0 commit comments

Comments
 (0)