Skip to content

Commit 3607f2f

Browse files
committed
test: add recursive property accessor test
1 parent 4451309 commit 3607f2f

File tree

1 file changed

+94
-0
lines changed

1 file changed

+94
-0
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Flags: --no-warnings
2+
import '../common/index.mjs';
3+
import { builtinModules } from 'node:module';
4+
5+
// This test recursively checks all properties on builtin modules and ensures that accessing them
6+
// does not throw. The only valid reason for property accessor to throw is "invalid this" error
7+
// when property must be not accessible directly on the prototype.
8+
9+
// Normally we don't have properties nested too deep
10+
const MAX_NESTING_DEPTH = 16;
11+
12+
// Some properties can be present or absent depending on the environment
13+
const knownExceptions = {
14+
module: [
15+
// The _cache and _pathCache are populated with local paths
16+
// Also, trying to access _cache.<common/index.js>.exports.then would throw
17+
/^\{module\}(?:\.Module|\.default|)\.(?:_cache|_pathCache)\./,
18+
],
19+
};
20+
21+
function isValid({ key, moduleName, path, propName, value, obj, fullPath }) {
22+
if (knownExceptions[moduleName])
23+
for (const skipRegex of knownExceptions[moduleName])
24+
if (skipRegex.test(fullPath))
25+
return false;
26+
27+
return true;
28+
}
29+
30+
async function buildList(obj, moduleName, path = [], visited = new WeakMap()) {
31+
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) return [];
32+
33+
if (path.length > MAX_NESTING_DEPTH) {
34+
throw new Error(`Too deeply nested property in ${moduleName}: ${path.join('.')}`);
35+
}
36+
37+
// Exclude circular references
38+
// Don't use plain Set, because same object can be exposed under different paths
39+
if (visited.has(obj)) {
40+
const visitedPaths = visited.get(obj);
41+
if (visitedPaths.some((prev) => prev.length <= path.length && prev.every((seg, i) => seg === path[i]))) {
42+
return [];
43+
}
44+
visitedPaths.push(path);
45+
} else {
46+
visited.set(obj, [path]);
47+
}
48+
49+
const paths = [];
50+
const deeperCalls = [];
51+
52+
for (const key of Reflect.ownKeys(obj)) {
53+
const propName = typeof key === 'symbol' ? `[${key.description}]` : key;
54+
const fullPath = `{${moduleName}}.${path.join('.')}${path.length ? '.' : ''}${propName}`;
55+
56+
if (!isValid({ key, moduleName, path, propName, obj, fullPath })) {
57+
continue;
58+
}
59+
60+
let value;
61+
try {
62+
value = await obj[key];
63+
} catch (cause) {
64+
// Accessing some properties directly on the prototype may throw or reject
65+
// Throw informative errors if access failed anywhere/anyhow else
66+
if (cause.name !== 'TypeError') {
67+
throw new Error(`Access to ${fullPath} failed with name=${cause.name}`, { cause });
68+
}
69+
if (cause.code !== 'ERR_INVALID_THIS' && cause.code !== undefined) {
70+
throw new Error(`Access to ${fullPath} failed with code=${cause.code}`, { cause });
71+
}
72+
if (path.at(-1) !== 'prototype') {
73+
throw new Error(`Access to ${fullPath} failed but it's not on prototype`, { cause });
74+
}
75+
}
76+
paths.push(fullPath);
77+
78+
if ((typeof value === 'object' || typeof value === 'function') && value !== obj) {
79+
deeperCalls.push(buildList(value, moduleName, [...path, propName], visited));
80+
}
81+
}
82+
83+
return [...paths, ...(await Promise.all(deeperCalls)).flat()];
84+
}
85+
86+
let total = 0;
87+
88+
await Promise.all(builtinModules.map(async (moduleName) => {
89+
const module = await import(moduleName);
90+
const { length } = await buildList(module, moduleName);
91+
total += length;
92+
}));
93+
94+
console.log(`Checked ${total} properties across ${builtinModules.length} modules.`);

0 commit comments

Comments
 (0)