Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): sort members alphabetically
- Loading branch information
Showing
2 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
199 changes: 199 additions & 0 deletions
199
packages/eslint-plugin/src/rules/sort-interface-members.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
80
packages/eslint-plugin/tests/rules/sort-interface-members.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
}, | ||
], | ||
}, | ||
], | ||
}); |