eslint-plugin-no-mistakes 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.js +1 -0
- package/src/rules/no-banned-import-outside-allowed-paths-aliases.js +193 -0
- package/src/rules/no-banned-import-outside-allowed-paths-config.js +64 -0
- package/src/rules/no-banned-import-outside-allowed-paths-imports.js +201 -0
- package/src/rules/no-banned-import-outside-allowed-paths-scopes.js +117 -0
- package/src/rules/no-banned-import-outside-allowed-paths-tags.js +133 -0
- package/src/rules/no-banned-import-outside-allowed-paths.js +225 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-no-mistakes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "ESLint and Oxlint rules for deterministic no-mistakes code analysis",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@typescript-eslint/parser": "^8.65.0",
|
|
25
25
|
"@vitest/coverage-v8": "^4.1.10",
|
|
26
26
|
"eslint": "^10.8.0",
|
|
27
|
-
"oxlint": "^1.
|
|
27
|
+
"oxlint": "^1.77.0",
|
|
28
28
|
"vitest": "^4.1.6"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const rules = {
|
|
|
10
10
|
"module-mock-preserve-exports": require("./rules/module-mock-preserve-exports"),
|
|
11
11
|
"nextjs-metadata-exports-location": require("./rules/nextjs-metadata-exports-location"),
|
|
12
12
|
"nextjs-no-manual-script-tags": require("./rules/nextjs-no-manual-script-tags"),
|
|
13
|
+
"no-banned-import-outside-allowed-paths": require("./rules/no-banned-import-outside-allowed-paths"),
|
|
13
14
|
"no-global-fetch-outside-helper": require("./rules/no-global-fetch-outside-helper"),
|
|
14
15
|
"no-delete-property": require("./rules/no-delete-property"),
|
|
15
16
|
"no-import-only-test-files": require("./rules/no-import-only-test-files"),
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { propertyName } = require("./module-mock-helpers");
|
|
4
|
+
const {
|
|
5
|
+
bindingIdentifier,
|
|
6
|
+
bindingIdentifiers,
|
|
7
|
+
resolveVariable,
|
|
8
|
+
} = require("./no-global-fetch-outside-helper-bindings");
|
|
9
|
+
const {
|
|
10
|
+
collectAssignmentExpressions,
|
|
11
|
+
collectVariableDeclarators,
|
|
12
|
+
isMaybeExecuted,
|
|
13
|
+
isOptionalChainArgument,
|
|
14
|
+
} = require("./no-global-fetch-outside-helper-traversal");
|
|
15
|
+
const { hasBannedName } = require("./no-banned-import-outside-allowed-paths-config");
|
|
16
|
+
const {
|
|
17
|
+
resolveNodeModuleCreateRequireTag,
|
|
18
|
+
tagForExpression,
|
|
19
|
+
} = require("./no-banned-import-outside-allowed-paths-tags");
|
|
20
|
+
|
|
21
|
+
function setOrClearTag(identifier, tag, context, aliasMap, clearedAliases) {
|
|
22
|
+
const variable = resolveVariable(identifier, context);
|
|
23
|
+
if (!variable) return;
|
|
24
|
+
if (tag) {
|
|
25
|
+
aliasMap.set(variable, tag);
|
|
26
|
+
clearedAliases?.delete(variable);
|
|
27
|
+
} else {
|
|
28
|
+
aliasMap.delete(variable);
|
|
29
|
+
clearedAliases?.add(variable);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolvePropertyTag(initTag, key, config) {
|
|
34
|
+
if (initTag?.kind !== "object" || !key) return null;
|
|
35
|
+
for (const module of initTag.modules) {
|
|
36
|
+
const createRequireTag = resolveNodeModuleCreateRequireTag(module, key);
|
|
37
|
+
if (createRequireTag) return createRequireTag;
|
|
38
|
+
if (hasBannedName(config, module, key)) return { kind: "direct", module, name: key };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Real-time (control-flow-sensitive) destructure recorder: clears every
|
|
44
|
+
// destructured identifier that doesn't resolve to a banned name, and
|
|
45
|
+
// conservatively keeps the whole object tag on a rest element.
|
|
46
|
+
function applyObjectPatternTag(pattern, initTag, context, aliasMap, clearedAliases, config) {
|
|
47
|
+
for (const property of pattern.properties) {
|
|
48
|
+
if (property.type === "RestElement") {
|
|
49
|
+
const identifier = bindingIdentifier(property.argument);
|
|
50
|
+
const tag = initTag?.kind === "object" ? initTag : null;
|
|
51
|
+
if (identifier) setOrClearTag(identifier, tag, context, aliasMap, clearedAliases);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (property.type !== "Property") continue;
|
|
55
|
+
const identifier = bindingIdentifier(property.value);
|
|
56
|
+
if (!identifier) {
|
|
57
|
+
// The target is itself a nested pattern (unsupported for tagging), but
|
|
58
|
+
// any identifier it rebinds must still lose a stale tag from an
|
|
59
|
+
// earlier assignment, or a later use would be a false positive.
|
|
60
|
+
for (const nested of bindingIdentifiers(property.value)) {
|
|
61
|
+
setOrClearTag(nested, null, context, aliasMap, clearedAliases);
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const tag = resolvePropertyTag(initTag, propertyName(property.key), config);
|
|
66
|
+
setOrClearTag(identifier, tag, context, aliasMap, clearedAliases);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function recordVariableTag(
|
|
71
|
+
node,
|
|
72
|
+
context,
|
|
73
|
+
aliasMap,
|
|
74
|
+
clearedAliases,
|
|
75
|
+
config,
|
|
76
|
+
readAliasMap = aliasMap,
|
|
77
|
+
) {
|
|
78
|
+
if (!node.init) return;
|
|
79
|
+
if (node.id.type === "Identifier") {
|
|
80
|
+
const tag = tagForExpression(node.init, context, readAliasMap, config);
|
|
81
|
+
setOrClearTag(node.id, tag, context, aliasMap, clearedAliases);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (node.id.type === "ObjectPattern") {
|
|
85
|
+
const initTag = tagForExpression(node.init, context, readAliasMap, config);
|
|
86
|
+
applyObjectPatternTag(node.id, initTag, context, aliasMap, clearedAliases, config);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function recordAssignmentTag(
|
|
91
|
+
node,
|
|
92
|
+
context,
|
|
93
|
+
aliasMap,
|
|
94
|
+
clearedAliases,
|
|
95
|
+
config,
|
|
96
|
+
readAliasMap = aliasMap,
|
|
97
|
+
) {
|
|
98
|
+
if (isOptionalChainArgument(node)) return;
|
|
99
|
+
if (node.operator === "||=" || node.operator === "??=") return;
|
|
100
|
+
if (node.operator !== "=") {
|
|
101
|
+
if (node.left?.type === "Identifier") {
|
|
102
|
+
setOrClearTag(node.left, null, context, aliasMap, clearedAliases);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (node.left?.type === "Identifier") {
|
|
107
|
+
const tag = tagForExpression(node.right, context, readAliasMap, config);
|
|
108
|
+
setOrClearTag(node.left, tag, context, aliasMap, clearedAliases);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (node.left?.type === "ObjectPattern") {
|
|
112
|
+
const initTag = tagForExpression(node.right, context, readAliasMap, config);
|
|
113
|
+
applyObjectPatternTag(node.left, initTag, context, aliasMap, clearedAliases, config);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (node.left?.type === "ArrayPattern") {
|
|
117
|
+
// Array destructuring never resolves to a tracked module tag (arrays
|
|
118
|
+
// aren't "the module object" in this model), but every identifier it
|
|
119
|
+
// rebinds must still lose a stale tag from an earlier assignment.
|
|
120
|
+
for (const identifier of bindingIdentifiers(node.left)) {
|
|
121
|
+
setOrClearTag(identifier, null, context, aliasMap, clearedAliases);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fixed-point (forward-reference) seeder: add-only, never clears, so repeated
|
|
127
|
+
// passes over the whole program monotonically converge (mirrors the
|
|
128
|
+
// reference rule's `collectPossibleAlias`).
|
|
129
|
+
function applyObjectPatternTagAddOnly(pattern, initTag, context, aliasMap, config) {
|
|
130
|
+
for (const property of pattern.properties) {
|
|
131
|
+
if (property.type === "RestElement") {
|
|
132
|
+
if (initTag.kind !== "object") continue;
|
|
133
|
+
const identifier = bindingIdentifier(property.argument);
|
|
134
|
+
if (identifier) setOrClearTag(identifier, initTag, context, aliasMap);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (property.type !== "Property") continue;
|
|
138
|
+
const identifier = bindingIdentifier(property.value);
|
|
139
|
+
if (!identifier) continue;
|
|
140
|
+
const tag = resolvePropertyTag(initTag, propertyName(property.key), config);
|
|
141
|
+
if (tag) setOrClearTag(identifier, tag, context, aliasMap);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function collectPossibleTag(node, context, aliasMap, config) {
|
|
146
|
+
if (node.type === "VariableDeclarator") {
|
|
147
|
+
if (!node.init) return;
|
|
148
|
+
if (node.id.type === "Identifier") {
|
|
149
|
+
const tag = tagForExpression(node.init, context, aliasMap, config);
|
|
150
|
+
if (tag) setOrClearTag(node.id, tag, context, aliasMap);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (node.id.type === "ObjectPattern") {
|
|
154
|
+
const initTag = tagForExpression(node.init, context, aliasMap, config);
|
|
155
|
+
if (initTag) applyObjectPatternTagAddOnly(node.id, initTag, context, aliasMap, config);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (node.operator !== "=") return;
|
|
160
|
+
if (node.left?.type === "Identifier") {
|
|
161
|
+
const tag = tagForExpression(node.right, context, aliasMap, config);
|
|
162
|
+
if (tag) setOrClearTag(node.left, tag, context, aliasMap);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (node.left?.type === "ObjectPattern") {
|
|
166
|
+
const initTag = tagForExpression(node.right, context, aliasMap, config);
|
|
167
|
+
if (initTag) applyObjectPatternTagAddOnly(node.left, initTag, context, aliasMap, config);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function collectBannedAliases(program, context, aliasMap, config) {
|
|
172
|
+
const candidates = [
|
|
173
|
+
...collectVariableDeclarators(program),
|
|
174
|
+
...collectAssignmentExpressions(program),
|
|
175
|
+
];
|
|
176
|
+
let changed = true;
|
|
177
|
+
while (changed) {
|
|
178
|
+
changed = false;
|
|
179
|
+
for (const node of candidates) {
|
|
180
|
+
if (isMaybeExecuted(node)) continue;
|
|
181
|
+
const before = aliasMap.size;
|
|
182
|
+
collectPossibleTag(node, context, aliasMap, config);
|
|
183
|
+
changed ||= aliasMap.size > before;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
collectBannedAliases,
|
|
190
|
+
recordAssignmentTag,
|
|
191
|
+
recordVariableTag,
|
|
192
|
+
setOrClearTag,
|
|
193
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { repoRelativeFilename, stringMatches } = require("./module-mock-helpers");
|
|
4
|
+
|
|
5
|
+
// Modules whose `createRequire` export is Node's real createRequire()
|
|
6
|
+
// regardless of how a config bans/allows their other exports; tracked
|
|
7
|
+
// unconditionally (see no-banned-import-outside-allowed-paths-tags.js's
|
|
8
|
+
// `resolveNodeModuleCreateRequireTag`) since createRequire is itself a
|
|
9
|
+
// capability that can synchronously load any other banned module, whether
|
|
10
|
+
// obtained via a static `import` or a `require()` call.
|
|
11
|
+
const CREATE_REQUIRE_MODULES = new Set(["node:module", "module"]);
|
|
12
|
+
|
|
13
|
+
// Normalizes the `bannedImports` option into `Map<module, Set<name>>`. The
|
|
14
|
+
// sentinel name `"default"` bans the module's default export when it is used
|
|
15
|
+
// as a directly callable value; any other name bans that named export (or,
|
|
16
|
+
// when reached as a member off a namespace/default/require binding, that
|
|
17
|
+
// method/property name).
|
|
18
|
+
// `entries` is already schema-validated (each item requires a string `module`
|
|
19
|
+
// and a non-empty string-array `names`), so no defensive shape-checking here.
|
|
20
|
+
function normalizeBannedImports(entries) {
|
|
21
|
+
const config = new Map();
|
|
22
|
+
for (const entry of entries ?? []) {
|
|
23
|
+
const names = config.get(entry.module) ?? new Set();
|
|
24
|
+
for (const name of entry.names) names.add(name);
|
|
25
|
+
config.set(entry.module, names);
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasBannedName(config, module, name) {
|
|
31
|
+
return config.get(module)?.has(name) ?? false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasAnyBannedName(config, module) {
|
|
35
|
+
return Boolean(config.get(module)?.size);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `export * from "mod"` never re-exports a module's default export (ES module
|
|
39
|
+
// semantics), so the reserved "default" name must not by itself make an
|
|
40
|
+
// unaliased export-star declaration reachable.
|
|
41
|
+
function hasAnyNonDefaultBannedName(config, module) {
|
|
42
|
+
const names = config.get(module);
|
|
43
|
+
if (!names) return false;
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
if (name !== "default") return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function shouldCheckFile(filename, options) {
|
|
51
|
+
const checked = options?.checkedPathPatterns ?? [];
|
|
52
|
+
if (checked.length === 0) return false;
|
|
53
|
+
const file = repoRelativeFilename(filename);
|
|
54
|
+
return stringMatches(file, checked) && !stringMatches(file, options.allowedPathPatterns ?? []);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
CREATE_REQUIRE_MODULES,
|
|
59
|
+
hasAnyBannedName,
|
|
60
|
+
hasAnyNonDefaultBannedName,
|
|
61
|
+
hasBannedName,
|
|
62
|
+
normalizeBannedImports,
|
|
63
|
+
shouldCheckFile,
|
|
64
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { importSpecifierName } = require("./module-mock-helpers");
|
|
4
|
+
const { resolveVariable } = require("./no-global-fetch-outside-helper-bindings");
|
|
5
|
+
const {
|
|
6
|
+
CREATE_REQUIRE_MODULES,
|
|
7
|
+
hasAnyBannedName,
|
|
8
|
+
hasAnyNonDefaultBannedName,
|
|
9
|
+
hasBannedName,
|
|
10
|
+
} = require("./no-banned-import-outside-allowed-paths-config");
|
|
11
|
+
const {
|
|
12
|
+
resolveNodeModuleCreateRequireTag,
|
|
13
|
+
tagForExpression,
|
|
14
|
+
} = require("./no-banned-import-outside-allowed-paths-tags");
|
|
15
|
+
|
|
16
|
+
function isTypeOnlyImport(node, specifier) {
|
|
17
|
+
return node.importKind === "type" || specifier.importKind === "type";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isTypeOnlyExport(node, specifier) {
|
|
21
|
+
return node.exportKind === "type" || specifier.exportKind === "type";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function seedImportSpecifier(specifier, node, moduleSpecifier, context, config, aliasMap) {
|
|
25
|
+
if (isTypeOnlyImport(node, specifier)) return;
|
|
26
|
+
const variable = resolveVariable(specifier.local, context);
|
|
27
|
+
if (!variable) return;
|
|
28
|
+
if (specifier.type === "ImportSpecifier") {
|
|
29
|
+
const name = importSpecifierName(specifier);
|
|
30
|
+
const createRequireTag = resolveNodeModuleCreateRequireTag(moduleSpecifier, name);
|
|
31
|
+
if (createRequireTag) {
|
|
32
|
+
aliasMap.set(variable, createRequireTag);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (name && hasBannedName(config, moduleSpecifier, name)) {
|
|
36
|
+
aliasMap.set(variable, { kind: "direct", module: moduleSpecifier, name });
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
specifier.type === "ImportDefaultSpecifier" ||
|
|
42
|
+
specifier.type === "ImportNamespaceSpecifier"
|
|
43
|
+
) {
|
|
44
|
+
// A CREATE_REQUIRE_MODULES namespace/default binding is tracked as an
|
|
45
|
+
// object whether or not the config separately bans anything from it, so
|
|
46
|
+
// a member access like `mod.createRequire` still resolves below (see
|
|
47
|
+
// resolveNodeModuleCreateRequireTag) even when nothing else about the
|
|
48
|
+
// module is configured as banned.
|
|
49
|
+
if (hasAnyBannedName(config, moduleSpecifier) || CREATE_REQUIRE_MODULES.has(moduleSpecifier)) {
|
|
50
|
+
aliasMap.set(variable, { kind: "object", modules: new Set([moduleSpecifier]) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Seeds import-derived tags. Import bindings are always top-level and
|
|
56
|
+
// unconditional (re-assigning them is a syntax error), so this is a single
|
|
57
|
+
// non-fixed-point pass, unlike the require()/createRequire() forward pass.
|
|
58
|
+
function seedImportTags(program, context, config, aliasMap) {
|
|
59
|
+
for (const node of program.body) {
|
|
60
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
61
|
+
const moduleSpecifier = node.source.value;
|
|
62
|
+
for (const specifier of node.specifiers) {
|
|
63
|
+
seedImportSpecifier(specifier, node, moduleSpecifier, context, config, aliasMap);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function specifierSourceName(node) {
|
|
69
|
+
return node.type === "Literal" ? String(node.value) : node.name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function reportTagLeak(reportNode, tag, config, context) {
|
|
73
|
+
if (tag?.kind === "direct") {
|
|
74
|
+
context.report({
|
|
75
|
+
node: reportNode,
|
|
76
|
+
messageId: "bannedReExport",
|
|
77
|
+
data: { module: tag.module, name: tag.name },
|
|
78
|
+
});
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (tag?.kind !== "object") return false;
|
|
82
|
+
for (const module of tag.modules) {
|
|
83
|
+
if (hasAnyBannedName(config, module)) {
|
|
84
|
+
context.report({
|
|
85
|
+
node: reportNode,
|
|
86
|
+
messageId: "bannedReExport",
|
|
87
|
+
data: { module, name: "*" },
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Resolves the tag reachable through `variable` for an export check: prefer
|
|
96
|
+
// a live real-time tag, but never fall back to the fixed-point forward tag
|
|
97
|
+
// once the variable has been explicitly cleared (real-time overwritten to
|
|
98
|
+
// something untracked) — otherwise a since-overwritten alias would "revive"
|
|
99
|
+
// its stale, no-longer-true forward tag and produce a false positive. A
|
|
100
|
+
// forward tag is only consulted for a variable never yet touched in real
|
|
101
|
+
// time, i.e. a genuine forward reference.
|
|
102
|
+
function resolveExportedTag(variable, aliasMap, clearedAliases, forwardAliasMap) {
|
|
103
|
+
if (!variable) return null;
|
|
104
|
+
if (aliasMap.has(variable)) return aliasMap.get(variable);
|
|
105
|
+
if (clearedAliases.has(variable)) return null;
|
|
106
|
+
return forwardAliasMap.get(variable) ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function reportLocalReExport(
|
|
110
|
+
specifier,
|
|
111
|
+
context,
|
|
112
|
+
config,
|
|
113
|
+
aliasMap,
|
|
114
|
+
clearedAliases,
|
|
115
|
+
forwardAliasMap,
|
|
116
|
+
) {
|
|
117
|
+
const variable = resolveVariable(specifier.local, context);
|
|
118
|
+
const tag = resolveExportedTag(variable, aliasMap, clearedAliases, forwardAliasMap);
|
|
119
|
+
reportTagLeak(specifier, tag, config, context);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function checkExportedDeclaration(
|
|
123
|
+
declaration,
|
|
124
|
+
context,
|
|
125
|
+
config,
|
|
126
|
+
aliasMap,
|
|
127
|
+
clearedAliases,
|
|
128
|
+
forwardAliasMap,
|
|
129
|
+
) {
|
|
130
|
+
if (declaration?.type !== "VariableDeclaration") return;
|
|
131
|
+
for (const declarator of declaration.declarations) {
|
|
132
|
+
if (declarator.id.type !== "Identifier") continue;
|
|
133
|
+
const variable = resolveVariable(declarator.id, context);
|
|
134
|
+
const tag = resolveExportedTag(variable, aliasMap, clearedAliases, forwardAliasMap);
|
|
135
|
+
reportTagLeak(declarator, tag, config, context);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function checkExportLeaks(node, context, config, aliasMap, clearedAliases, forwardAliasMap) {
|
|
140
|
+
if (node.type === "ExportAllDeclaration") {
|
|
141
|
+
const moduleSpecifier = node.source?.value;
|
|
142
|
+
// An unaliased `export * from "mod"` never re-exports the module's
|
|
143
|
+
// default export (ES module semantics), so a module banned only on
|
|
144
|
+
// "default" exposes nothing reachable through this form.
|
|
145
|
+
if (hasAnyNonDefaultBannedName(config, moduleSpecifier)) {
|
|
146
|
+
context.report({
|
|
147
|
+
node,
|
|
148
|
+
messageId: "bannedReExport",
|
|
149
|
+
data: { module: moduleSpecifier, name: "*" },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// An inline export declaration (`export const compile = ts.createProgram;`)
|
|
155
|
+
// exposes a tagged value directly, with no `specifiers` entry to inspect.
|
|
156
|
+
checkExportedDeclaration(
|
|
157
|
+
node.declaration,
|
|
158
|
+
context,
|
|
159
|
+
config,
|
|
160
|
+
aliasMap,
|
|
161
|
+
clearedAliases,
|
|
162
|
+
forwardAliasMap,
|
|
163
|
+
);
|
|
164
|
+
for (const specifier of node.specifiers ?? []) {
|
|
165
|
+
if (specifier.type !== "ExportSpecifier" || isTypeOnlyExport(node, specifier)) continue;
|
|
166
|
+
if (node.source) {
|
|
167
|
+
const sourceName = specifierSourceName(specifier.local);
|
|
168
|
+
if (hasBannedName(config, node.source.value, sourceName)) {
|
|
169
|
+
context.report({
|
|
170
|
+
node: specifier,
|
|
171
|
+
messageId: "bannedReExport",
|
|
172
|
+
data: { module: node.source.value, name: sourceName },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
reportLocalReExport(specifier, context, config, aliasMap, clearedAliases, forwardAliasMap);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function checkDefaultExportLeak(node, context, config, aliasMap, clearedAliases, forwardAliasMap) {
|
|
182
|
+
const { declaration } = node;
|
|
183
|
+
if (declaration.type === "Identifier") {
|
|
184
|
+
const variable = resolveVariable(declaration, context);
|
|
185
|
+
const tag = resolveExportedTag(variable, aliasMap, clearedAliases, forwardAliasMap);
|
|
186
|
+
reportTagLeak(node, tag, config, context);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// A non-identifier declaration (`export default ts.createProgram;` or
|
|
190
|
+
// `export default require("typescript").createProgram;`) is resolved with
|
|
191
|
+
// the same expression tagger used for calls and aliases, matching
|
|
192
|
+
// real-time (depth-0, no forward merge) semantics.
|
|
193
|
+
const tag = tagForExpression(declaration, context, aliasMap, config);
|
|
194
|
+
reportTagLeak(node, tag, config, context);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
checkDefaultExportLeak,
|
|
199
|
+
checkExportLeaks,
|
|
200
|
+
seedImportTags,
|
|
201
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Tracks binding-tag scopes with discard-on-exit semantics for conditionally
|
|
4
|
+
// executed constructs, mirroring the reference rule
|
|
5
|
+
// no-global-fetch-outside-helper.js's push/pop-scope pattern (if/for/while
|
|
6
|
+
// bodies, switch cases with fallthrough tracking, try block/handler,
|
|
7
|
+
// non-static field initializers, and conditional/logical branch
|
|
8
|
+
// expressions). A tagged alias assigned inside one of these constructs must
|
|
9
|
+
// not leak into the surrounding scope, since the construct may execute zero
|
|
10
|
+
// or more times, or not on every path.
|
|
11
|
+
function createAliasScopeTracker() {
|
|
12
|
+
let aliases = new Map();
|
|
13
|
+
let clearedForwardAliases = new Set();
|
|
14
|
+
const aliasStack = [];
|
|
15
|
+
const clearedAliasStack = [];
|
|
16
|
+
const switchStack = [];
|
|
17
|
+
|
|
18
|
+
function push() {
|
|
19
|
+
aliasStack.push(aliases);
|
|
20
|
+
clearedAliasStack.push(clearedForwardAliases);
|
|
21
|
+
aliases = new Map(aliases);
|
|
22
|
+
clearedForwardAliases = new Set(clearedForwardAliases);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pop() {
|
|
26
|
+
aliases = aliasStack.pop();
|
|
27
|
+
clearedForwardAliases = clearedAliasStack.pop();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Discards the current branch's mutations and starts a fresh clone from
|
|
31
|
+
// the same base a sibling branch (e.g. a ternary's alternate) should see.
|
|
32
|
+
// Used between mutually exclusive branches that share one push(), instead
|
|
33
|
+
// of a second pop-then-push pair keyed to the branch node itself: keying
|
|
34
|
+
// to the branch node races an enter/exit listener on that same node (see
|
|
35
|
+
// the call sites in the main rule file for why).
|
|
36
|
+
function resetBranch() {
|
|
37
|
+
pop();
|
|
38
|
+
push();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A switch case's aliases start from the switch's base state, or, on
|
|
42
|
+
// fallthrough from a case with no terminating break/return/throw, from
|
|
43
|
+
// whatever the previous case left behind; they're discarded again on exit
|
|
44
|
+
// unless that case also falls through.
|
|
45
|
+
function enterSwitch() {
|
|
46
|
+
switchStack.push({ baseAliases: null, baseCleared: null, fallthrough: false });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function exitSwitch() {
|
|
50
|
+
const state = switchStack.pop();
|
|
51
|
+
if (!state.baseAliases) return;
|
|
52
|
+
aliases = state.baseAliases;
|
|
53
|
+
clearedForwardAliases = state.baseCleared;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function enterSwitchCase() {
|
|
57
|
+
const state = switchStack.at(-1);
|
|
58
|
+
if (!state) return;
|
|
59
|
+
if (!state.baseAliases) {
|
|
60
|
+
state.baseAliases = aliases;
|
|
61
|
+
state.baseCleared = clearedForwardAliases;
|
|
62
|
+
}
|
|
63
|
+
if (state.fallthrough) return;
|
|
64
|
+
aliases = new Map(state.baseAliases);
|
|
65
|
+
clearedForwardAliases = new Set(state.baseCleared);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A terminator nested inside a bare `{ }` block still runs unconditionally
|
|
69
|
+
// (the block's statements execute in sequence, same as direct case-body
|
|
70
|
+
// children), so it's scanned too. A terminator nested inside an `if` or
|
|
71
|
+
// other conditional/repeated construct is deliberately NOT scanned: it
|
|
72
|
+
// isn't a guaranteed terminator for the whole case, and treating it as one
|
|
73
|
+
// would wrongly discard a real fallthrough — the same class of unsoundness
|
|
74
|
+
// this scope tracker exists to prevent.
|
|
75
|
+
function terminatesCase(node) {
|
|
76
|
+
if (
|
|
77
|
+
node.type === "BreakStatement" ||
|
|
78
|
+
node.type === "ReturnStatement" ||
|
|
79
|
+
node.type === "ThrowStatement"
|
|
80
|
+
) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return node.type === "BlockStatement" && node.body.some(terminatesCase);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function exitsSwitchCase(node) {
|
|
87
|
+
return node.consequent?.some(terminatesCase) ?? false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function exitSwitchCase(node) {
|
|
91
|
+
const state = switchStack.at(-1);
|
|
92
|
+
if (!state) return;
|
|
93
|
+
state.fallthrough = !exitsSwitchCase(node);
|
|
94
|
+
if (!state.fallthrough) {
|
|
95
|
+
aliases = new Map(state.baseAliases);
|
|
96
|
+
clearedForwardAliases = new Set(state.baseCleared);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
push,
|
|
102
|
+
pop,
|
|
103
|
+
resetBranch,
|
|
104
|
+
enterSwitch,
|
|
105
|
+
exitSwitch,
|
|
106
|
+
enterSwitchCase,
|
|
107
|
+
exitSwitchCase,
|
|
108
|
+
get aliases() {
|
|
109
|
+
return aliases;
|
|
110
|
+
},
|
|
111
|
+
get clearedForwardAliases() {
|
|
112
|
+
return clearedForwardAliases;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { createAliasScopeTracker };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { literalString, memberPropertyName } = require("./module-mock-helpers");
|
|
4
|
+
const { hasLocalBinding, resolveVariable } = require("./no-global-fetch-outside-helper-bindings");
|
|
5
|
+
const {
|
|
6
|
+
CREATE_REQUIRE_MODULES,
|
|
7
|
+
hasBannedName,
|
|
8
|
+
} = require("./no-banned-import-outside-allowed-paths-config");
|
|
9
|
+
|
|
10
|
+
// Tag shapes tracked per resolved variable:
|
|
11
|
+
// { kind: "direct", module, name } - this binding IS a specific banned export's value
|
|
12
|
+
// { kind: "object", modules: Set } - this binding is "the module object" (namespace/default
|
|
13
|
+
// import, require()/dynamic-import() result, or a spread
|
|
14
|
+
// merge of such objects); member access and destructure
|
|
15
|
+
// against it are checked against each module's banned names
|
|
16
|
+
// { kind: "require-fn" } - this binding is a Node `require`-shaped function, either
|
|
17
|
+
// the global `require` or the result of calling createRequire()
|
|
18
|
+
// { kind: "create-require" } - this binding is Node's `createRequire` itself
|
|
19
|
+
|
|
20
|
+
const UNWRAP_TYPES = new Set([
|
|
21
|
+
"ChainExpression",
|
|
22
|
+
"TSAsExpression",
|
|
23
|
+
"TSSatisfiesExpression",
|
|
24
|
+
"TSNonNullExpression",
|
|
25
|
+
"TSInstantiationExpression",
|
|
26
|
+
"TSTypeAssertion",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function unwrapExpression(node) {
|
|
30
|
+
let current = node;
|
|
31
|
+
while (current && (current.type === "AwaitExpression" || UNWRAP_TYPES.has(current.type))) {
|
|
32
|
+
current = current.type === "AwaitExpression" ? current.argument : current.expression;
|
|
33
|
+
}
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isUnshadowedRequire(node, context) {
|
|
38
|
+
return node?.type === "Identifier" && node.name === "require" && !hasLocalBinding(node, context);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tagForIdentifier(node, context, aliasMap) {
|
|
42
|
+
if (node.type !== "Identifier") return null;
|
|
43
|
+
const variable = resolveVariable(node, context);
|
|
44
|
+
return (variable && aliasMap.get(variable)) ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `createRequire` is Node's real createRequire() whenever it's reached off
|
|
48
|
+
// one of CREATE_REQUIRE_MODULES, regardless of the binding form (a static
|
|
49
|
+
// `import { createRequire }` specifier, a require()'d object's destructured
|
|
50
|
+
// property, or a require()'d object's member access) — it's a capability in
|
|
51
|
+
// its own right, not a name a config bans per module.
|
|
52
|
+
function resolveNodeModuleCreateRequireTag(module, name) {
|
|
53
|
+
if (CREATE_REQUIRE_MODULES.has(module) && name === "createRequire") {
|
|
54
|
+
return { kind: "create-require" };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tagForCallExpression(node, context, aliasMap, config) {
|
|
60
|
+
// Resolve the callee through the full expression tagger, not just
|
|
61
|
+
// identifiers, so a `createRequire` reached directly off a member
|
|
62
|
+
// expression (e.g. `nodeModuleNs.createRequire(url)`, with no
|
|
63
|
+
// intermediate variable) is recognized the same way as an aliased one.
|
|
64
|
+
const calleeTag = tagForExpression(node.callee, context, aliasMap, config);
|
|
65
|
+
if (calleeTag?.kind === "create-require") return { kind: "require-fn" };
|
|
66
|
+
const isRequireFn = calleeTag?.kind === "require-fn";
|
|
67
|
+
if (!isRequireFn && !isUnshadowedRequire(node.callee, context)) return null;
|
|
68
|
+
const specifier = literalString(node.arguments[0]);
|
|
69
|
+
if (!specifier) return null;
|
|
70
|
+
// A require()'d CREATE_REQUIRE_MODULES object is tracked whether or not
|
|
71
|
+
// the config separately bans anything from it, so its createRequire
|
|
72
|
+
// property still resolves below even when nothing else about the module
|
|
73
|
+
// is configured as banned.
|
|
74
|
+
if (!CREATE_REQUIRE_MODULES.has(specifier) && !config.has(specifier)) return null;
|
|
75
|
+
return { kind: "object", modules: new Set([specifier]) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tagForMemberExpression(node, context, aliasMap, config) {
|
|
79
|
+
// Resolve the object through the full expression tagger, not just
|
|
80
|
+
// identifiers, so member access chained directly off an inline
|
|
81
|
+
// require()/import() call (e.g. `require("typescript").createProgram()`)
|
|
82
|
+
// is tracked without first assigning the module object to a variable.
|
|
83
|
+
const objectTag = tagForExpression(node.object, context, aliasMap, config);
|
|
84
|
+
if (objectTag?.kind !== "object") return null;
|
|
85
|
+
const name = memberPropertyName(node);
|
|
86
|
+
if (!name) return null;
|
|
87
|
+
for (const module of objectTag.modules) {
|
|
88
|
+
const createRequireTag = resolveNodeModuleCreateRequireTag(module, name);
|
|
89
|
+
if (createRequireTag) return createRequireTag;
|
|
90
|
+
if (hasBannedName(config, module, name)) return { kind: "direct", module, name };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tagForDynamicImport(node, config) {
|
|
96
|
+
const specifier = literalString(node.source);
|
|
97
|
+
if (!specifier || !config.has(specifier)) return null;
|
|
98
|
+
return { kind: "object", modules: new Set([specifier]) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function tagForSpread(node, context, aliasMap, config) {
|
|
102
|
+
const modules = new Set();
|
|
103
|
+
for (const property of node.properties) {
|
|
104
|
+
if (property.type !== "SpreadElement") continue;
|
|
105
|
+
const tag = tagForExpression(property.argument, context, aliasMap, config);
|
|
106
|
+
if (tag?.kind === "object") for (const module of tag.modules) modules.add(module);
|
|
107
|
+
}
|
|
108
|
+
return modules.size > 0 ? { kind: "object", modules } : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function tagForExpression(node, context, aliasMap, config) {
|
|
112
|
+
const unwrapped = unwrapExpression(node);
|
|
113
|
+
if (!unwrapped) return null;
|
|
114
|
+
if (unwrapped.type === "Identifier") return tagForIdentifier(unwrapped, context, aliasMap);
|
|
115
|
+
if (unwrapped.type === "MemberExpression") {
|
|
116
|
+
return tagForMemberExpression(unwrapped, context, aliasMap, config);
|
|
117
|
+
}
|
|
118
|
+
if (unwrapped.type === "CallExpression") {
|
|
119
|
+
return tagForCallExpression(unwrapped, context, aliasMap, config);
|
|
120
|
+
}
|
|
121
|
+
if (unwrapped.type === "ImportExpression") return tagForDynamicImport(unwrapped, config);
|
|
122
|
+
if (unwrapped.type === "ObjectExpression")
|
|
123
|
+
return tagForSpread(unwrapped, context, aliasMap, config);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
isUnshadowedRequire,
|
|
129
|
+
resolveNodeModuleCreateRequireTag,
|
|
130
|
+
tagForExpression,
|
|
131
|
+
tagForIdentifier,
|
|
132
|
+
unwrapExpression,
|
|
133
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { rule } = require("../helpers");
|
|
4
|
+
const {
|
|
5
|
+
hasBannedName,
|
|
6
|
+
normalizeBannedImports,
|
|
7
|
+
shouldCheckFile,
|
|
8
|
+
} = require("./no-banned-import-outside-allowed-paths-config");
|
|
9
|
+
const {
|
|
10
|
+
checkDefaultExportLeak,
|
|
11
|
+
checkExportLeaks,
|
|
12
|
+
seedImportTags,
|
|
13
|
+
} = require("./no-banned-import-outside-allowed-paths-imports");
|
|
14
|
+
const {
|
|
15
|
+
collectBannedAliases,
|
|
16
|
+
recordAssignmentTag,
|
|
17
|
+
recordVariableTag,
|
|
18
|
+
} = require("./no-banned-import-outside-allowed-paths-aliases");
|
|
19
|
+
const { createAliasScopeTracker } = require("./no-banned-import-outside-allowed-paths-scopes");
|
|
20
|
+
const { tagForExpression } = require("./no-banned-import-outside-allowed-paths-tags");
|
|
21
|
+
|
|
22
|
+
module.exports = rule(
|
|
23
|
+
{
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: {
|
|
26
|
+
description: "disallow banned capability imports outside allowed paths",
|
|
27
|
+
recommended: false,
|
|
28
|
+
},
|
|
29
|
+
schema: [
|
|
30
|
+
{
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
checkedPathPatterns: { type: "array", items: { type: "string" } },
|
|
34
|
+
allowedPathPatterns: { type: "array", items: { type: "string" } },
|
|
35
|
+
bannedImports: {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
module: { type: "string" },
|
|
41
|
+
names: { type: "array", items: { type: "string" }, minItems: 1 },
|
|
42
|
+
},
|
|
43
|
+
required: ["module", "names"],
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
messages: {
|
|
52
|
+
bannedImport:
|
|
53
|
+
'Reachable use of banned import "{{name}}" from "{{module}}" outside allowed paths. Move it into an allowed helper path.',
|
|
54
|
+
bannedReExport:
|
|
55
|
+
'Do not re-export banned import "{{name}}" from "{{module}}"; it stays reachable outside allowed paths.',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
(context) => {
|
|
59
|
+
const options = context.options?.[0] ?? {};
|
|
60
|
+
if (!shouldCheckFile(context.filename, options)) return {};
|
|
61
|
+
const config = normalizeBannedImports(options.bannedImports);
|
|
62
|
+
if (config.size === 0) return {};
|
|
63
|
+
|
|
64
|
+
const scopes = createAliasScopeTracker();
|
|
65
|
+
const forwardAliases = new Map();
|
|
66
|
+
let functionDepth = 0;
|
|
67
|
+
|
|
68
|
+
function isIifeFunction(node) {
|
|
69
|
+
const parent = node?.parent;
|
|
70
|
+
return parent?.type === "CallExpression" && parent.callee === node;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pushFunctionScope(node) {
|
|
74
|
+
if (isIifeFunction(node)) return;
|
|
75
|
+
functionDepth += 1;
|
|
76
|
+
scopes.push();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function popFunctionScope(node) {
|
|
80
|
+
if (isIifeFunction(node)) return;
|
|
81
|
+
scopes.pop();
|
|
82
|
+
functionDepth -= 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Merges forward-declared module-scope tags (imports, plus hoistable
|
|
86
|
+
// require()/createRequire() forward references) with the current
|
|
87
|
+
// block-scoped tags when resolving inside a function body. At module
|
|
88
|
+
// depth 0, `aliases` alone reflects real top-to-bottom JS execution
|
|
89
|
+
// order, so no forward merge happens there (matches the reference rule).
|
|
90
|
+
function activeAliases() {
|
|
91
|
+
if (functionDepth === 0) return scopes.aliases;
|
|
92
|
+
const active = new Map(forwardAliases);
|
|
93
|
+
for (const variable of scopes.clearedForwardAliases) active.delete(variable);
|
|
94
|
+
for (const [variable, tag] of scopes.aliases) active.set(variable, tag);
|
|
95
|
+
return active;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function reportCall(node, module, name) {
|
|
99
|
+
context.report({ node, messageId: "bannedImport", data: { module, name } });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Shared by CallExpression and NewExpression: a banned capability is
|
|
103
|
+
// just as reachable through `new BannedClient()` as through
|
|
104
|
+
// `BannedClient()`, and both invocation forms resolve their callee the
|
|
105
|
+
// same way.
|
|
106
|
+
function checkInvocation(node) {
|
|
107
|
+
const tag = tagForExpression(node.callee, context, activeAliases(), config);
|
|
108
|
+
if (tag?.kind === "direct") {
|
|
109
|
+
reportCall(node.callee, tag.module, tag.name);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (tag?.kind !== "object") return;
|
|
113
|
+
for (const module of tag.modules) {
|
|
114
|
+
if (hasBannedName(config, module, "default")) {
|
|
115
|
+
reportCall(node.callee, module, "default");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
Program(node) {
|
|
123
|
+
seedImportTags(node, context, config, scopes.aliases);
|
|
124
|
+
for (const [variable, tag] of scopes.aliases) forwardAliases.set(variable, tag);
|
|
125
|
+
collectBannedAliases(node, context, forwardAliases, config);
|
|
126
|
+
},
|
|
127
|
+
FunctionDeclaration: pushFunctionScope,
|
|
128
|
+
"FunctionDeclaration:exit": popFunctionScope,
|
|
129
|
+
FunctionExpression: pushFunctionScope,
|
|
130
|
+
"FunctionExpression:exit": popFunctionScope,
|
|
131
|
+
ArrowFunctionExpression: pushFunctionScope,
|
|
132
|
+
"ArrowFunctionExpression:exit": popFunctionScope,
|
|
133
|
+
"IfStatement > .consequent": scopes.push,
|
|
134
|
+
"IfStatement > .consequent:exit": scopes.pop,
|
|
135
|
+
"IfStatement > .alternate": scopes.push,
|
|
136
|
+
"IfStatement > .alternate:exit": scopes.pop,
|
|
137
|
+
"ForStatement > .body": scopes.push,
|
|
138
|
+
"ForStatement > .body:exit": scopes.pop,
|
|
139
|
+
"ForInStatement > .body": scopes.push,
|
|
140
|
+
"ForInStatement > .body:exit": scopes.pop,
|
|
141
|
+
"ForOfStatement > .body": scopes.push,
|
|
142
|
+
"ForOfStatement > .body:exit": scopes.pop,
|
|
143
|
+
"WhileStatement > .body": scopes.push,
|
|
144
|
+
"WhileStatement > .body:exit": scopes.pop,
|
|
145
|
+
SwitchStatement: scopes.enterSwitch,
|
|
146
|
+
"SwitchStatement:exit": scopes.exitSwitch,
|
|
147
|
+
SwitchCase: scopes.enterSwitchCase,
|
|
148
|
+
"SwitchCase:exit": scopes.exitSwitchCase,
|
|
149
|
+
"TryStatement > .block": scopes.push,
|
|
150
|
+
"TryStatement > .block:exit": scopes.pop,
|
|
151
|
+
"TryStatement > .handler": scopes.push,
|
|
152
|
+
"TryStatement > .handler:exit": scopes.pop,
|
|
153
|
+
"FieldDefinition[static=false] > .value": scopes.push,
|
|
154
|
+
"FieldDefinition[static=false] > .value:exit": scopes.pop,
|
|
155
|
+
"PropertyDefinition[static=false] > .value": scopes.push,
|
|
156
|
+
"PropertyDefinition[static=false] > .value:exit": scopes.pop,
|
|
157
|
+
// A ternary's or `&&`/`||`'s conditionally-executed operand can itself
|
|
158
|
+
// be a bare AssignmentExpression (no wrapping statement), unlike an
|
|
159
|
+
// `if`/loop/switch/try body, which is always a Statement. Pushing on
|
|
160
|
+
// that operand's own field selector would race the operand's own
|
|
161
|
+
// enter listener (both fire on the same node, and the plain-type
|
|
162
|
+
// listener runs first), so the push/pop below is keyed to the
|
|
163
|
+
// guaranteed-unconditional sibling's exit and the container's exit
|
|
164
|
+
// instead, which always bracket the conditional operand's own enter
|
|
165
|
+
// and exit regardless of listener-specificity ordering.
|
|
166
|
+
"ConditionalExpression > .test:exit": scopes.push,
|
|
167
|
+
"ConditionalExpression > .consequent:exit": scopes.resetBranch,
|
|
168
|
+
"ConditionalExpression:exit": scopes.pop,
|
|
169
|
+
"LogicalExpression > .left:exit": scopes.push,
|
|
170
|
+
"LogicalExpression:exit": scopes.pop,
|
|
171
|
+
VariableDeclarator(node) {
|
|
172
|
+
recordVariableTag(
|
|
173
|
+
node,
|
|
174
|
+
context,
|
|
175
|
+
scopes.aliases,
|
|
176
|
+
scopes.clearedForwardAliases,
|
|
177
|
+
config,
|
|
178
|
+
activeAliases(),
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
AssignmentExpression(node) {
|
|
182
|
+
recordAssignmentTag(
|
|
183
|
+
node,
|
|
184
|
+
context,
|
|
185
|
+
scopes.aliases,
|
|
186
|
+
scopes.clearedForwardAliases,
|
|
187
|
+
config,
|
|
188
|
+
activeAliases(),
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
CallExpression: checkInvocation,
|
|
192
|
+
NewExpression: checkInvocation,
|
|
193
|
+
ExportNamedDeclaration(node) {
|
|
194
|
+
checkExportLeaks(
|
|
195
|
+
node,
|
|
196
|
+
context,
|
|
197
|
+
config,
|
|
198
|
+
scopes.aliases,
|
|
199
|
+
scopes.clearedForwardAliases,
|
|
200
|
+
forwardAliases,
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
ExportAllDeclaration(node) {
|
|
204
|
+
checkExportLeaks(
|
|
205
|
+
node,
|
|
206
|
+
context,
|
|
207
|
+
config,
|
|
208
|
+
scopes.aliases,
|
|
209
|
+
scopes.clearedForwardAliases,
|
|
210
|
+
forwardAliases,
|
|
211
|
+
);
|
|
212
|
+
},
|
|
213
|
+
ExportDefaultDeclaration(node) {
|
|
214
|
+
checkDefaultExportLeak(
|
|
215
|
+
node,
|
|
216
|
+
context,
|
|
217
|
+
config,
|
|
218
|
+
scopes.aliases,
|
|
219
|
+
scopes.clearedForwardAliases,
|
|
220
|
+
forwardAliases,
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
);
|