diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts index 0569470233..d38c3b26d6 100644 --- a/packages/eslint-plugin/src/configs/recommended.ts +++ b/packages/eslint-plugin/src/configs/recommended.ts @@ -8,6 +8,7 @@ const recommended = { "@calcom/eslint/avoid-prisma-client-import-for-enums": "error", "@calcom/eslint/no-prisma-include-true": "warn", "@calcom/eslint/no-scroll-into-view-embed": "error", + "@calcom/eslint/no-this-in-static-method": "error", }, }; diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index cb28efdeec..069095dbd7 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -8,4 +8,5 @@ export default { "no-prisma-include-true": require("./no-prisma-include-true").default, "deprecated-imports-next-router": require("./deprecated-imports-next-router").default, "no-scroll-into-view-embed": require("./no-scroll-into-view-embed").default, + "no-this-in-static-method": require("./no-this-in-static-method").default, } as ESLint.Plugin["rules"]; diff --git a/packages/eslint-plugin/src/rules/no-this-in-static-method.ts b/packages/eslint-plugin/src/rules/no-this-in-static-method.ts new file mode 100644 index 0000000000..c308d74ef7 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-this-in-static-method.ts @@ -0,0 +1,109 @@ +import type { TSESTree } from "@typescript-eslint/utils"; +import { ESLintUtils } from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator((name) => `https://developer.cal.com/eslint/rule/${name}`); + +const rule = createRule({ + create(context) { + let currentMethodIsStatic = false; + let currentClassName = ""; + + return { + MethodDefinition(node) { + if (node.static && node.key.type === "Identifier") { + currentMethodIsStatic = true; + if (node.parent?.type === "ClassBody" && node.parent.parent?.type === "ClassDeclaration") { + const classNode = node.parent.parent as TSESTree.ClassDeclaration; + if (classNode.id?.name) { + currentClassName = classNode.id.name; + } + } + } + }, + "MethodDefinition:exit"(node: TSESTree.MethodDefinition) { + if (node.static) { + currentMethodIsStatic = false; + currentClassName = ""; + } + }, + MemberExpression(node) { + if ( + currentMethodIsStatic && + node.object.type === "ThisExpression" && + node.property.type === "Identifier" + ) { + const parent = node.parent; + + const isPassedToCallback = + parent?.type === "CallExpression" && + parent.arguments.includes(node) && + parent.callee.type === "MemberExpression" && + parent.callee.property.type === "Identifier" && + ["map", "filter", "forEach", "reduce", "find", "some", "every"].includes( + parent.callee.property.name + ); + + const isVariableAssignment = parent?.type === "VariableDeclarator" && parent.init === node; + + const isObjectProperty = parent?.type === "Property" && parent.value === node; + + const isArrayElement = parent?.type === "ArrayExpression" && parent.elements.includes(node); + + const isFunctionArgument = + parent?.type === "CallExpression" && + parent.arguments.includes(node) && + !( + parent.callee.type === "MemberExpression" && + parent.callee.property.type === "Identifier" && + ["map", "filter", "forEach", "reduce", "find", "some", "every"].includes( + parent.callee.property.name + ) + ); + + const isReturnStatement = parent?.type === "ReturnStatement" && parent.argument === node; + + if ( + isPassedToCallback || + isVariableAssignment || + isObjectProperty || + isArrayElement || + isFunctionArgument || + isReturnStatement + ) { + context.report({ + node, + messageId: "no-this-in-static-method", + data: { + className: currentClassName, + methodName: node.property.name, + }, + fix(fixer) { + if (currentClassName && node.property.type === "Identifier") { + return fixer.replaceText(node, `${currentClassName}.${node.property.name}`); + } + return null; + }, + }); + } + } + }, + }; + }, + name: "no-this-in-static-method", + meta: { + type: "problem", + docs: { + description: "Disallow using 'this' to reference static methods within static methods", + recommended: "error", + }, + messages: { + "no-this-in-static-method": + "Do not use 'this.{{methodName}}' in static methods. Use '{{className}}.{{methodName}}' instead to avoid context loss when passed to callbacks.", + }, + fixable: "code", + schema: [], + }, + defaultOptions: [], +}); + +export default rule;