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): add sort-interface-members rule
- Loading branch information
Showing
2 changed files
with
292 additions
and
0 deletions.
There are no files selected for viewing
212 changes: 212 additions & 0 deletions
212
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,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
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', | ||
}, | ||
], | ||
}, | ||
], | ||
}); |