Skip to content

Commit

Permalink
feat(eslint-plugin): add sort-interface-members rule
Browse files Browse the repository at this point in the history
  • Loading branch information
timkraut committed Feb 20, 2019
1 parent 1b3d31e commit b1256a9
Show file tree
Hide file tree
Showing 2 changed files with 292 additions and 0 deletions.
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
},
};
},
});
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 b1256a9

Please sign in to comment.