Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [member-ordering] add natural sort order #5662

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/eslint-plugin/docs/rules/member-ordering.md
Expand Up @@ -24,7 +24,12 @@ type OrderConfig = MemberType[] | SortedOrderConfig | 'never';

interface SortedOrderConfig {
memberTypes?: MemberType[] | 'never';
order: 'alphabetically' | 'alphabetically-case-insensitive' | 'as-written';
order:
| 'alphabetically'
| 'alphabetically-case-insensitive'
| 'as-written'
| 'natural'
| 'natural-case-insensitive';
}

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

### Orders

The `order` value specifies what order members should be within a group.
It defaults to `as-written`, meaning any order is fine.
Other allowed values are:

- `alphabetically`: Sorted in a-z alphabetical order, directly using string `<` comparison (so `B` comes before `a`)
- `alphabetically-case-insensitive`: Sorted in a-z alphabetical order, ignoring case (so `a` comes before `B`)
- `natural`: Same as `alphabetically`, but using [`natural-compare-lite`](https://github.com/litejs/natural-compare-lite) for more friendly sorting of numbers
- `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

### Default configuration

The default configuration looks as follows:
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/package.json
Expand Up @@ -49,6 +49,7 @@
"@typescript-eslint/utils": "5.40.0",
"debug": "^4.3.4",
"ignore": "^5.2.0",
"natural-compare-lite": "^1.4.0",
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
"regexpp": "^3.2.0",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
Expand All @@ -57,6 +58,7 @@
"@types/debug": "*",
"@types/json-schema": "*",
"@types/marked": "*",
"@types/natural-compare-lite": "^1.4.0",
"@types/prettier": "*",
"chalk": "^5.0.1",
"json-schema": "*",
Expand Down
53 changes: 38 additions & 15 deletions packages/eslint-plugin/src/rules/member-ordering.ts
@@ -1,5 +1,6 @@
import type { JSONSchema, TSESLint, TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import naturalCompare from 'natural-compare-lite';

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

Expand Down Expand Up @@ -34,10 +35,13 @@ type BaseMemberType =

type MemberType = BaseMemberType | BaseMemberType[];

type Order =
type AlphabeticalOrder =
| 'alphabetically'
| 'alphabetically-case-insensitive'
| 'as-written';
| 'natural'
| 'natural-case-insensitive';
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved

type Order = AlphabeticalOrder | 'as-written';

interface SortedOrderConfig {
memberTypes?: MemberType[] | 'never';
Expand Down Expand Up @@ -87,7 +91,13 @@ const objectConfig = (memberTypes: MemberType[]): JSONSchema.JSONSchema4 => ({
},
order: {
type: 'string',
enum: ['alphabetically', 'alphabetically-case-insensitive', 'as-written'],
enum: [
'alphabetically',
'alphabetically-case-insensitive',
'as-written',
'natural',
'natural-case-insensitive',
],
},
},
additionalProperties: false,
Expand Down Expand Up @@ -629,7 +639,7 @@ export default util.createRule<Options, MessageIds>({
*/
function checkAlphaSort(
members: Member[],
caseSensitive: boolean,
order: AlphabeticalOrder,
): boolean {
let previousName = '';
let isCorrectlySorted = true;
Expand All @@ -640,11 +650,7 @@ export default util.createRule<Options, MessageIds>({

// Note: Not all members have names
if (name) {
if (
caseSensitive
? name < previousName
: name.toLowerCase() < previousName.toLowerCase()
) {
if (naturalOutOfOrder(name, previousName, order)) {
context.report({
node: member,
messageId: 'incorrectOrder',
Expand All @@ -664,6 +670,25 @@ export default util.createRule<Options, MessageIds>({
return isCorrectlySorted;
}

function naturalOutOfOrder(
name: string,
previousName: string,
order: AlphabeticalOrder,
): boolean {
switch (order) {
case 'alphabetically':
return name < previousName;
case 'alphabetically-case-insensitive':
return name.toLowerCase() < previousName.toLowerCase();
case 'natural':
return naturalCompare(name, previousName) !== 1;
case 'natural-case-insensitive':
return (
naturalCompare(name.toLowerCase(), previousName.toLowerCase()) !== 1
);
}
}

/**
* Validates if all members are correctly sorted.
*
Expand All @@ -681,7 +706,7 @@ export default util.createRule<Options, MessageIds>({
}

// Standardize config
let order: Order | null = null;
let order: Order | undefined;
let memberTypes;

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

const hasAlphaSort = order?.startsWith('alphabetically');
const alphaSortIsCaseSensitive =
order !== 'alphabetically-case-insensitive';
const hasAlphaSort = !!(order && order !== 'as-written');

// Check order
if (Array.isArray(memberTypes)) {
Expand All @@ -706,11 +729,11 @@ export default util.createRule<Options, MessageIds>({
if (hasAlphaSort) {
grouped.some(
groupMember =>
!checkAlphaSort(groupMember, alphaSortIsCaseSensitive),
!checkAlphaSort(groupMember, order as AlphabeticalOrder),
);
}
} else if (hasAlphaSort) {
checkAlphaSort(members, alphaSortIsCaseSensitive);
checkAlphaSort(members, order as AlphabeticalOrder);
}
}

Expand Down
@@ -0,0 +1,135 @@
import rule from '../../src/rules/member-ordering';
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
import { RuleTester } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('member-ordering-natural-order', rule, {
valid: [
{
code: `
interface Example {
1: number;
5: number;
10: number;
}
`,
options: [
{
default: {
order: 'natural-case-insensitive',
},
},
],
},
{
code: `
interface Example {
new (): unknown;

a1(): void;
a5(): void;
a10(): void;
B1(): void;
B5(): void;
B10(): void;

a1: number;
a5: number;
a10: number;
B1: number;
B5: number;
B10: number;
}
`,
options: [
{
default: {
memberTypes: ['constructor', 'method', 'field'],
order: 'natural-case-insensitive',
},
},
],
},
],
invalid: [
{
code: `
interface Example {
1: number;
10: number;
5: number;
}
`,
errors: [
{
messageId: 'incorrectOrder',
data: {
beforeMember: 10,
member: 5,
},
line: 5,
column: 3,
},
],
options: [
{
default: {
order: 'natural-case-insensitive',
},
},
],
},

{
code: `
interface Example {
new (): unknown;

a1(): void;
a10(): void;
a5(): void;
B5(): void;
B10(): void;
B1(): void;

a5: number;
a10: number;
B1: number;
a1: number;
B5: number;
B10: number;
}
`,
errors: [
{
column: 3,
data: {
beforeMember: 'a10',
member: 'a5',
},
line: 7,
messageId: 'incorrectOrder',
},
{
column: 3,
data: {
beforeMember: 'B10',
member: 'B1',
},
line: 10,
messageId: 'incorrectOrder',
},
],
options: [
{
default: {
memberTypes: ['constructor', 'method', 'field'],
order: 'natural-case-insensitive',
},
},
],
},
],
});