Skip to content

Commit 00709fb

Browse files
committed
ready for review
1 parent edecf9f commit 00709fb

File tree

3 files changed

+74
-49
lines changed

3 files changed

+74
-49
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
],
3434
"scripts": {
3535
"build": "npx tsc --build ./tsconfig.json",
36-
"test": "mocha --timeout 10000 'dist/tests/**/noRepeatedMemberAccess.test.js'",
36+
"test": "mocha --timeout 10000 'dist/tests/**/**.test.js'",
3737
"test:coverage": "c8 npm test",
3838
"lint": "npx eslint plugins/ tests/",
3939
"watch": "npx tsc --build ./tsconfig.json --watch",

plugins/rules/memberAccess.ts

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -60,34 +60,39 @@ const noRepeatedMemberAccess = createRule({
6060
// Track which chains have already been reported to avoid duplicate reports
6161
const reportedChains = new Set<string>();
6262

63-
type ScopeData = Map<
63+
// We have got two map types, chainMap and scopeDataMap
64+
// it works like: scopeDataMap -> chainMap -> chainInfo
65+
66+
// Stores info to decide if a extraction is necessary
67+
type ChainMap = Map<
6468
string,
6569
{
6670
count: number; // Number of times this chain is accessed
67-
node: TSESTree.MemberExpression; // AST nodes where this chain appears
6871
modified: boolean; // Whether this chain is modified (written to)
6972
}
7073
>;
71-
// Stores data for each scope using WeakMap to avoid memory leaks
72-
const scopeDataMap = new WeakMap<Scope.Scope, ScopeData>();
74+
// Stores mapping of scope to ChainMap
75+
const scopeDataMap = new WeakMap<Scope.Scope, ChainMap>();
7376

74-
function getScopeData(scope: Scope.Scope): ScopeData {
77+
function getChainMap(scope: Scope.Scope): ChainMap {
7578
if (!scopeDataMap.has(scope)) {
76-
// Create new scope data if not already present
77-
const newScopeData = new Map<
79+
// Create new info map if not already present
80+
const newChainMap = new Map<
7881
string,
7982
{
8083
count: number;
81-
node: TSESTree.MemberExpression;
8284
modified: boolean;
8385
}
8486
>();
85-
scopeDataMap.set(scope, newScopeData);
87+
scopeDataMap.set(scope, newChainMap);
8688
}
8789
return scopeDataMap.get(scope)!;
8890
}
8991

90-
function analyzeChain(node: TSESTree.MemberExpression) {
92+
// This function generates ["a", "a.b", "a.b.c"] from a.b.c
93+
// We will further add [count, modified] info to them in ChainMap, and use them as an indication for extraction
94+
// eslint-disable-next-line unicorn/consistent-function-scoping
95+
function analyzeChain(node: TSESTree.MemberExpression): string[] {
9196
const properties: string[] = []; // AST is iterated in reverse order
9297
let current: TSESTree.Node = node; // Current node in traversal
9398

@@ -138,11 +143,10 @@ const noRepeatedMemberAccess = createRule({
138143

139144
function setModifiedFlag(chain: string, node: TSESTree.Node) {
140145
const scope = sourceCode.getScope(node);
141-
const scopeData = getScopeData(scope);
146+
const scopeData = getChainMap(scope);
142147

143148
for (const [existingChain, record] of scopeData) {
144-
// Check if the existing chain starts with the modified chain followed by a dot or bracket
145-
// This handles cases where modifying "a.b" should invalidate "a.b.c", "a.b.d", etc.
149+
// Check if the existing chain starts with the modified chain followed by a dot or bracket, and if so, marks them as modified
146150
if (
147151
existingChain === chain ||
148152
existingChain.startsWith(chain + ".") ||
@@ -154,7 +158,6 @@ const noRepeatedMemberAccess = createRule({
154158
if (!scopeData.has(chain)) {
155159
scopeData.set(chain, {
156160
count: 0,
157-
node: node as TSESTree.MemberExpression, // to do: check this conversion!!
158161
modified: true,
159162
});
160163
}
@@ -166,20 +169,43 @@ const noRepeatedMemberAccess = createRule({
166169
// not the sub-expressions a.b or a
167170
if (node.parent?.type === AST_NODE_TYPES.MemberExpression) return;
168171

169-
const scope = sourceCode.getScope(node);
170-
const scopeData = getScopeData(scope);
171-
172172
const chainInfo = analyzeChain(node);
173173
if (!chainInfo) return;
174174

175-
const longestValidChain = chainInfo[-1];
176-
const record = scopeData.get(longestValidChain)!;
177-
if (
178-
record.count >= minOccurrences &&
179-
!reportedChains.has(longestValidChain)
180-
) {
175+
const scope = sourceCode.getScope(node);
176+
const infoMap = getChainMap(scope);
177+
178+
// keeps record of the longest valid chain, and only report it instead of shorter ones (to avoid repeated reports)
179+
let longestValidChain = "";
180+
181+
// Update chain statistics for each part of the hierarchy
182+
for (const chain of chainInfo) {
183+
// Skip single-level chains
184+
if (!chain.includes(".")) continue;
185+
186+
const record = infoMap.get(chain) || {
187+
count: 0,
188+
modified: false,
189+
};
190+
if (record.modified) break;
191+
192+
record.count++;
193+
infoMap.set(chain, record);
194+
195+
// record longest extractable chain
196+
if (
197+
record.count >= minOccurrences &&
198+
chain.length > longestValidChain.length
199+
) {
200+
longestValidChain = chain;
201+
}
202+
}
203+
204+
// report the longest chain
205+
if (longestValidChain && !reportedChains.has(longestValidChain)) {
206+
const record = infoMap.get(longestValidChain)!;
181207
context.report({
182-
node: record.node,
208+
node: node,
183209
messageId: "repeatedAccess",
184210
data: { chain: longestValidChain, count: record.count },
185211
});
@@ -194,10 +220,8 @@ const noRepeatedMemberAccess = createRule({
194220
AssignmentExpression: (node) => {
195221
if (node.left.type === AST_NODE_TYPES.MemberExpression) {
196222
const chainInfo = analyzeChain(node.left);
197-
if (chainInfo) {
198-
for (const chain of chainInfo) {
199-
setModifiedFlag(chain, node);
200-
}
223+
for (const chain of chainInfo) {
224+
setModifiedFlag(chain, node);
201225
}
202226
}
203227
},
@@ -207,10 +231,8 @@ const noRepeatedMemberAccess = createRule({
207231
UpdateExpression: (node) => {
208232
if (node.argument.type === AST_NODE_TYPES.MemberExpression) {
209233
const chainInfo = analyzeChain(node.argument);
210-
if (chainInfo) {
211-
for (const chain of chainInfo) {
212-
setModifiedFlag(chain, node);
213-
}
234+
for (const chain of chainInfo) {
235+
setModifiedFlag(chain, node);
214236
}
215237
}
216238
},
@@ -220,10 +242,8 @@ const noRepeatedMemberAccess = createRule({
220242
CallExpression: (node) => {
221243
if (node.callee.type === AST_NODE_TYPES.MemberExpression) {
222244
const chainInfo = analyzeChain(node.callee);
223-
if (chainInfo) {
224-
for (const chain of chainInfo) {
225-
setModifiedFlag(chain, node);
226-
}
245+
for (const chain of chainInfo) {
246+
setModifiedFlag(chain, node);
227247
}
228248
}
229249
},

tests/rules/noRepeatedMemberAccess.test.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,14 @@ describe("Rule: no-spread", () => {
3636
`,
3737
`
3838
switch (reason) {
39-
case Test.STARTUP: {
40-
return "STARTUP"
39+
case Test.x: {
40+
return "x"
4141
}
42-
case Test.DEBOUNCE: {
43-
return "DEBOUNCE"
42+
case Test.y: {
43+
return "y"
4444
}
45-
case Test.INSTANT: {
46-
return "INSTANT"
47-
}
48-
case Test.SHUTDOWN: {
49-
return "SHUTDOWN"
50-
}
51-
default: {
52-
return "UNKNOWN"
45+
case Test.z: {
46+
return "z"
5347
}
5448
}
5549
`,
@@ -74,6 +68,12 @@ describe("Rule: no-spread", () => {
7468
data[0][1].count++;
7569
send(data[0][1].id);
7670
`,
71+
`
72+
const a = dataset[0][1].x + dataset[0][1].y;
73+
dataset[0][1].update();
74+
const b = dataset[0][1].z * 2;
75+
notify(dataset[0][1].timestamp);
76+
`,
7777
// WARN: DONT extract when function with possible side effect is called upon
7878
`
7979
const a = data.x + data.y;
@@ -82,6 +82,11 @@ describe("Rule: no-spread", () => {
8282
notify(data.x);
8383
`,
8484
`
85+
const first = data.items[0].config['security'].rules[2].level;
86+
data.items[0].config['security'].rules[2].enabled = true;
87+
validate(data.items[0].config['security'].rules[2].level);
88+
`,
89+
`
8590
const v1 = obj[123].value;
8691
const v2 = obj[123].value;
8792
const v3 = obj[123].value;

0 commit comments

Comments
 (0)