chore(eslint): add no-this-in-static-method rule to prevent context loss (#22410)

Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Eunjae Lee
2025-07-18 13:05:53 -07:00
committed by GitHub
co-authored by eunjae@cal.com <hey@eunjae.dev> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Keith Williams Omar López
parent 04ce054cf7
commit 30a92a4d66
3 changed files with 111 additions and 0 deletions
@@ -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",
},
};
@@ -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"];
@@ -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;