Skip to content

Commit

Permalink
feat(eslint-plugin): sort members alphabetically
Browse files Browse the repository at this point in the history
  • Loading branch information
timkraut committed Mar 5, 2019
1 parent 00eae48 commit dc6d9ab
Show file tree
Hide file tree
Showing 3 changed files with 383 additions and 61 deletions.
152 changes: 91 additions & 61 deletions packages/eslint-plugin/docs/rules/member-ordering.md
Expand Up @@ -5,83 +5,113 @@ expressions easier to read, navigate and edit.

## Rule Details

This rule aims to standardise the way interfaces, type literals, classes and class expressions are structured.
This rule aims to standardise the way interfaces, type literals, classes and class expressions are structured and ordered.

It allows to group members by their type (e.g. `public-static-field`, `protected-static-field`, `private-static-field`, `public-instance-field`, ...). By default, their order is the same inside `classes`, `classExpressions`, `interfaces` and `typeLiterals`. It is possible to define the order for any of those individually or to change the default order for all of them by setting the `default` option.

## Options

This rule, in its default state, does not require any argument, in which case the following order is enforced:
The configuration options look as follows:

```js
{
default?: memberTypes[] | never
classes?: memberTypes[] | never
classExpressions?: memberTypes[] | never
interfaces?: memberTypes[] | never
typeLiterals?: memberTypes[] | never
}
```

See below for the possible definitions of `memberTypes`.

### Member types (granular form)

There are multiple ways to specify the member types. The most explicit and granular form is the following:

#### Fields

- `public-static-field`
- `protected-static-field`
- `private-static-field`
- `public-instance-field`
- `protected-instance-field`
- `private-instance-field`
- `public-field` (ignores scope)
- `protected-field` (ignores scope)
- `private-field` (ignores scope)
- `static-field` (ignores accessibility)
- `instance-field` (ignores accessibility)
- `field` (ignores scope and/or accessibility)
- `constructor` (ignores scope and/or accessibility)

#### Constructors

- `public-constructor`
- `protected-constructor`
- `private-constructor`

#### Methods

- `public-static-method`
- `protected-static-method`
- `private-static-method`
- `public-instance-method`
- `protected-instance-method`
- `private-instance-method`
- `public-method` (ignores scope)
- `protected-method` (ignores scope)
- `private-method` (ignores scope)
- `static-method` (ignores accessibility)
- `instance-method` (ignores accessibility)
- `method` (ignores scope and/or accessibility)

The rule can also take one or more of the following options:

- `default`, use this to change the default order (used when no specific configuration has been provided).
- `classes`, use this to change the order in classes.
- `classExpressions`, use this to change the order in class expressions.
- `interfaces`, use this to change the order in interfaces.
- `typeLiterals`, use this to change the order in type literals.

### default

Disable using `never` or use one of the following values to specify an order:

- Fields:
`public-static-field`
`protected-static-field`
`private-static-field`
`public-instance-field`
`protected-instance-field`
`private-instance-field`
`public-field` (= public-_-field)
`protected-field` (= protected-_-field)
`private-field` (= private-_-field)
`static-field` (= _-static-field)
`instance-field` (= \*-instance-field)
`field` (= all)

- Constructors:
`public-constructor`
`protected-constructor`
`private-constructor`
`constructor` (= \*-constructor)

- Methods:
`public-static-method`
`protected-static-method`
`private-static-method`
`public-instance-method`
`protected-instance-method`
`private-instance-method`
`public-method` (= public-_-method)
`protected-method` (= protected-_-method)
`private-method` (= private-_-method)
`static-method` (= _-static-method)
`instance-method` (= \*-instance-method)
`method` (= all)

Note: If nothing else is specified, this is the default order used when this rule is enabled.

### Member group types ignoring scope

It is also possible to group member types as follows, ignoring their scope (`static`, `instance`).

#### Fields

- `public-field` (= `public-static-field`, `public-instance-field`)
- `protected-field` (= `protected-static-field`, `protected-instance-field`)
- `private-field` (= `private-static-field`, `protected-instance-field`)

#### Constructors

Only the accessibility of constructors is configurable. See below.

#### Methods

- `public-method` (= `public-static-method`, `public-instance-method`)
- `protected-method` (= `protected-static-method`, `protected-instance-method`)
- `private-method` (= `private-static-method`, `private-instance-method`)

### Member group types ignoring accessibility

Another option is to group the member types as follows, ignoring their accessibility (`public`, `protected`, `private`).

#### Fields

- `static-field` (= `public-static-field`, `protected-static-field`, `private-static-field`)
- `instance-field` (= `public-instance-field`, `protected-static-field`, `private-static-field`)

#### Constructors

- `constructor` (= `public-constructor`, `protected-constructor`, `private-constructor`)

#### Methods

- `static-method` (= `public-static-method`, `protected-static-method`, `private-static-method`)
- `instance-method` (= `public-instance-method`, `protected-instance-method`, `private-static-method`)

### Member group types ignoring scope and accessibility

The third grouping option is to ignore both scope (`static`, `instance`) and accessibility (`public`, `protected`, `private`).

#### Fields

- `field` (= `public-static-field`, `protected-static-field`, `private-static-field`, `public-instance-field`, `protected-instance-field`, `private-instance-field`)

#### Constructors

- Only the accessibility of constructors is configurable. See above.

#### Methods

- `method` (= `public-static-method`, `protected-static-method`, `private-static-method`, `public-instance-method`, `protected-instance-method`, `private-instance-method`)

## Examples

### Default configuration

Examples of **incorrect** code for the `{ "default": [...] }` option:

Expand Down
212 changes: 212 additions & 0 deletions packages/eslint-plugin/src/rules/sort-interface-members.ts
@@ -0,0 +1,212 @@
/**
* @fileoverview Forbids unsorted interface members
*/

import * as util from '../util';
import {
Identifier,
TSCallSignatureDeclaration,
TSConstructSignatureDeclaration,
TSIndexSignature,
TSMethodSignature,
TSPropertySignature,
TSTypeAnnotation,
TSTypeReference,
TypeElement,
} from '@typescript-eslint/typescript-estree/dist/ts-estree/ts-estree';

type Options = [];
type MessageIds = 'notSorted';

function isPropertySignature(
member: TypeElement,
): member is TSPropertySignature {
return (<TSPropertySignature>member).type === 'TSPropertySignature';
}

function isMethodSignature(member: TypeElement): member is TSMethodSignature {
return (<TSMethodSignature>member).type === 'TSMethodSignature';
}

function isIndexSignature(member: TypeElement): member is TSIndexSignature {
return (<TSIndexSignature>member).type === 'TSIndexSignature';
}

function isConstructSignatureDeclaration(
member: TypeElement,
): member is TSConstructSignatureDeclaration {
return (
(<TSConstructSignatureDeclaration>member).type ===
'TSConstructSignatureDeclaration'
);
}

function isCallSignatureDeclaration(
member: TypeElement,
): member is TSCallSignatureDeclaration {
return (
(<TSCallSignatureDeclaration>member).type === 'TSCallSignatureDeclaration'
);
}

export default util.createRule<Options, MessageIds>({
name: 'sort-interface-members',
meta: {
type: 'suggestion',
docs: {
description: 'Forbids unsorted interface members',
category: 'Stylistic Issues',
recommended: false,
},
schema: [],
messages: {
notSorted: 'The interface members are not sorted alphabetically.',
},
},
defaultOptions: [],
create(context) {
return {
TSInterfaceBody(node) {
const members = node.body;
const propertySignatures: TSPropertySignature[] = [];
const methodSignatures: TSMethodSignature[] = [];
const indexSignatures: TSIndexSignature[] = [];
const constructSignatureDeclarations: TSConstructSignatureDeclaration[] = [];
const callSignatureDeclarations: TSCallSignatureDeclaration[] = [];

// TODO This algorithm assumes an order of TSPropertySignature > TSMethodSignature > TSIndexSignature > TSConstructSignatureDeclaration > TSCallSignatureDeclaration - it is only used to evaluate if it works alongside the member-ordering rule
for (let i = 0; i < members.length; i++) {
if (isPropertySignature(members[i])) {
if (
methodSignatures.length > 0 ||
indexSignatures.length > 0 ||
constructSignatureDeclarations.length > 0 ||
callSignatureDeclarations.length > 0
) {
return context.report({
messageId: 'notSorted',
node: members[i],
});
}

propertySignatures.push(members[i] as TSPropertySignature);
} else if (isMethodSignature(members[i])) {
if (
indexSignatures.length > 0 ||
constructSignatureDeclarations.length > 0 ||
callSignatureDeclarations.length > 0
) {
return context.report({
messageId: 'notSorted',
node: members[i],
});
}

methodSignatures.push(members[i] as TSMethodSignature);
} else if (isIndexSignature(members[i])) {
if (
constructSignatureDeclarations.length > 0 ||
callSignatureDeclarations.length > 0
) {
return context.report({
messageId: 'notSorted',
node: members[i],
});
}

indexSignatures.push(members[i] as TSIndexSignature);
} else if (isConstructSignatureDeclaration(members[i])) {
if (callSignatureDeclarations.length > 0) {
return context.report({
messageId: 'notSorted',
node: members[i],
});
}

constructSignatureDeclarations.push(members[
i
] as TSConstructSignatureDeclaration);
} else if (isCallSignatureDeclaration(members[i])) {
callSignatureDeclarations.push(members[
i
] as TSCallSignatureDeclaration);
}
}

for (let i = 0; i < propertySignatures.length - 1; i++) {
const currentItem = <Identifier>propertySignatures[i].key;
const nextItem = <Identifier>propertySignatures[i + 1].key;

if (currentItem.name > nextItem.name) {
return context.report({
messageId: 'notSorted',
node: currentItem,
});
}
}

for (let i = 0; i < methodSignatures.length - 1; i++) {
const currentItem = <Identifier>methodSignatures[i].key;
const nextItem = <Identifier>methodSignatures[i + 1].key;

if (currentItem.name > nextItem.name) {
return context.report({
messageId: 'notSorted',
node: currentItem,
});
}
}

for (let i = 0; i < indexSignatures.length - 1; i++) {
const currentItem = <Identifier>indexSignatures[i].parameters[0];
const nextItem = <Identifier>indexSignatures[i + 1].parameters[0];

if (currentItem.name > nextItem.name) {
return context.report({
messageId: 'notSorted',
node: currentItem,
});
}
}

for (let i = 0; i < constructSignatureDeclarations.length - 1; i++) {
const currentItem = (<TSTypeReference>(
(<TSTypeAnnotation>constructSignatureDeclarations[i].returnType)
.typeAnnotation
)).typeName;
const nextItem = (<TSTypeReference>(
(<TSTypeAnnotation>constructSignatureDeclarations[i + 1].returnType)
.typeAnnotation
)).typeName;

if (currentItem > nextItem) {
return context.report({
messageId: 'notSorted',
node: currentItem,
});
}
}

for (let i = 0; i < callSignatureDeclarations.length - 1; i++) {
const currentItem = (<TSTypeReference>(
(<TSTypeAnnotation>callSignatureDeclarations[i].returnType)
.typeAnnotation
)).typeName;
const nextItem = (<TSTypeReference>(
(<TSTypeAnnotation>callSignatureDeclarations[i + 1].returnType)
.typeAnnotation
)).typeName;

if (currentItem > nextItem) {
return context.report({
messageId: 'notSorted',
node: currentItem,
});
}
}

return; // No rule violation found
},
};
},
});

0 comments on commit dc6d9ab

Please sign in to comment.