117616: Created custom sort-standalone-imports rule

This commit is contained in:
Alexandre Vryghem
2024-12-13 00:23:24 +01:00
parent 6017537107
commit aed0460cfe
5 changed files with 532 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ import {
} from '../../util/structure';
/* eslint-disable import/no-namespace */
import * as aliasImports from './alias-imports';
import * as sortStandaloneImports from './sort-standalone-imports';
import * as themedComponentClasses from './themed-component-classes';
import * as themedComponentSelectors from './themed-component-selectors';
import * as themedComponentUsages from './themed-component-usages';
@@ -20,6 +21,7 @@ import * as uniqueDecorators from './unique-decorators';
const index = [
aliasImports,
sortStandaloneImports,
themedComponentClasses,
themedComponentSelectors,
themedComponentUsages,

View File

@@ -0,0 +1,306 @@
import {
ASTUtils as TSESLintASTUtils,
ESLintUtils,
TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
import {
DSpaceESLintRuleInfo,
NamedTests,
OptionDoc,
} from '../../util/structure';
const DEFAULT_LOCALE = 'en-US';
const DEFAULT_MAX_SIZE = 0;
const DEFAULT_SPACE_INDENT_AMOUNT = 2;
const DEFAULT_TRAILING_COMMA = true;
export enum Message {
SORT_STANDALONE_IMPORTS_ARRAYS = 'sortStandaloneImportsArrays',
}
export interface UniqueDecoratorsOptions {
locale: string;
maxItems: number;
indent: number;
trailingComma: boolean;
}
export interface UniqueDecoratorsDocOptions {
locale: OptionDoc;
maxItems: OptionDoc;
indent: OptionDoc;
trailingComma: OptionDoc;
}
export const info: DSpaceESLintRuleInfo<[UniqueDecoratorsOptions], [UniqueDecoratorsDocOptions]> = {
name: 'sort-standalone-imports',
meta: {
docs: {
description: 'Sorts the standalone `@Component` imports alphabetically',
},
messages: {
[Message.SORT_STANDALONE_IMPORTS_ARRAYS]: 'Standalone imports should be sorted alphabetically',
},
fixable: 'code',
type: 'problem',
schema: [
{
type: 'object',
properties: {
locale: {
type: 'string',
},
maxItems: {
type: 'number',
},
indent: {
type: 'number',
},
trailingComma: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
},
optionDocs: [
{
locale: {
title: '`locale`',
description: 'The locale used to sort the imports.',
},
maxItems: {
title: '`maxItems`',
description: 'The maximum number of imports that should be displayed before each import is separated onto its own line.',
},
indent: {
title: '`indent`',
description: 'The indent used for the project.',
},
trailingComma: {
title: '`trailingComma`',
description: 'Whether the last import should have a trailing comma (only applicable for multiline imports).',
},
},
],
defaultOptions: [
{
locale: DEFAULT_LOCALE,
maxItems: DEFAULT_MAX_SIZE,
indent: DEFAULT_SPACE_INDENT_AMOUNT,
trailingComma: DEFAULT_TRAILING_COMMA,
},
],
};
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: TSESLint.RuleContext<Message, unknown[]>, [{ locale, maxItems, indent, trailingComma }]: any) {
return {
['ClassDeclaration > Decorator > CallExpression[callee.name="Component"] > ObjectExpression > Property[key.name="imports"] > ArrayExpression']: (node: TSESTree.ArrayExpression) => {
const identifiers = node.elements.filter(TSESLintASTUtils.isIdentifier);
const sortedNames: string[] = identifiers
.map((identifier) => identifier.name)
.sort((a: string, b: string) => a.localeCompare(b, locale));
const isSorted: boolean = identifiers.every((identifier, index) => identifier.name === sortedNames[index]);
const requiresMultiline: boolean = maxItems < node.elements.length;
const isMultiline: boolean = /\n/.test(context.sourceCode.getText(node));
const incorrectFormat: boolean = requiresMultiline !== isMultiline;
if (isSorted && !incorrectFormat) {
return;
}
context.report({
node: node.parent,
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
fix: (fixer: TSESLint.RuleFixer) => {
if (requiresMultiline) {
const multilineImports: string = sortedNames
.map((name: string) => `${' '.repeat(2 * indent)}${name}${trailingComma ? ',' : ''}`)
.join(trailingComma ? '\n' : ',\n');
return fixer.replaceText(node, `[\n${multilineImports}\n${' '.repeat(indent)}]`);
} else {
return fixer.replaceText(node, `[${sortedNames.join(', ')}]`);
}
},
});
},
};
},
});
export const tests: NamedTests = {
plugin: info.name,
valid: [
{
name: 'should sort multiple imports on separate lines',
code: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [
AsyncPipe,
RootComponent,
],
})
export class AppComponent {}`,
},
{
name: 'should not inlines singular imports when maxItems is 0',
code: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [
RootComponent,
],
})
export class AppComponent {}`,
},
{
name: 'should inline singular imports when maxItems is 1',
options: [{ maxItems: 1 }],
code: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [RootComponent],
})
export class AppComponent {}`,
},
],
invalid: [
{
name: 'should sort multiple imports alphabetically',
code: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [
RootComponent,
AsyncPipe,
],
})
export class AppComponent {}`,
errors: [
{
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
},
],
output: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [
AsyncPipe,
RootComponent,
],
})
export class AppComponent {}`,
},
{
name: 'should not put singular imports on one line when maxItems is 0',
code: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [RootComponent],
})
export class AppComponent {}`,
errors: [
{
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
},
],
output: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [
RootComponent,
],
})
export class AppComponent {}`,
},
{
name: 'should not put singular imports on a separate line when maxItems is 1',
options: [{ maxItems: 1 }],
code: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [
RootComponent,
],
})
export class AppComponent {}`,
errors: [
{
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
},
],
output: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [RootComponent],
})
export class AppComponent {}`,
},
{
name: 'should not display multiple imports on the same line',
code: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [AsyncPipe, RootComponent],
})
export class AppComponent {}`,
errors: [
{
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
},
],
output: `
@Component({
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [
AsyncPipe,
RootComponent,
],
})
export class AppComponent {}`,
},
],
};