diff --git a/packages/eslint-plugin/docs/rules/member-ordering.md b/packages/eslint-plugin/docs/rules/member-ordering.md index a0b8f8983565..8bc0934b4218 100644 --- a/packages/eslint-plugin/docs/rules/member-ordering.md +++ b/packages/eslint-plugin/docs/rules/member-ordering.md @@ -5,11 +5,31 @@ 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` @@ -17,71 +37,81 @@ This rule, in its default state, does not require any argument, in which case th - `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: diff --git a/packages/eslint-plugin/src/rules/sort-interface-members.ts b/packages/eslint-plugin/src/rules/sort-interface-members.ts new file mode 100644 index 000000000000..a90d15f12898 --- /dev/null +++ b/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 (member).type === 'TSPropertySignature'; +} + +function isMethodSignature(member: TypeElement): member is TSMethodSignature { + return (member).type === 'TSMethodSignature'; +} + +function isIndexSignature(member: TypeElement): member is TSIndexSignature { + return (member).type === 'TSIndexSignature'; +} + +function isConstructSignatureDeclaration( + member: TypeElement, +): member is TSConstructSignatureDeclaration { + return ( + (member).type === + 'TSConstructSignatureDeclaration' + ); +} + +function isCallSignatureDeclaration( + member: TypeElement, +): member is TSCallSignatureDeclaration { + return ( + (member).type === 'TSCallSignatureDeclaration' + ); +} + +export default util.createRule({ + 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 = propertySignatures[i].key; + const nextItem = 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 = methodSignatures[i].key; + const nextItem = 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 = indexSignatures[i].parameters[0]; + const nextItem = 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 = (( + (constructSignatureDeclarations[i].returnType) + .typeAnnotation + )).typeName; + const nextItem = (( + (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 = (( + (callSignatureDeclarations[i].returnType) + .typeAnnotation + )).typeName; + const nextItem = (( + (callSignatureDeclarations[i + 1].returnType) + .typeAnnotation + )).typeName; + + if (currentItem > nextItem) { + return context.report({ + messageId: 'notSorted', + node: currentItem, + }); + } + } + + return; // No rule violation found + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/sort-interface-members.test.ts b/packages/eslint-plugin/tests/rules/sort-interface-members.test.ts new file mode 100644 index 000000000000..4a45ee31f2b6 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/sort-interface-members.test.ts @@ -0,0 +1,80 @@ +import { RuleTester } from '../RuleTester'; +import rule from '../../src/rules/sort-interface-members'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +// TODO Add tests for lowercase/uppercase letters +// TODO Add tests for special characters +// TODO Add tests for the order of the groups +// TODO Add tests for the order inside of the groups +ruleTester.run('sort-interface-members', rule, { + valid: [ + ` +interface Foo { + a : string; + a() : string; + [a: string]: string; + new () : A; + () : A; +} + `, + ` +interface Foo { + a : string; + a : string; + a() : string; + a() : string; + [a: string]: string; + [a: string]: string; + new () : A; + new () : A; + () : A; + () : A; +} + `, + ` +interface Foo { + a : string; + b : string; + a() : string; + b() : string; + [a: string]: string; + [b: string]: string; + new () : A; + new () : B; + () : A; + () : B; +} + `, + ], + invalid: [ + { + code: ` +interface Foo { + a() : string; + a : string; +} + `, + errors: [ + { + messageId: 'notSorted', + }, + ], + }, + { + code: ` +interface Foo { + b : string; + a : string; +} + `, + errors: [ + { + messageId: 'notSorted', + }, + ], + }, + ], +});