Skip to content

Commit 324b445

Browse files
authored
feat: add iteratePropertyReferences to ReferenceTracker class (#248)
1 parent cefc12c commit 324b445

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed

docs/api/scope-utils.md

+79
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,82 @@ module.exports = {
315315
},
316316
}
317317
```
318+
319+
## tracker.iteratePropertyReferences
320+
321+
```js
322+
const it = tracker.iteratePropertyReferences(node, traceMap)
323+
```
324+
325+
Iterate the property references of the given node that the given `traceMap` determined.
326+
This method starts to search from the given expression node.
327+
328+
### Parameters
329+
330+
| Name | Type | Description |
331+
| :------- | :----- | :-------------------------------------------------- |
332+
| node | object | The expression node. |
333+
| traceMap | object | The object which determines properties it iterates. |
334+
335+
### Return value
336+
337+
The Iterator which iterates the reference of properties.
338+
Every reference is the object that has the following properties.
339+
340+
| Name | Type | Description |
341+
| :---- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
342+
| node | Node | The node of the reference. |
343+
| path | string[] | The path of the reference. For example, if it's the access of `x.length` then `["length"]`. |
344+
| type | symbol | The reference type. If this is `ReferenceTracker.READ` then it read the variable (or property). If this is `ReferenceTracker.CALL` then it called the variable (or property). If this is `ReferenceTracker.CONSTRUCT` then it called the variable (or property) with the `new` operator. |
345+
| entry | any | The property value of any of `ReferenceTracker.READ`, `ReferenceTracker.CALL`, and `ReferenceTracker.CONSTRUCT`. |
346+
347+
### Examples
348+
349+
```js
350+
const { ReferenceTracker } = require("@eslint-community/eslint-utils")
351+
352+
module.exports = {
353+
meta: {},
354+
create(context) {
355+
return {
356+
"MetaProperty:exit"(node) {
357+
if (
358+
node.meta.name !== "import" ||
359+
node.property.name !== "meta"
360+
) {
361+
return
362+
}
363+
const tracker = new ReferenceTracker(
364+
context.sourceCode.getScope(context.sourceCode.ast),
365+
)
366+
367+
const traceMap = {
368+
// Find `import.meta.resolve()`.
369+
resolve: {
370+
[ReferenceTracker.CALL]: true,
371+
},
372+
// Find `import.meta.dirname`.
373+
dirname: {
374+
[ReferenceTracker.READ]: true,
375+
},
376+
// Find `import.meta.filename`.
377+
filename: {
378+
[ReferenceTracker.READ]: true,
379+
},
380+
}
381+
382+
for (const { node, path } of tracker.iteratePropertyReferences(
383+
node,
384+
traceMap,
385+
)) {
386+
context.report({
387+
node,
388+
message: "disallow {{name}}.",
389+
data: { name: "import.meta." + path.join(".") },
390+
})
391+
}
392+
},
393+
}
394+
},
395+
}
396+
```

src/reference-tracker.mjs

+10
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ export class ReferenceTracker {
207207
}
208208
}
209209

210+
/**
211+
* Iterate the property references for a given expression AST node.
212+
* @param {object} node The expression AST node to iterate property references.
213+
* @param {object} traceMap The trace map.
214+
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate property references.
215+
*/
216+
*iteratePropertyReferences(node, traceMap) {
217+
yield* this._iteratePropertyReferences(node, [], traceMap)
218+
}
219+
210220
/**
211221
* Iterate the references for a given variable.
212222
* @param {Variable} variable The variable to iterate that references.

test/reference-tracker.mjs

+179
Original file line numberDiff line numberDiff line change
@@ -1052,4 +1052,183 @@ describe("The 'ReferenceTracker' class:", () => {
10521052
})
10531053
}
10541054
})
1055+
1056+
describe("the 'iteratePropertyReferences' method", () => {
1057+
for (const { description, code, traceMap, expected } of [
1058+
{
1059+
description:
1060+
"should iterate the property references of a target expression.",
1061+
code: [
1062+
"const { abc } = target();",
1063+
"abc();",
1064+
"new abc();",
1065+
"abc.xyz;",
1066+
].join("\n"),
1067+
traceMap: {
1068+
abc: {
1069+
[READ]: 1,
1070+
[CALL]: 2,
1071+
[CONSTRUCT]: 3,
1072+
xyz: { [READ]: 4 },
1073+
},
1074+
},
1075+
expected: [
1076+
{
1077+
node: { type: "Property" },
1078+
path: ["abc"],
1079+
type: READ,
1080+
info: 1,
1081+
},
1082+
{
1083+
node: { type: "CallExpression" },
1084+
path: ["abc"],
1085+
type: CALL,
1086+
info: 2,
1087+
},
1088+
{
1089+
node: { type: "NewExpression" },
1090+
path: ["abc"],
1091+
type: CONSTRUCT,
1092+
info: 3,
1093+
},
1094+
{
1095+
node: { type: "MemberExpression" },
1096+
path: ["abc", "xyz"],
1097+
type: READ,
1098+
info: 4,
1099+
},
1100+
],
1101+
},
1102+
{
1103+
description: "should track to the rename property.",
1104+
code: [
1105+
"const { abc: x } = target();",
1106+
"x.a;",
1107+
"x.b();",
1108+
"new x.c();",
1109+
].join("\n"),
1110+
traceMap: {
1111+
abc: {
1112+
a: { [READ]: 1 },
1113+
b: { [CALL]: 2 },
1114+
c: { [CONSTRUCT]: 3 },
1115+
},
1116+
},
1117+
expected: [
1118+
{
1119+
node: { type: "MemberExpression" },
1120+
path: ["abc", "a"],
1121+
type: READ,
1122+
info: 1,
1123+
},
1124+
{
1125+
node: { type: "CallExpression" },
1126+
path: ["abc", "b"],
1127+
type: CALL,
1128+
info: 2,
1129+
},
1130+
{
1131+
node: { type: "NewExpression" },
1132+
path: ["abc", "c"],
1133+
type: CONSTRUCT,
1134+
info: 3,
1135+
},
1136+
],
1137+
},
1138+
{
1139+
description: "should track to the re-assign property.",
1140+
code: [
1141+
"const foo = target();",
1142+
"const bar = foo;",
1143+
"const { abc: x } = bar;",
1144+
"x.a;",
1145+
"x.b();",
1146+
"new x.c();",
1147+
].join("\n"),
1148+
traceMap: {
1149+
abc: {
1150+
a: { [READ]: 1 },
1151+
b: { [CALL]: 2 },
1152+
c: { [CONSTRUCT]: 3 },
1153+
},
1154+
},
1155+
expected: [
1156+
{
1157+
node: { type: "MemberExpression" },
1158+
path: ["abc", "a"],
1159+
type: READ,
1160+
info: 1,
1161+
},
1162+
{
1163+
node: { type: "CallExpression" },
1164+
path: ["abc", "b"],
1165+
type: CALL,
1166+
info: 2,
1167+
},
1168+
{
1169+
node: { type: "NewExpression" },
1170+
path: ["abc", "c"],
1171+
type: CONSTRUCT,
1172+
info: 3,
1173+
},
1174+
],
1175+
},
1176+
]) {
1177+
it(description, () => {
1178+
const linter = newCompatLinter()
1179+
1180+
let actual = null
1181+
linter.verify(code, {
1182+
...config,
1183+
plugins: {
1184+
test: {
1185+
rules: {
1186+
test: {
1187+
create(context) {
1188+
const sourceCode =
1189+
context.sourceCode ||
1190+
context.getSourceCode()
1191+
const tracker = new ReferenceTracker(
1192+
sourceCode.scopeManager.globalScope,
1193+
)
1194+
return {
1195+
"CallExpression:exit"(node) {
1196+
if (
1197+
node.callee.name !==
1198+
"target"
1199+
) {
1200+
return
1201+
}
1202+
actual = Array.from(
1203+
tracker.iteratePropertyReferences(
1204+
node,
1205+
traceMap,
1206+
),
1207+
).map((x) =>
1208+
Object.assign(x, {
1209+
node: {
1210+
type: x.node.type,
1211+
...(x.node.optional
1212+
? {
1213+
optional:
1214+
x.node
1215+
.optional,
1216+
}
1217+
: {}),
1218+
},
1219+
}),
1220+
)
1221+
},
1222+
}
1223+
},
1224+
},
1225+
},
1226+
},
1227+
},
1228+
})
1229+
1230+
assert.deepStrictEqual(actual, expected)
1231+
})
1232+
}
1233+
})
10551234
})

0 commit comments

Comments
 (0)