Files
dspace-angular/lint/src/rules/ts/alias-imports.ts

305 lines
8.4 KiB
TypeScript

import {
AST_NODE_TYPES,
ESLintUtils,
TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
import { Scope } from '@typescript-eslint/utils/ts-eslint';
import {
DSpaceESLintRuleInfo,
NamedTests,
OptionDoc,
} from '../../util/structure';
export enum Message {
MISSING_ALIAS = 'missingAlias',
WRONG_ALIAS = 'wrongAlias',
MULTIPLE_ALIASES = 'multipleAliases',
UNNECESSARY_ALIAS = 'unnecessaryAlias',
}
interface AliasImportOptions {
aliases: AliasImportOption[];
}
interface AliasImportOption {
package: string;
imported: string;
local: string;
}
interface AliasImportDocOptions {
aliases: OptionDoc;
}
export const info: DSpaceESLintRuleInfo<[AliasImportOptions], [AliasImportDocOptions]> = {
name: 'alias-imports',
meta: {
docs: {
description: 'Unclear imports should be aliased for clarity',
},
messages: {
[Message.MISSING_ALIAS]: 'This import must be aliased',
[Message.WRONG_ALIAS]: 'This import uses the wrong alias (should be {{ local }})',
[Message.MULTIPLE_ALIASES]: 'This import was used twice with a different alias (should be {{ local }})',
[Message.UNNECESSARY_ALIAS]: 'This import should not use an alias',
},
fixable: 'code',
type: 'problem',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
package: { type: 'string' },
imported: { type: 'string' },
local: { type: 'string' },
},
},
},
},
optionDocs: [
{
aliases: {
title: '`aliases`',
description: `A list of all the imports that you want to alias for clarity. Every alias should be declared in the following format:
\`\`\`json
{
"package": "rxjs",
"imported": "of",
"local": "observableOf"
}
\`\`\``,
},
},
],
defaultOptions: [
{
aliases: [
{
package: 'rxjs',
imported: 'of',
local: 'observableOf',
},
],
},
],
};
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: TSESLint.RuleContext<Message, unknown[]>, options: any) {
return (options[0] as AliasImportOptions).aliases.reduce((selectors: any, option: AliasImportOption) => {
selectors[`ImportDeclaration[source.value = "${option.package}"] > ImportSpecifier[imported.name = "${option.imported}"][local.name != "${option.local}"]`] = (node: TSESTree.ImportSpecifier) => handleUnaliasedImport(context, option, node);
return selectors;
}, {});
},
});
export const tests: NamedTests = {
plugin: info.name,
valid: [
{
name: 'correctly aliased imports',
code: `
import { of as observableOf } from 'rxjs';
`,
options: [
{
aliases: [
{
package: 'rxjs',
imported: 'of',
local: 'observableOf',
},
],
},
],
},
{
name: 'enforce unaliased import',
code: `
import { combineLatest } from 'rxjs';
`,
options: [
{
aliases: [
{
package: 'rxjs',
imported: 'combineLatest',
local: 'combineLatest',
},
],
},
],
},
],
invalid: [
{
name: 'imports without alias',
code: `
import { of } from 'rxjs';
`,
errors: [
{
messageId: 'missingAlias',
},
],
output: `
import { of as observableOf } from 'rxjs';
`,
},
{
name: 'imports under the wrong alias',
code: `
import { of as ofSomething } from 'rxjs';
`,
errors: [
{
messageId: 'wrongAlias',
},
],
output: `
import { of as observableOf } from 'rxjs';
`,
},
{
name: 'disallow aliasing import',
code: `
import { combineLatest as observableCombineLatest } from 'rxjs';
`,
errors: [
{
messageId: 'unnecessaryAlias',
},
],
output: `
import { combineLatest } from 'rxjs';
`,
options: [
{
aliases: [
{
package: 'rxjs',
imported: 'combineLatest',
local: 'combineLatest',
},
],
},
],
},
],
};
/**
* Replaces the incorrectly aliased imports with the ones defined in the defaultOptions
*
* @param context The current {@link TSESLint.RuleContext}
* @param option The current {@link AliasImportOption} that needs to be handled
* @param node The incorrect import node that should be fixed
*/
function handleUnaliasedImport(context: TSESLint.RuleContext<Message, unknown[]>, option: AliasImportOption, node: TSESTree.ImportSpecifier): void {
const hasCorrectAliasedImport: boolean = (node.parent as TSESTree.ImportDeclaration).specifiers.find((specifier: TSESTree.ImportClause) => specifier.local.name === option.local && specifier.type === AST_NODE_TYPES.ImportSpecifier && (specifier as TSESTree.ImportSpecifier).imported.name === option.imported) !== undefined;
if (option.imported === option.local) {
if (hasCorrectAliasedImport) {
context.report({
messageId: Message.MULTIPLE_ALIASES,
data: { local: option.local },
node: node,
fix(fixer: TSESLint.RuleFixer) {
const fixes: TSESLint.RuleFix[] = [];
const commaAfter = context.sourceCode.getTokenAfter(node, {
filter: (token: TSESTree.Token) => token.value === ',',
});
if (commaAfter) {
fixes.push(fixer.removeRange([node.range[0], commaAfter.range[1]]));
} else {
fixes.push(fixer.remove(node));
}
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
return fixes;
},
});
} else {
context.report({
messageId: Message.UNNECESSARY_ALIAS,
data: { local: option.local },
node: node,
fix(fixer: TSESLint.RuleFixer) {
const fixes: TSESLint.RuleFix[] = [];
fixes.push(fixer.replaceText(node, option.imported));
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
return fixes;
},
});
}
} else {
if (hasCorrectAliasedImport) {
context.report({
messageId: Message.MULTIPLE_ALIASES,
data: { local: option.local },
node: node,
fix(fixer: TSESLint.RuleFixer) {
const fixes: TSESLint.RuleFix[] = [];
const commaAfter = context.sourceCode.getTokenAfter(node, {
filter: (token: TSESTree.Token) => token.value === ',',
});
if (commaAfter) {
fixes.push(fixer.removeRange([node.range[0], commaAfter.range[1]]));
} else {
fixes.push(fixer.remove(node));
}
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
return fixes;
},
});
} else if (node.local.name === node.imported.name) {
context.report({
messageId: Message.MISSING_ALIAS,
node: node,
fix(fixer: TSESLint.RuleFixer) {
const fixes: TSESLint.RuleFix[] = [];
fixes.push(fixer.replaceText(node.local, `${option.imported} as ${option.local}`));
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
return fixes;
},
});
} else {
context.report({
messageId: Message.WRONG_ALIAS,
data: { local: option.local },
node: node,
fix(fixer: TSESLint.RuleFixer) {
const fixes: TSESLint.RuleFix[] = [];
fixes.push(fixer.replaceText(node.local, option.local));
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
return fixes;
},
});
}
}
}
/**
* Generates the {@link TSESLint.RuleFix}s for all the usages of the incorrect import.
*
* @param context The current {@link TSESLint.RuleContext}
* @param fixer The instance {@link TSESLint.RuleFixer}
* @param node The node which needs to be replaced
* @param newAlias The new import name
*/
function retrieveUsageReplacementFixes(context: TSESLint.RuleContext<Message, unknown[]>, fixer: TSESLint.RuleFixer, node: TSESTree.ImportSpecifier, newAlias: string): TSESLint.RuleFix[] {
return context.sourceCode.getDeclaredVariables(node)[0].references.map((reference: Scope.Reference) => fixer.replaceText(reference.identifier, newAlias));
}