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 13, 2019
1 parent 2946e63 commit f35ce10
Show file tree
Hide file tree
Showing 2 changed files with 279 additions and 0 deletions.
199 changes: 199 additions & 0 deletions 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<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: 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
},
};
},
});
80 changes: 80 additions & 0 deletions 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',
},
],
},
],
});

0 comments on commit f35ce10

Please sign in to comment.