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', + }, + ], + }, + ], +});