Skip to content

Commit 1eaae09

Browse files
authoredOct 24, 2022
feat(eslint-plugin): [member-ordering] add natural sort order (#5662)
* [WIP] feat(eslint-plugin): [member-ordering] add natural sort order * Fix yarn.lock and split option on case sensitivity * Document it too * Remove last todos * Move member-ordering sub-tests into sub-dirs
1 parent 3bd38ca commit 1eaae09

8 files changed

+352
-22
lines changed
 

‎packages/eslint-plugin/docs/rules/member-ordering.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ type OrderConfig = MemberType[] | SortedOrderConfig | 'never';
2424

2525
interface SortedOrderConfig {
2626
memberTypes?: MemberType[] | 'never';
27-
order: 'alphabetically' | 'alphabetically-case-insensitive' | 'as-written';
27+
order:
28+
| 'alphabetically'
29+
| 'alphabetically-case-insensitive'
30+
| 'as-written'
31+
| 'natural'
32+
| 'natural-case-insensitive';
2833
}
2934

3035
// See below for the more specific MemberType strings
@@ -56,6 +61,17 @@ The supported member attributes are, in order:
5661
Member attributes may be joined with a `'-'` to combine into more specific groups.
5762
For example, `'public-field'` would come before `'private-field'`.
5863

64+
### Orders
65+
66+
The `order` value specifies what order members should be within a group.
67+
It defaults to `as-written`, meaning any order is fine.
68+
Other allowed values are:
69+
70+
- `alphabetically`: Sorted in a-z alphabetical order, directly using string `<` comparison (so `B` comes before `a`)
71+
- `alphabetically-case-insensitive`: Sorted in a-z alphabetical order, ignoring case (so `a` comes before `B`)
72+
- `natural`: Same as `alphabetically`, but using [`natural-compare-lite`](https://github.com/litejs/natural-compare-lite) for more friendly sorting of numbers
73+
- `natural-case-insensitive`: Same as `alphabetically-case-insensitive`, but using [`natural-compare-lite`](https://github.com/litejs/natural-compare-lite) for more friendly sorting of numbers
74+
5975
### Default configuration
6076

6177
The default configuration looks as follows:

‎packages/eslint-plugin/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@typescript-eslint/utils": "5.41.0",
5050
"debug": "^4.3.4",
5151
"ignore": "^5.2.0",
52+
"natural-compare-lite": "^1.4.0",
5253
"regexpp": "^3.2.0",
5354
"semver": "^7.3.7",
5455
"tsutils": "^3.21.0"
@@ -57,6 +58,7 @@
5758
"@types/debug": "*",
5859
"@types/json-schema": "*",
5960
"@types/marked": "*",
61+
"@types/natural-compare-lite": "^1.4.0",
6062
"@types/prettier": "*",
6163
"chalk": "^5.0.1",
6264
"json-schema": "*",

‎packages/eslint-plugin/src/rules/member-ordering.ts

+38-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { JSONSchema, TSESLint, TSESTree } from '@typescript-eslint/utils';
22
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3+
import naturalCompare from 'natural-compare-lite';
34

45
import * as util from '../util';
56

@@ -34,10 +35,13 @@ type BaseMemberType =
3435

3536
type MemberType = BaseMemberType | BaseMemberType[];
3637

37-
type Order =
38+
type AlphabeticalOrder =
3839
| 'alphabetically'
3940
| 'alphabetically-case-insensitive'
40-
| 'as-written';
41+
| 'natural'
42+
| 'natural-case-insensitive';
43+
44+
type Order = AlphabeticalOrder | 'as-written';
4145

4246
interface SortedOrderConfig {
4347
memberTypes?: MemberType[] | 'never';
@@ -87,7 +91,13 @@ const objectConfig = (memberTypes: MemberType[]): JSONSchema.JSONSchema4 => ({
8791
},
8892
order: {
8993
type: 'string',
90-
enum: ['alphabetically', 'alphabetically-case-insensitive', 'as-written'],
94+
enum: [
95+
'alphabetically',
96+
'alphabetically-case-insensitive',
97+
'as-written',
98+
'natural',
99+
'natural-case-insensitive',
100+
],
91101
},
92102
},
93103
additionalProperties: false,
@@ -629,7 +639,7 @@ export default util.createRule<Options, MessageIds>({
629639
*/
630640
function checkAlphaSort(
631641
members: Member[],
632-
caseSensitive: boolean,
642+
order: AlphabeticalOrder,
633643
): boolean {
634644
let previousName = '';
635645
let isCorrectlySorted = true;
@@ -640,11 +650,7 @@ export default util.createRule<Options, MessageIds>({
640650

641651
// Note: Not all members have names
642652
if (name) {
643-
if (
644-
caseSensitive
645-
? name < previousName
646-
: name.toLowerCase() < previousName.toLowerCase()
647-
) {
653+
if (naturalOutOfOrder(name, previousName, order)) {
648654
context.report({
649655
node: member,
650656
messageId: 'incorrectOrder',
@@ -664,6 +670,25 @@ export default util.createRule<Options, MessageIds>({
664670
return isCorrectlySorted;
665671
}
666672

673+
function naturalOutOfOrder(
674+
name: string,
675+
previousName: string,
676+
order: AlphabeticalOrder,
677+
): boolean {
678+
switch (order) {
679+
case 'alphabetically':
680+
return name < previousName;
681+
case 'alphabetically-case-insensitive':
682+
return name.toLowerCase() < previousName.toLowerCase();
683+
case 'natural':
684+
return naturalCompare(name, previousName) !== 1;
685+
case 'natural-case-insensitive':
686+
return (
687+
naturalCompare(name.toLowerCase(), previousName.toLowerCase()) !== 1
688+
);
689+
}
690+
}
691+
667692
/**
668693
* Validates if all members are correctly sorted.
669694
*
@@ -681,7 +706,7 @@ export default util.createRule<Options, MessageIds>({
681706
}
682707

683708
// Standardize config
684-
let order: Order | null = null;
709+
let order: Order | undefined;
685710
let memberTypes;
686711

687712
if (Array.isArray(orderConfig)) {
@@ -691,9 +716,7 @@ export default util.createRule<Options, MessageIds>({
691716
memberTypes = orderConfig.memberTypes;
692717
}
693718

694-
const hasAlphaSort = order?.startsWith('alphabetically');
695-
const alphaSortIsCaseSensitive =
696-
order !== 'alphabetically-case-insensitive';
719+
const hasAlphaSort = !!(order && order !== 'as-written');
697720

698721
// Check order
699722
if (Array.isArray(memberTypes)) {
@@ -706,11 +729,11 @@ export default util.createRule<Options, MessageIds>({
706729
if (hasAlphaSort) {
707730
grouped.some(
708731
groupMember =>
709-
!checkAlphaSort(groupMember, alphaSortIsCaseSensitive),
732+
!checkAlphaSort(groupMember, order as AlphabeticalOrder),
710733
);
711734
}
712735
} else if (hasAlphaSort) {
713-
checkAlphaSort(members, alphaSortIsCaseSensitive);
736+
checkAlphaSort(members, order as AlphabeticalOrder);
714737
}
715738
}
716739

‎packages/eslint-plugin/tests/rules/member-ordering-alphabetically-case-insensitive-order.test.ts ‎packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-case-insensitive-order.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { TSESLint } from '@typescript-eslint/utils';
22

3-
import type { MessageIds, Options } from '../../src/rules/member-ordering';
4-
import rule, { defaultOrder } from '../../src/rules/member-ordering';
5-
import { RuleTester } from '../RuleTester';
3+
import type { MessageIds, Options } from '../../../src/rules/member-ordering';
4+
import rule, { defaultOrder } from '../../../src/rules/member-ordering';
5+
import { RuleTester } from '../../RuleTester';
66

77
const ruleTester = new RuleTester({
88
parser: '@typescript-eslint/parser',

‎packages/eslint-plugin/tests/rules/member-ordering-alphabetically-order.test.ts ‎packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { TSESLint } from '@typescript-eslint/utils';
22

3-
import type { MessageIds, Options } from '../../src/rules/member-ordering';
4-
import rule, { defaultOrder } from '../../src/rules/member-ordering';
5-
import { RuleTester } from '../RuleTester';
3+
import type { MessageIds, Options } from '../../../src/rules/member-ordering';
4+
import rule, { defaultOrder } from '../../../src/rules/member-ordering';
5+
import { RuleTester } from '../../RuleTester';
66

77
const ruleTester = new RuleTester({
88
parser: '@typescript-eslint/parser',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import rule from '../../../src/rules/member-ordering';
2+
import { RuleTester } from '../../RuleTester';
3+
4+
const ruleTester = new RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
});
7+
8+
ruleTester.run('member-ordering-natural-order', rule, {
9+
valid: [
10+
{
11+
code: `
12+
interface Example {
13+
1: number;
14+
5: number;
15+
10: number;
16+
}
17+
`,
18+
options: [
19+
{
20+
default: {
21+
order: 'natural-case-insensitive',
22+
},
23+
},
24+
],
25+
},
26+
{
27+
code: `
28+
interface Example {
29+
new (): unknown;
30+
31+
a1(): void;
32+
a5(): void;
33+
a10(): void;
34+
B1(): void;
35+
B5(): void;
36+
B10(): void;
37+
38+
a1: number;
39+
a5: number;
40+
a10: number;
41+
B1: number;
42+
B5: number;
43+
B10: number;
44+
}
45+
`,
46+
options: [
47+
{
48+
default: {
49+
memberTypes: ['constructor', 'method', 'field'],
50+
order: 'natural-case-insensitive',
51+
},
52+
},
53+
],
54+
},
55+
],
56+
invalid: [
57+
{
58+
code: `
59+
interface Example {
60+
1: number;
61+
10: number;
62+
5: number;
63+
}
64+
`,
65+
errors: [
66+
{
67+
messageId: 'incorrectOrder',
68+
data: {
69+
beforeMember: 10,
70+
member: 5,
71+
},
72+
line: 5,
73+
column: 3,
74+
},
75+
],
76+
options: [
77+
{
78+
default: {
79+
order: 'natural-case-insensitive',
80+
},
81+
},
82+
],
83+
},
84+
85+
{
86+
code: `
87+
interface Example {
88+
new (): unknown;
89+
90+
a1(): void;
91+
a10(): void;
92+
a5(): void;
93+
B5(): void;
94+
B10(): void;
95+
B1(): void;
96+
97+
a5: number;
98+
a10: number;
99+
B1: number;
100+
a1: number;
101+
B5: number;
102+
B10: number;
103+
}
104+
`,
105+
errors: [
106+
{
107+
column: 3,
108+
data: {
109+
beforeMember: 'a10',
110+
member: 'a5',
111+
},
112+
line: 7,
113+
messageId: 'incorrectOrder',
114+
},
115+
{
116+
column: 3,
117+
data: {
118+
beforeMember: 'B10',
119+
member: 'B1',
120+
},
121+
line: 10,
122+
messageId: 'incorrectOrder',
123+
},
124+
],
125+
options: [
126+
{
127+
default: {
128+
memberTypes: ['constructor', 'method', 'field'],
129+
order: 'natural-case-insensitive',
130+
},
131+
},
132+
],
133+
},
134+
],
135+
});

0 commit comments

Comments
 (0)
Please sign in to comment.