This repository has been archived by the owner on Mar 25, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 887
/
importBlacklistRule.ts
274 lines (247 loc) · 11.2 KB
/
importBlacklistRule.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/**
* @license
* Copyright 2018 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
findImports,
ImportKind,
isExportDeclaration,
isImportDeclaration,
isNamedExports,
isNamedImports,
} from "tsutils";
import * as ts from "typescript";
import * as Lint from "../index";
export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "import-blacklist",
description: Lint.Utils.dedent`
Disallows importing the specified modules via \`import\` and \`require\`,
or importing specific named exports of the specified modules,
or using imports matching specified regular expression patterns.`,
rationale: Lint.Utils.dedent`
For some libraries, importing the library directly can cause unused
submodules to be loaded, so you may want to block these imports and
require that users directly import only the submodules they need.
In other cases, you may simply want to ban an import because using
it is undesirable or unsafe.`,
optionsDescription:
"A list of blacklisted modules, named imports, or regular expression patterns.",
options: {
type: "array",
items: {
oneOf: [
{
type: "string",
minLength: 1,
},
{
type: "object",
additionalProperties: {
type: "array",
minItems: 1,
items: {
type: "string",
minLength: 1,
},
},
},
{
type: "array",
items: {
type: "string",
},
minLength: 1,
},
],
},
},
optionExamples: [
true,
[true, "rxjs", "lodash"],
[true, [".*\\.temp$", ".*\\.tmp$"]],
[true, { lodash: ["pull", "pullAll"] }],
[true, "lodash", { lodash: ["pull", "pullAll"] }],
[true, "rxjs", { lodash: ["pull", "pullAll"] }, [".*\\.temp$", ".*\\.tmp$"]],
],
type: "functionality",
typescriptOnly: false,
};
public static WHOLE_MODULE_FAILURE_STRING =
"Importing this module is blacklisted. Try importing a submodule instead.";
public static IMPLICIT_NAMED_IMPORT_FAILURE_STRING =
"Some named exports from this module are blacklisted for importing " +
"(or re-exporting). Import/re-export only the specific values you want, " +
"instead of the whole module.";
public static FAILURE_STRING_REGEX = "This import is blacklisted by ";
public static MAKE_NAMED_IMPORT_FAILURE_STRING(importName: string) {
return importName === "default"
? "Importing (or re-exporting) the default export is blacklisted."
: `The export "${importName}" is blacklisted.`;
}
public isEnabled(): boolean {
return super.isEnabled() && this.ruleArguments.length > 0;
}
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk, this.ruleArguments);
}
}
type Options = Array<string | { [moduleName: string]: string[] } | string[]>;
function walk(ctx: Lint.WalkContext<Options>) {
interface BannedImports {
[moduleName: string]: true | Set<string>;
}
// Merge/normalize options.
// E.g., ["a", { "b": ["c"], "d": ["e", "e"] }, "f", { "f": ["g"] }]
// becomes { "a": true, "b": Set(["c"]), "d": Set(["e"]), "f": true }.
const bannedImports = ctx.options.reduce<BannedImports>(
(acc, it) => {
if (typeof it === "string") {
acc[it] = true;
} else if (!Array.isArray(it)) {
Object.keys(it).forEach(moduleName => {
if (acc[moduleName] instanceof Set) {
it[moduleName].forEach(bannedImport => {
(acc[moduleName] as Set<string>).add(bannedImport);
});
} else if (acc[moduleName] !== true) {
acc[moduleName] = new Set(it[moduleName]);
}
});
}
return acc;
},
Object.create(null) as BannedImports,
);
const regexOptions = [];
for (const option of ctx.options) {
if (Array.isArray(option)) {
for (const pattern of option) {
regexOptions.push(RegExp(pattern));
}
}
}
for (const name of findImports(ctx.sourceFile, ImportKind.All)) {
// TODO #3963: Resolve/normalize relative file imports to a canonical path?
const importedModule = name.text;
const bansForModule = bannedImports[importedModule];
// Check if at least some imports from this module are banned.
if (bansForModule !== undefined) {
// If importing this module is totally banned, we can error now,
// without determining whether the user is importing the whole
// module or named exports.
if (bansForModule === true) {
ctx.addFailure(
name.getStart(ctx.sourceFile) + 1,
name.end - 1,
Rule.WHOLE_MODULE_FAILURE_STRING,
);
continue;
}
// Otherwise, find the named imports, if any, and fail if the
// user tried to import any of them. We don't have named imports
// when the user is importing the whole module, which includes:
//
// - ImportKind.Require (i.e., `require('module-specifier')`),
// - ImportKind.DynamicImport (i.e., `import("module-specifier")`),
// - ImportKind.ImportEquals (i.e., `import x = require()`),
// - and ImportKind.ImportDeclaration, where there's a full namespace
// import (i.e. `import * as x from "module-specifier"`)
//
// However, namedImports will be an array when we have one of the
// various permutations of `import x, { a, b as c } from "y"`.
//
// We treat re-exports from other modules the same as attempting to
// import the re-exported binding(s), as the re-export is essentially
// an import followed by an export, and not treating these as an
// import would allow backdoor imports of the banned bindings. So,
// our last case is `ImportKind.ExportFrom`, and for that:
//
// - `export nameForDefault from "module"` isn't part of the ESM
// syntax (yet), so we only have to handle two cases below:
// `export { x } from "y"` and `export * from "specifier"`.
const parentNode = name.parent;
// Disable strict-boolean-expressions for the next few lines so our &&
// checks can help type inference figure out if when don't have undefined.
// tslint:disable strict-boolean-expressions
const importClause =
parentNode && isImportDeclaration(parentNode) ? parentNode.importClause : undefined;
const importsDefaultExport = importClause && Boolean(importClause.name);
// Below, check isNamedImports to rule out the
// `import * as ns from "..."` case.
const importsSpecificNamedExports =
importClause &&
importClause.namedBindings &&
isNamedImports(importClause.namedBindings);
// If parentNode is an ExportDeclaration, it must represent an
// `export from`, as findImports verifies that. Then, if exportClause
// is undefined, we're dealing with `export * from ...`.
const reExportsSpecificNamedExports =
parentNode && isExportDeclaration(parentNode) && Boolean(parentNode.exportClause);
// tslint:enable strict-boolean-expressions
if (
importsDefaultExport ||
importsSpecificNamedExports ||
reExportsSpecificNamedExports
) {
// Add an import for the default import and any named bindings.
// For the named bindings, we use the name of the export from the
// module (i.e., .propertyName) over its alias in the import when
// the two diverge.
const toExportName = (it: ts.ImportSpecifier | ts.ExportSpecifier) =>
(it.propertyName || it.name).text; // tslint:disable-line strict-boolean-expressions
const exportClause = reExportsSpecificNamedExports
? (parentNode as ts.ExportDeclaration).exportClause!
: undefined;
const namedImportsOrReExports = [
...(importsDefaultExport ? ["default"] : []),
...(importsSpecificNamedExports
? (importClause!.namedBindings as ts.NamedImports).elements.map(
toExportName,
)
: []),
...(exportClause !== undefined && isNamedExports(exportClause)
? exportClause.elements.map(toExportName)
: []),
];
for (const importName of namedImportsOrReExports) {
if (bansForModule.has(importName)) {
ctx.addFailureAtNode(
exportClause !== undefined ? exportClause : importClause!,
Rule.MAKE_NAMED_IMPORT_FAILURE_STRING(importName),
);
}
}
} else {
// If we're here, the user tried to import/re-export the whole module
ctx.addFailure(
name.getStart(ctx.sourceFile) + 1,
name.end - 1,
Rule.IMPLICIT_NAMED_IMPORT_FAILURE_STRING,
);
}
}
for (const regex of regexOptions) {
if (regex.test(name.text)) {
ctx.addFailure(
name.getStart(ctx.sourceFile) + 1,
name.end - 1,
Rule.FAILURE_STRING_REGEX + regex.toString(),
);
}
}
}
}