mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Custom ESLint rules to enforce new ThemedComponent selector convention
The following cases are covered: - ThemedComponent wrapper selectors must not start with ds-themed- - Base component selectors must start with ds-base- - Themed component selectors must start with ds-themed- - The ThemedComponent wrapper must always be used in HTML - The ThemedComponent wrapper must be used in TypeScript _where appropriate_: - Required - Explicit usages (e.g. modal instantiation, routing modules, ...) - By.css selector queries (in order to align with the HTML rule) - Unchecked - Non-routing modules (to ensure the components can be declared) - ViewChild hooks (since they need to attach to the underlying component) All rules work with --fix to automatically migrate to the new convention This covers most of the codebase, but minor manual adjustment are needed afterwards
This commit is contained in:
132
lint/src/rules/ts/themed-component-usages.ts
Normal file
132
lint/src/rules/ts/themed-component-usages.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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 { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { findUsages } from '../../util/misc';
|
||||
import {
|
||||
allThemeableComponents,
|
||||
DISALLOWED_THEME_SELECTORS,
|
||||
fixSelectors,
|
||||
getThemeableComponentByBaseClass,
|
||||
inThemedComponentFile,
|
||||
isAllowedUnthemedUsage,
|
||||
} from '../../util/theme-support';
|
||||
|
||||
export default ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
schema: [],
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
mustUseThemedWrapper: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
mustImportThemedWrapper: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context: any, options: any): any {
|
||||
function handleUnthemedUsagesInTypescript(node: any) {
|
||||
if (isAllowedUnthemedUsage(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = getThemeableComponentByBaseClass(node.name);
|
||||
|
||||
if (entry === undefined) {
|
||||
// this should never happen
|
||||
throw new Error(`No such themeable component in registry: '${node.name}'`);
|
||||
}
|
||||
|
||||
context.report({
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
node: node,
|
||||
fix(fixer: any) {
|
||||
return fixer.replaceText(node, entry.wrapperClass);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleThemedSelectorQueriesInTests(node: any) {
|
||||
|
||||
}
|
||||
|
||||
function handleUnthemedImportsInTypescript(specifierNode: any) {
|
||||
const allUsages = findUsages(context, specifierNode.local);
|
||||
const badUsages = allUsages.filter(usage => !isAllowedUnthemedUsage(usage));
|
||||
|
||||
if (badUsages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importedNode = specifierNode.imported;
|
||||
const declarationNode = specifierNode.parent;
|
||||
|
||||
const entry = getThemeableComponentByBaseClass(importedNode.name);
|
||||
if (entry === undefined) {
|
||||
// this should never happen
|
||||
throw new Error(`No such themeable component in registry: '${importedNode.name}'`);
|
||||
}
|
||||
|
||||
context.report({
|
||||
messageId: 'mustImportThemedWrapper',
|
||||
node: importedNode,
|
||||
fix(fixer: any) {
|
||||
const ops = [];
|
||||
|
||||
const oldImportSource = declarationNode.source.value;
|
||||
const newImportLine = `import { ${entry.wrapperClass} } from '${oldImportSource.replace(entry.baseFileName, entry.wrapperFileName)}';`;
|
||||
|
||||
if (declarationNode.specifiers.length === 1) {
|
||||
if (allUsages.length === badUsages.length) {
|
||||
ops.push(fixer.replaceText(declarationNode, newImportLine));
|
||||
} else {
|
||||
ops.push(fixer.insertTextAfter(declarationNode, newImportLine));
|
||||
}
|
||||
} else {
|
||||
ops.push(fixer.replaceText(specifierNode, entry.wrapperClass));
|
||||
ops.push(fixer.insertTextAfter(declarationNode, newImportLine));
|
||||
}
|
||||
|
||||
return ops;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ignore tests and non-routing modules
|
||||
if (context.getFilename()?.endsWith('.spec.ts')) {
|
||||
return {
|
||||
[`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`](node: any) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'mustUseThemedWrapper',
|
||||
fix(fixer: any){
|
||||
const newSelector = fixSelectors(node.raw);
|
||||
return fixer.replaceText(node, newSelector);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
} else if (
|
||||
context.getFilename()?.match(/(?!routing).module.ts$/)
|
||||
|| context.getFilename()?.match(/themed-.+\.component\.ts$/)
|
||||
|| inThemedComponentFile(context)
|
||||
) {
|
||||
// do nothing
|
||||
return {};
|
||||
} else {
|
||||
return allThemeableComponents().reduce(
|
||||
(rules, entry) => {
|
||||
return {
|
||||
...rules,
|
||||
[`:not(:matches(ClassDeclaration, ImportSpecifier)) > Identifier[name = "${entry.baseClass}"]`]: handleUnthemedUsagesInTypescript,
|
||||
[`ImportSpecifier[imported.name = "${entry.baseClass}"]`]: handleUnthemedImportsInTypescript,
|
||||
};
|
||||
}, {},
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user