-
-
Notifications
You must be signed in to change notification settings - Fork 243
fix(eslint-plugin): [sort-ngmodule-metadata-arrays]: properly handle multiple fixes at once #695
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
import { Selectors } from '@angular-eslint/utils'; | ||
import type { TSESTree } from '@typescript-eslint/experimental-utils'; | ||
import { ASTUtils, partition, Selectors } from '@angular-eslint/utils'; | ||
import type { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
import { ASTUtils as TSESLintASTUtils } from '@typescript-eslint/experimental-utils'; | ||
import { createESLintRule } from '../utils/create-eslint-rule'; | ||
|
||
type Options = []; | ||
export type MessageIds = 'sortNgmoduleMetadataArrays'; | ||
export type MessageIds = 'sortNgmoduleMetadataArrays' | 'suggestFix'; | ||
export const RULE_NAME = 'sort-ngmodule-metadata-arrays'; | ||
|
||
export default createESLintRule<Options, MessageIds>({ | ||
|
@@ -16,39 +16,106 @@ export default createESLintRule<Options, MessageIds>({ | |
'Ensures ASC alphabetical order for `NgModule` metadata arrays for easy visual scanning', | ||
category: 'Best Practices', | ||
recommended: false, | ||
suggestion: true, | ||
}, | ||
fixable: 'code', | ||
schema: [], | ||
messages: { | ||
sortNgmoduleMetadataArrays: | ||
'`NgModule` metadata arrays should be sorted in ASC alphabetical order', | ||
suggestFix: 'Sort members (WARNING! comments will probably be messed up)', | ||
}, | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
|
||
return { | ||
[`${Selectors.MODULE_CLASS_DECORATOR} Property ArrayExpression`]({ | ||
elements, | ||
}: TSESTree.ArrayExpression) { | ||
const unorderedNodes = elements | ||
.filter(TSESLintASTUtils.isIdentifier) | ||
.map((current, index, list) => [current, list[index + 1]]) | ||
.find(([current, next]) => { | ||
return next && current.name.localeCompare(next.name) === 1; | ||
}); | ||
const [expressions, identifiers] = partition( | ||
elements, | ||
TSESLintASTUtils.isIdentifier, | ||
); | ||
const firstUnorderedNode = getFirstUnderedNode(identifiers); | ||
|
||
if (!unorderedNodes) return; | ||
if (!firstUnorderedNode) { | ||
return; | ||
} | ||
|
||
const [unorderedNode, nextNode] = unorderedNodes; | ||
const hasComments = elements.some((element) => | ||
ASTUtils.hasComments(sourceCode, element), | ||
); | ||
const fix: TSESLint.ReportFixFunction = (fixer) => | ||
getFixes(sourceCode, fixer, elements, expressions, identifiers); | ||
context.report({ | ||
node: nextNode, | ||
node: firstUnorderedNode, | ||
messageId: 'sortNgmoduleMetadataArrays', | ||
fix: (fixer) => [ | ||
fixer.replaceText(unorderedNode, nextNode.name), | ||
fixer.replaceText(nextNode, unorderedNode.name), | ||
], | ||
...(hasComments | ||
? { suggest: [{ messageId: 'suggestFix', fix }] } | ||
: { fix }), | ||
Comment on lines
+55
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I even tried to use Example of hard cases can be found here, for example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing that came to mind now is, since this is something we have control over and wouldn't cause any compilation errors if applied, we could add an option called something like Let me know what you think, so I can send another PR for this if that's the case. |
||
}); | ||
}, | ||
}; | ||
}, | ||
}); | ||
|
||
function getFirstUnderedNode(identifiers: readonly TSESTree.Identifier[]) { | ||
return identifiers.find((identifier, index, array) => { | ||
const previousElement = array[index - 1]; | ||
return ( | ||
previousElement && | ||
previousElement.name.localeCompare(identifier.name) === 1 | ||
); | ||
}); | ||
} | ||
|
||
function getFixes( | ||
sourceCode: Readonly<TSESLint.SourceCode>, | ||
fixer: TSESLint.RuleFixer, | ||
elements: readonly TSESTree.Expression[], | ||
expressions: readonly TSESTree.Expression[], | ||
identifiers: readonly TSESTree.Identifier[], | ||
) { | ||
const sortedIdentifiers = [...identifiers].sort((one, other) => | ||
one.name.localeCompare(other.name), | ||
); | ||
const sortedExpressions = [...sortedIdentifiers, ...expressions]; | ||
const text = sortedExpressions.reduce(toText(sourceCode, elements), ''); | ||
return fixer.replaceTextRange(getRangeFromFirstToLast(elements), text); | ||
} | ||
|
||
function toText( | ||
sourceCode: Readonly<TSESLint.SourceCode>, | ||
elements: readonly TSESTree.Expression[], | ||
) { | ||
return ( | ||
accumulator: string, | ||
expression: TSESTree.Expression, | ||
index: number, | ||
) => { | ||
const isLastElement = index === elements.length - 1; | ||
const textAfterExpression = isLastElement | ||
? '' | ||
: sourceCode | ||
.getText() | ||
.slice(...getRangeBetweenCurrentAndNext(elements, index)); | ||
return `${accumulator}${sourceCode.getText( | ||
expression, | ||
)}${textAfterExpression}`; | ||
}; | ||
} | ||
|
||
function getRangeBetweenCurrentAndNext( | ||
elements: readonly TSESTree.Expression[], | ||
index: number, | ||
): TSESTree.Range { | ||
return [elements[index].range[1], elements[index + 1].range[0]]; | ||
} | ||
|
||
function getRangeFromFirstToLast( | ||
elements: readonly TSESTree.Expression[], | ||
): TSESTree.Range { | ||
return [elements[0].range[0], elements[elements.length - 1].range[1]]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, suggest a better messageId/description if that's the case.