Files
twenty/packages/twenty-eslint-plugin/src/rules/effect-components.ts
Charles Bochet 44baaee28e Update scripts and documentation to use nx and new monorepo architecture (#2912)
* Update scripts and documentation to use nx and new monorepo architecture

* Start fixing docker

* Migrate eslint plugin and postgres setup

* Fix docker

* Fix patches

* Fix

* fix: wip try to fix the patches

* Apply patches

---------

Co-authored-by: Jérémy Magrin <jeremy.magrin@gmail.com>
2023-12-11 10:54:57 +01:00

167 lines
5.6 KiB
TypeScript

import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`);
const checkIsPascalCase = (input: string): boolean => {
const pascalCaseRegex = /^[A-Z][a-zA-Z0-9_]*/g;
return pascalCaseRegex.test(input);
};
type ComponentType =
| TSESTree.FunctionDeclaration
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionExpression;
const effectComponentsRule = createRule({
create: (context) => {
const checkThatNodeIsEffectComponent = (node: ComponentType) => {
let componentName = "";
let identifierNode = node.id;
const isIdentifier = (
node: TSESTree.Node | null,
): node is TSESTree.Identifier =>
node?.type === TSESTree.AST_NODE_TYPES.Identifier;
const isVariableDeclarator = (
node: TSESTree.Node,
): node is TSESTree.VariableDeclarator =>
node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator;
const isArrowFunction = (
node: TSESTree.Node,
): node is TSESTree.ArrowFunctionExpression =>
node.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression;
const isFunctionDeclaration = (
node: TSESTree.Node,
): node is TSESTree.FunctionDeclaration =>
node.type === TSESTree.AST_NODE_TYPES.FunctionDeclaration;
const isFunctionExpression = (
node: TSESTree.Node,
): node is TSESTree.FunctionExpression =>
node.type === TSESTree.AST_NODE_TYPES.FunctionExpression;
if (
isArrowFunction(node) &&
isVariableDeclarator(node.parent) &&
isIdentifier(node.parent.id)
) {
componentName = node.parent.id.name;
identifierNode = node.parent.id;
} else if (isFunctionDeclaration(node) && isIdentifier(node.id)) {
componentName = node.id.name;
identifierNode = node.id;
} else if (
isFunctionExpression(node) &&
isVariableDeclarator(node.parent) &&
isIdentifier(node.parent.id)
) {
componentName = node.parent.id.name;
identifierNode = node.parent.id;
}
if (!checkIsPascalCase(componentName)) {
return;
}
const isReturningEmptyFragmentOrNull =
// Direct return of JSX fragment, e.g., () => <></>
(node.body.type === "JSXFragment" && node.body.children.length === 0) ||
// Direct return of null, e.g., () => null
(node.body.type === "Literal" && node.body.value === null) ||
// Return JSX fragment or null from block
(node.body.type === "BlockStatement" &&
node.body.body.some(
(statement) =>
statement.type === "ReturnStatement" &&
// Empty JSX fragment return, e.g., return <></>;
((statement.argument?.type === "JSXFragment" &&
statement.argument.children.length === 0) ||
// Empty React.Fragment return, e.g., return <React.Fragment></React.Fragment>;
(statement.argument?.type === "JSXElement" &&
statement.argument.openingElement.name.type ===
"JSXIdentifier" &&
statement.argument.openingElement.name.name ===
"React.Fragment" &&
statement.argument.children.length === 0) ||
// Literal null return, e.g., return null;
(statement.argument?.type === "Literal" &&
statement.argument.value === null)),
));
const hasEffectSuffix = componentName.endsWith("Effect");
const hasEffectSuffixButIsNotEffectComponent =
hasEffectSuffix && !isReturningEmptyFragmentOrNull;
const isEffectComponentButDoesNotHaveEffectSuffix =
!hasEffectSuffix && isReturningEmptyFragmentOrNull;
if (isEffectComponentButDoesNotHaveEffectSuffix) {
context.report({
node,
messageId: "effectSuffix",
data: {
componentName: componentName,
},
fix: (fixer) => {
if (isArrowFunction(node))
if (identifierNode) {
return fixer.replaceText(
identifierNode,
componentName + "Effect",
);
}
return null;
},
});
} else if (hasEffectSuffixButIsNotEffectComponent) {
context.report({
node,
messageId: "noEffectSuffix",
data: {
componentName: componentName,
},
fix: (fixer) => {
if (identifierNode) {
return fixer.replaceText(
identifierNode,
componentName.replace("Effect", ""),
);
}
return null;
},
});
}
};
return {
ArrowFunctionExpression: checkThatNodeIsEffectComponent,
FunctionDeclaration: checkThatNodeIsEffectComponent,
FunctionExpression: checkThatNodeIsEffectComponent,
};
},
name: "effect-components",
meta: {
docs: {
description:
"Effect components should end with the Effect suffix. This rule checks only components that are in PascalCase and that return a JSX fragment or null. Any renderProps or camelCase components are ignored.",
},
messages: {
effectSuffix:
"Effect component {{ componentName }} should end with the Effect suffix.",
noEffectSuffix:
"Component {{ componentName }} shouldn't end with the Effect suffix because it doesn't return a JSX fragment or null.",
},
type: "suggestion",
schema: [],
fixable: "code",
},
defaultOptions: [],
});
module.exports = effectComponentsRule;
export default effectComponentsRule;