Skip to content

Commit 2a05148

Browse files
committed
initial work on no-repeated-member-access rule
1 parent 32cc730 commit 2a05148

File tree

6 files changed

+737
-0
lines changed

6 files changed

+737
-0
lines changed

plugins/perfPlugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
* in AssemblyScript code.
66
*/
77
import arrayInitStyle from "./rules/arrayInitStyle.js";
8+
import noRepeatedMemberAccess from "./rules/memberAccess.js";
89

910
export default {
1011
rules: {
1112
"array-init-style": arrayInitStyle,
13+
"no-repeated-member-access": noRepeatedMemberAccess,
1214
},
1315
};

plugins/rules/incorrectReference.ts

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import {
2+
AST_NODE_TYPES,
3+
ESLintUtils,
4+
TSESTree,
5+
} from "@typescript-eslint/utils";
6+
import { RuleListener, RuleModule } from "@typescript-eslint/utils/ts-eslint";
7+
import createRule from "../utils/createRule.js";
8+
9+
// Define the type for the rule options
10+
type NoRepeatedMemberAccessOptions = [
11+
{
12+
minOccurrences?: number;
13+
}?
14+
];
15+
16+
const noRepeatedMemberAccess: ESLintUtils.RuleModule<
17+
"repeatedAccess", // Message ID type
18+
NoRepeatedMemberAccessOptions, // Options type
19+
unknown, // This parameter is often unused or unknown
20+
ESLintUtils.RuleListener // Listener type
21+
> = createRule({
22+
name: "no-repeated-member-access",
23+
defaultOptions: [{ minOccurrences: 2 }], // Provide a default object matching the options structure
24+
meta: {
25+
type: "suggestion",
26+
docs: {
27+
description:
28+
"Avoid getting member variable multiple-times in the same context",
29+
},
30+
fixable: "code",
31+
messages: {
32+
repeatedAccess:
33+
"Try refactor member access to a variable (e.g. 'const temp = {{ path }};') to avoid possible performance loss",
34+
},
35+
schema: [
36+
{
37+
type: "object",
38+
properties: {
39+
minOccurrences: { type: "number", minimum: 2 },
40+
},
41+
},
42+
],
43+
},
44+
create(context) {
45+
function getObjectChain(node: TSESTree.Node) {
46+
// node is the outermost MemberExpression, e.g. ctx.data.v1
47+
let current = node;
48+
const path: string[] = [];
49+
50+
// Helper function to skip TSNonNullExpression nodes
51+
function skipTSNonNullExpression(
52+
node: TSESTree.Node
53+
): TSESTree.Expression {
54+
if (node.type === "TSNonNullExpression") {
55+
return skipTSNonNullExpression(node.expression);
56+
}
57+
return node as TSESTree.Expression;
58+
}
59+
60+
// First check if this is part of a switch-case statement
61+
let parent = node;
62+
while (parent && parent.parent) {
63+
parent = parent.parent;
64+
if (
65+
parent.type === AST_NODE_TYPES.SwitchCase &&
66+
parent.test &&
67+
(node === parent.test || isDescendant(node, parent.test))
68+
) {
69+
return null; // Skip members used in switch-case statements
70+
}
71+
}
72+
73+
// Helper function to check if a node is a descendant of another node
74+
function isDescendant(
75+
node: TSESTree.Node,
76+
possibleAncestor: TSESTree.Node
77+
): boolean {
78+
let current = node.parent;
79+
while (current) {
80+
if (current === possibleAncestor) return true;
81+
current = current.parent;
82+
}
83+
return false;
84+
}
85+
86+
// Traverse up to the second last member (object chain)
87+
while (
88+
(current && current.type === "MemberExpression") ||
89+
current.type === "TSNonNullExpression"
90+
) {
91+
// Skip non-null assertions
92+
if (current.type === "TSNonNullExpression") {
93+
current = current.expression;
94+
continue;
95+
}
96+
97+
// Only handle static property or static index
98+
if (current.computed) {
99+
// true means access with "[]"
100+
// false means access with "."
101+
if (current.property.type === "Literal") {
102+
// e.g. obj[1], obj["name"]
103+
path.unshift(`[${current.property.value}]`);
104+
} else {
105+
// Ignore dynamic property access
106+
// e.g. obj[var], obj[func()]
107+
return null;
108+
}
109+
} else {
110+
// e.g. obj.prop
111+
path.unshift(`.${current.property.name}`);
112+
}
113+
114+
// Check if we've reached the base object
115+
let objExpr = current.object;
116+
if (objExpr?.type === "TSNonNullExpression") {
117+
objExpr = skipTSNonNullExpression(objExpr);
118+
}
119+
120+
// If object is not MemberExpression, we've reached the base object
121+
if (!objExpr || objExpr.type !== "MemberExpression") {
122+
// Handle "this" expressions
123+
if (objExpr && objExpr.type === "ThisExpression") {
124+
path.unshift("this");
125+
126+
// Skip reporting if the chain is just 'this.property'
127+
if (path.length <= 2) {
128+
return null;
129+
}
130+
131+
path.pop(); // Remove the last property
132+
return path.join("").replace(/^\./, "");
133+
}
134+
135+
// If object is Identifier, add it to the path
136+
if (objExpr && objExpr.type === "Identifier") {
137+
const baseName = objExpr.name;
138+
139+
// Skip if the base looks like an enum/constant (starts with capital letter)
140+
if (
141+
baseName.length > 0 &&
142+
baseName[0] === baseName[0].toUpperCase()
143+
) {
144+
return null; // Likely an enum or static class
145+
}
146+
147+
path.unshift(baseName);
148+
// Remove the last property (keep only the object chain)
149+
path.pop();
150+
return path.join("").replace(/^\./, "");
151+
}
152+
return null;
153+
}
154+
current = objExpr;
155+
}
156+
return null;
157+
}
158+
159+
// Store nodes for each object chain in each scope for auto-fixing
160+
const chainNodesMap = new Map<string, TSESTree.MemberExpression[]>();
161+
162+
const occurrences = new Map();
163+
const minOccurrences = context.options[0]?.minOccurrences || 2;
164+
165+
return {
166+
MemberExpression(node) {
167+
// Only check the outermost member expression
168+
if (node.parent && node.parent.type === "MemberExpression") return;
169+
170+
const objectChain = getObjectChain(node);
171+
if (!objectChain) return;
172+
173+
const baseObjectName = objectChain.split(/[.[]/)[0];
174+
// no need to continue if what we extract is the same as the base object
175+
if (objectChain === baseObjectName) return;
176+
177+
// Use scope range as part of the key
178+
const scope = context.sourceCode.getScope(node);
179+
if (!scope || !scope.block || !scope.block.range) return;
180+
181+
const key = `${scope.block.range.join("-")}-${objectChain}`;
182+
183+
// Store node for auto-fixing
184+
if (!chainNodesMap.has(key)) {
185+
chainNodesMap.set(key, []);
186+
}
187+
chainNodesMap.get(key)?.push(node as TSESTree.MemberExpression);
188+
189+
const count = (occurrences.get(key) || 0) + 1;
190+
occurrences.set(key, count);
191+
192+
if (count >= minOccurrences) {
193+
context.report({
194+
node,
195+
messageId: "repeatedAccess",
196+
data: {
197+
path: objectChain,
198+
count: count,
199+
},
200+
*fix(fixer) {
201+
const nodes = chainNodesMap.get(key);
202+
if (!nodes || nodes.length < minOccurrences) return;
203+
204+
// Create a safe variable name based on the object chain
205+
const safeVarName = `_${objectChain.replace(
206+
/[^a-zA-Z0-9_]/g,
207+
"_"
208+
)}`;
209+
210+
// Find the first statement containing the first instance
211+
let statement: TSESTree.Node = nodes[0];
212+
while (
213+
statement.parent &&
214+
![
215+
"Program",
216+
"BlockStatement",
217+
"StaticBlock",
218+
"SwitchCase",
219+
].includes(statement.parent.type)
220+
) {
221+
statement = statement.parent;
222+
}
223+
224+
// Check if the variable already exists in this scope
225+
const scope = context.sourceCode.getScope(nodes[0]);
226+
const variableExists = scope.variables.some(
227+
(v) => v.name === safeVarName
228+
);
229+
230+
// Only insert declaration if variable doesn't exist
231+
if (!variableExists) {
232+
yield fixer.insertTextBefore(
233+
statement,
234+
`const ${safeVarName} = ${objectChain};\n`
235+
);
236+
}
237+
238+
// Replace ALL occurrences, not just the current node
239+
for (const memberNode of nodes) {
240+
const objText = context.sourceCode.getText(memberNode.object);
241+
if (objText === objectChain) {
242+
yield fixer.replaceText(memberNode.object, safeVarName);
243+
}
244+
}
245+
},
246+
});
247+
}
248+
},
249+
};
250+
},
251+
});

plugins/rules/memberAccess.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
AST_NODE_TYPES,
3+
ESLintUtils,
4+
TSESTree,
5+
} from "@typescript-eslint/utils";
6+
import { RuleListener, RuleModule } from "@typescript-eslint/utils/ts-eslint";
7+
import createRule from "../utils/createRule.js";
8+
9+
const noRepeatedMemberAccess: ESLintUtils.RuleModule<
10+
"repeatedAccess", // Message ID type
11+
unknown[],
12+
unknown,
13+
ESLintUtils.RuleListener // Listener type
14+
> = createRule({
15+
name: "no-repeated-member-access",
16+
defaultOptions: [{ minOccurrences: 2 }], // Provide a default object matching the options structure
17+
meta: {
18+
type: "suggestion",
19+
docs: {
20+
description:
21+
"Avoid getting member variable multiple-times in the same context",
22+
},
23+
fixable: "code",
24+
messages: {
25+
repeatedAccess:
26+
"Try refactor member access to a variable (e.g. 'const temp = {{ path }};') to avoid possible performance loss",
27+
},
28+
schema: [
29+
{
30+
type: "object",
31+
properties: {
32+
minOccurrences: { type: "number", minimum: 2 },
33+
},
34+
},
35+
],
36+
},
37+
create(context) {
38+
return {};
39+
},
40+
});
41+
42+
export default noRepeatedMemberAccess;

0 commit comments

Comments
 (0)