Make rules more type-safe

This commit is contained in:
Yury Bondarenko
2024-03-15 13:19:47 +01:00
parent b0758c23e5
commit 6e22b5376a
15 changed files with 314 additions and 158 deletions

View File

@@ -1,9 +1,17 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import {
bundle,
RuleExports,
} from '../../util/structure';
import * as themedComponentUsages from './themed-component-usages';
/* eslint-disable import/no-namespace */
import * as themedComponentSelectors from './themed-component-selectors';
import * as themedComponentUsages from './themed-component-usages';
const index = [
themedComponentUsages,

View File

@@ -5,9 +5,13 @@
*
* http://www.dspace.org/license/
*/
import { ESLintUtils } from '@typescript-eslint/utils';
import { fixture } from '../../../test/fixture';
import {
ESLintUtils,
TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
import { fixture } from '../../../test/fixture';
import { getComponentSelectorNode } from '../../util/angular';
import { stringLiteral } from '../../util/misc';
import { DSpaceESLintRuleInfo } from '../../util/structure';
@@ -16,6 +20,7 @@ import {
isThemeableComponent,
isThemedComponentWrapper,
} from '../../util/theme-support';
import { getFilename } from '../../util/typescript';
export enum Message {
BASE = 'wrongSelectorUnthemedComponent',
@@ -53,41 +58,43 @@ Unit tests are exempt from this rule, because they may redefine components using
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: any): any {
if (context.getFilename()?.endsWith('.spec.ts')) {
create(context: TSESLint.RuleContext<Message, unknown[]>) {
const filename = getFilename(context);
if (filename.endsWith('.spec.ts')) {
return {};
}
function enforceWrapperSelector(selectorNode: any) {
function enforceWrapperSelector(selectorNode: TSESTree.StringLiteral) {
if (selectorNode?.value.startsWith('ds-themed-')) {
context.report({
messageId: Message.WRAPPER,
node: selectorNode,
fix(fixer: any) {
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-')));
},
});
}
}
function enforceBaseSelector(selectorNode: any) {
function enforceBaseSelector(selectorNode: TSESTree.StringLiteral) {
if (!selectorNode?.value.startsWith('ds-base-')) {
context.report({
messageId: Message.BASE,
node: selectorNode,
fix(fixer: any) {
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-')));
},
});
}
}
function enforceThemedSelector(selectorNode: any) {
function enforceThemedSelector(selectorNode: TSESTree.StringLiteral) {
if (!selectorNode?.value.startsWith('ds-themed-')) {
context.report({
messageId: Message.THEMED,
node: selectorNode,
fix(fixer: any) {
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-')));
},
});
@@ -95,11 +102,15 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({
}
return {
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: any) {
// keep track of all @Component nodes by their selector
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) {
const selectorNode = getComponentSelectorNode(node);
if (selectorNode === undefined) {
return;
}
const selector = selectorNode?.value;
const classNode = node.parent;
const classNode = node.parent as TSESTree.ClassDeclaration;
const className = classNode.id?.name;
if (selector === undefined || className === undefined) {
@@ -108,7 +119,7 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({
if (isThemedComponentWrapper(node)) {
enforceWrapperSelector(selectorNode);
} else if (inThemedComponentOverrideFile(context)) {
} else if (inThemedComponentOverrideFile(filename)) {
enforceThemedSelector(selectorNode);
} else if (isThemeableComponent(className)) {
enforceBaseSelector(selectorNode);
@@ -124,11 +135,11 @@ export const tests = {
{
name: 'Regular non-themeable component selector',
code: `
@Component({
selector: 'ds-something',
})
class Something {
}
@Component({
selector: 'ds-something',
})
class Something {
}
`,
},
{

View File

@@ -5,9 +5,13 @@
*
* http://www.dspace.org/license/
*/
import { ESLintUtils } from '@typescript-eslint/utils';
import {
ESLintUtils,
TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
import { fixture } from '../../../test/fixture';
import { findUsages } from '../../util/misc';
import { DSpaceESLintRuleInfo } from '../../util/structure';
import {
allThemeableComponents,
@@ -17,6 +21,10 @@ import {
inThemedComponentFile,
isAllowedUnthemedUsage,
} from '../../util/theme-support';
import {
findUsages,
getFilename,
} from '../../util/typescript';
export enum Message {
WRONG_CLASS = 'mustUseThemedWrapperClass',
@@ -52,8 +60,10 @@ There are a few exceptions where the base class can still be used:
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: any, options: any): any {
function handleUnthemedUsagesInTypescript(node: any) {
create(context: TSESLint.RuleContext<Message, unknown[]>) {
const filename = getFilename(context);
function handleUnthemedUsagesInTypescript(node: TSESTree.Identifier) {
if (isAllowedUnthemedUsage(node)) {
return;
}
@@ -68,24 +78,24 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({
context.report({
messageId: Message.WRONG_CLASS,
node: node,
fix(fixer: any) {
fix(fixer) {
return fixer.replaceText(node, entry.wrapperClass);
},
});
}
function handleThemedSelectorQueriesInTests(node: any) {
function handleThemedSelectorQueriesInTests(node: TSESTree.Literal) {
context.report({
node,
messageId: Message.WRONG_SELECTOR,
fix(fixer: any){
fix(fixer){
const newSelector = fixSelectors(node.raw);
return fixer.replaceText(node, newSelector);
},
});
}
function handleUnthemedImportsInTypescript(specifierNode: any) {
function handleUnthemedImportsInTypescript(specifierNode: TSESTree.ImportSpecifier) {
const allUsages = findUsages(context, specifierNode.local);
const badUsages = allUsages.filter(usage => !isAllowedUnthemedUsage(usage));
@@ -94,7 +104,7 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({
}
const importedNode = specifierNode.imported;
const declarationNode = specifierNode.parent;
const declarationNode = specifierNode.parent as TSESTree.ImportDeclaration;
const entry = getThemeableComponentByBaseClass(importedNode.name);
if (entry === undefined) {
@@ -105,7 +115,7 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({
context.report({
messageId: Message.WRONG_IMPORT,
node: importedNode,
fix(fixer: any) {
fix(fixer) {
const ops = [];
const oldImportSource = declarationNode.source.value;
@@ -128,17 +138,17 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({
}
// ignore tests and non-routing modules
if (context.getFilename()?.endsWith('.spec.ts')) {
if (filename.endsWith('.spec.ts')) {
return {
[`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests,
};
} else if (context.getFilename()?.endsWith('.cy.ts')) {
} else if (filename.endsWith('.cy.ts')) {
return {
[`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests,
};
} else if (
context.getFilename()?.match(/(?!routing).module.ts$/)
|| context.getFilename()?.match(/themed-.+\.component\.ts$/)
filename.match(/(?!routing).module.ts$/)
|| filename.match(/themed-.+\.component\.ts$/)
|| inThemedComponentFile(context)
) {
// do nothing