diff --git a/packages/eslint-plugin/docs/rules/member-ordering.md b/packages/eslint-plugin/docs/rules/member-ordering.md index a0b8f8983565..1b689648f6a7 100644 --- a/packages/eslint-plugin/docs/rules/member-ordering.md +++ b/packages/eslint-plugin/docs/rules/member-ordering.md @@ -5,83 +5,109 @@ 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: - -- `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) -- `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) +The configuration options look as follows: + +```js +{ + default?: Array | never + classes?: Array | never + classExpressions?: Array | never + interfaces?: Array | never + typeLiterals?: Array | 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' + +// 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' +``` + +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', 'private-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-instance-field', 'private-instance-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-instance-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..7d7c53a8cd79 --- /dev/null +++ b/packages/eslint-plugin/src/rules/sort-interface-members.ts @@ -0,0 +1,199 @@ +/** + * @fileoverview Forbids unsorted interface members + */ + +import * as util from '../util'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree'; + +type Options = []; +type MessageIds = 'notSorted'; + +function isPropertySignature( + member: TSESTree.TypeElement, +): member is TSESTree.TSPropertySignature { + return member.type === AST_NODE_TYPES.TSPropertySignature; +} + +function isMethodSignature( + member: TSESTree.TypeElement, +): member is TSESTree.TSMethodSignature { + return member.type === AST_NODE_TYPES.TSMethodSignature; +} + +function isIndexSignature( + member: TSESTree.TypeElement, +): member is TSESTree.TSIndexSignature { + return member.type === AST_NODE_TYPES.TSIndexSignature; +} + +function isConstructSignatureDeclaration( + member: TSESTree.TypeElement, +): member is TSESTree.TSConstructSignatureDeclaration { + return member.type === AST_NODE_TYPES.TSConstructSignatureDeclaration; +} + +function isCallSignatureDeclaration( + member: TSESTree.TypeElement, +): member is TSESTree.TSCallSignatureDeclaration { + return member.type === AST_NODE_TYPES.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: TSESTree.TSPropertySignature[] = []; + const methodSignatures: TSESTree.TSMethodSignature[] = []; + const indexSignatures: TSESTree.TSIndexSignature[] = []; + const constructSignatureDeclarations: TSESTree.TSConstructSignatureDeclaration[] = []; + const callSignatureDeclarations: TSESTree.TSCallSignatureDeclaration[] = []; + + // TODO This algorithm assumes an order of TSESTree.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 TSESTree.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 TSESTree.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 TSESTree.TSIndexSignature); + } else if (isConstructSignatureDeclaration(members[i])) { + if (callSignatureDeclarations.length > 0) { + return context.report({ + messageId: 'notSorted', + node: members[i], + }); + } + + constructSignatureDeclarations.push(members[ + i + ] as TSESTree.TSConstructSignatureDeclaration); + } else if (isCallSignatureDeclaration(members[i])) { + callSignatureDeclarations.push(members[ + i + ] as TSESTree.TSCallSignatureDeclaration); + } + } + + for (let i = 0; i < propertySignatures.length - 1; i++) { + const currentItem = propertySignatures[i].key as TSESTree.Identifier; + const nextItem = propertySignatures[i + 1].key as TSESTree.Identifier; + + 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 as TSESTree.Identifier; + const nextItem = methodSignatures[i + 1].key as TSESTree.Identifier; + + 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] as TSESTree.Identifier; + const nextItem = indexSignatures[i + 1] + .parameters[0] as TSESTree.Identifier; + + 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 as TSESTree.TSTypeAnnotation) + .typeAnnotation as TSESTree.TSTypeReference).typeName; + const nextItem = ((constructSignatureDeclarations[i + 1] + .returnType as TSESTree.TSTypeAnnotation) + .typeAnnotation as TSESTree.TSTypeReference).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 as TSESTree.TSTypeAnnotation) + .typeAnnotation as TSESTree.TSTypeReference).typeName; + const nextItem = ((callSignatureDeclarations[i + 1] + .returnType as TSESTree.TSTypeAnnotation) + .typeAnnotation as TSESTree.TSTypeReference).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', + }, + ], + }, + ], +});