Skip to content

Commit 30a92a4

Browse files
eunjae-leedevin-ai-integration[bot]keithwillcodezomars
authored
chore(eslint): add no-this-in-static-method rule to prevent context loss (calcom#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>
1 parent 04ce054 commit 30a92a4

3 files changed

Lines changed: 111 additions & 0 deletions

File tree

packages/eslint-plugin/src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const recommended = {
88
"@calcom/eslint/avoid-prisma-client-import-for-enums": "error",
99
"@calcom/eslint/no-prisma-include-true": "warn",
1010
"@calcom/eslint/no-scroll-into-view-embed": "error",
11+
"@calcom/eslint/no-this-in-static-method": "error",
1112
},
1213
};
1314

packages/eslint-plugin/src/rules/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export default {
88
"no-prisma-include-true": require("./no-prisma-include-true").default,
99
"deprecated-imports-next-router": require("./deprecated-imports-next-router").default,
1010
"no-scroll-into-view-embed": require("./no-scroll-into-view-embed").default,
11+
"no-this-in-static-method": require("./no-this-in-static-method").default,
1112
} as ESLint.Plugin["rules"];
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { TSESTree } from "@typescript-eslint/utils";
2+
import { ESLintUtils } from "@typescript-eslint/utils";
3+
4+
const createRule = ESLintUtils.RuleCreator((name) => `https://developer.cal.com/eslint/rule/${name}`);
5+
6+
const rule = createRule({
7+
create(context) {
8+
let currentMethodIsStatic = false;
9+
let currentClassName = "";
10+
11+
return {
12+
MethodDefinition(node) {
13+
if (node.static && node.key.type === "Identifier") {
14+
currentMethodIsStatic = true;
15+
if (node.parent?.type === "ClassBody" && node.parent.parent?.type === "ClassDeclaration") {
16+
const classNode = node.parent.parent as TSESTree.ClassDeclaration;
17+
if (classNode.id?.name) {
18+
currentClassName = classNode.id.name;
19+
}
20+
}
21+
}
22+
},
23+
"MethodDefinition:exit"(node: TSESTree.MethodDefinition) {
24+
if (node.static) {
25+
currentMethodIsStatic = false;
26+
currentClassName = "";
27+
}
28+
},
29+
MemberExpression(node) {
30+
if (
31+
currentMethodIsStatic &&
32+
node.object.type === "ThisExpression" &&
33+
node.property.type === "Identifier"
34+
) {
35+
const parent = node.parent;
36+
37+
const isPassedToCallback =
38+
parent?.type === "CallExpression" &&
39+
parent.arguments.includes(node) &&
40+
parent.callee.type === "MemberExpression" &&
41+
parent.callee.property.type === "Identifier" &&
42+
["map", "filter", "forEach", "reduce", "find", "some", "every"].includes(
43+
parent.callee.property.name
44+
);
45+
46+
const isVariableAssignment = parent?.type === "VariableDeclarator" && parent.init === node;
47+
48+
const isObjectProperty = parent?.type === "Property" && parent.value === node;
49+
50+
const isArrayElement = parent?.type === "ArrayExpression" && parent.elements.includes(node);
51+
52+
const isFunctionArgument =
53+
parent?.type === "CallExpression" &&
54+
parent.arguments.includes(node) &&
55+
!(
56+
parent.callee.type === "MemberExpression" &&
57+
parent.callee.property.type === "Identifier" &&
58+
["map", "filter", "forEach", "reduce", "find", "some", "every"].includes(
59+
parent.callee.property.name
60+
)
61+
);
62+
63+
const isReturnStatement = parent?.type === "ReturnStatement" && parent.argument === node;
64+
65+
if (
66+
isPassedToCallback ||
67+
isVariableAssignment ||
68+
isObjectProperty ||
69+
isArrayElement ||
70+
isFunctionArgument ||
71+
isReturnStatement
72+
) {
73+
context.report({
74+
node,
75+
messageId: "no-this-in-static-method",
76+
data: {
77+
className: currentClassName,
78+
methodName: node.property.name,
79+
},
80+
fix(fixer) {
81+
if (currentClassName && node.property.type === "Identifier") {
82+
return fixer.replaceText(node, `${currentClassName}.${node.property.name}`);
83+
}
84+
return null;
85+
},
86+
});
87+
}
88+
}
89+
},
90+
};
91+
},
92+
name: "no-this-in-static-method",
93+
meta: {
94+
type: "problem",
95+
docs: {
96+
description: "Disallow using 'this' to reference static methods within static methods",
97+
recommended: "error",
98+
},
99+
messages: {
100+
"no-this-in-static-method":
101+
"Do not use 'this.{{methodName}}' in static methods. Use '{{className}}.{{methodName}}' instead to avoid context loss when passed to callbacks.",
102+
},
103+
fixable: "code",
104+
schema: [],
105+
},
106+
defaultOptions: [],
107+
});
108+
109+
export default rule;

0 commit comments

Comments
 (0)