This repository has been archived by the owner on Jun 11, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 149
/
module-info.ts
409 lines (368 loc) · 16.4 KB
/
module-info.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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
import assert = require("assert");
import * as path from "path";
import * as ts from "typescript";
import { FS } from "../get-definitely-typed";
import { hasWindowsSlashes, joinPaths, normalizeSlashes, sort } from "../util/util";
import { readFileAndThrowOnBOM } from "./definition-parser";
export function getModuleInfo(packageName: string, all: Map<string, ts.SourceFile>): ModuleInfo {
const dependencies = new Set<string>();
const declaredModules: string[] = [];
const globals = new Set<string>();
function addDependency(ref: string): void {
if (ref.startsWith(".")) { return; }
const dependency = rootName(ref, all, packageName);
if (dependency !== packageName) {
dependencies.add(dependency);
}
// TODO: else throw new Error(`Package ${packageName} references itself. (via ${src.fileName})`);
}
for (const sourceFile of all.values()) {
for (const ref of imports(sourceFile)) {
addDependency(ref);
}
for (const ref of sourceFile.typeReferenceDirectives) {
addDependency(ref.fileName);
}
if (ts.isExternalModule(sourceFile)) {
if (sourceFileExportsSomething(sourceFile)) {
declaredModules.push(properModuleName(packageName, sourceFile.fileName));
const namespaceExport = sourceFile.statements.find(ts.isNamespaceExportDeclaration);
if (namespaceExport) {
globals.add(namespaceExport.name.text);
}
}
} else {
for (const node of sourceFile.statements) {
switch (node.kind) {
case ts.SyntaxKind.ModuleDeclaration: {
const decl = node as ts.ModuleDeclaration;
const name = decl.name.text;
if (decl.name.kind === ts.SyntaxKind.StringLiteral) {
declaredModules.push(assertNoWindowsSlashes(packageName, name));
} else if (isValueNamespace(decl)) {
globals.add(name);
}
break;
}
case ts.SyntaxKind.VariableStatement:
for (const decl of (node as ts.VariableStatement).declarationList.declarations) {
if (decl.name.kind === ts.SyntaxKind.Identifier) {
globals.add(decl.name.text);
}
}
break;
case ts.SyntaxKind.EnumDeclaration:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.FunctionDeclaration: {
// Deliberately not doing this for types, because those won't show up in JS code and can't be used for ATA
const nameNode = (node as ts.EnumDeclaration | ts.ClassDeclaration | ts.FunctionDeclaration).name;
if (nameNode) {
globals.add(nameNode.text);
}
break;
}
case ts.SyntaxKind.ImportEqualsDeclaration:
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.TypeAliasDeclaration:
break;
default:
throw new Error(`Unexpected node kind ${ts.SyntaxKind[node.kind]}`);
}
}
}
}
return { dependencies, declaredModules, globals: sort(globals) };
}
/**
* A file is a proper module if it is an external module *and* it has at least one export.
* A module with only imports is not a proper module; it likely just augments some other module.
*/
function sourceFileExportsSomething({ statements }: ts.SourceFile): boolean {
return statements.some(statement => {
switch (statement.kind) {
case ts.SyntaxKind.ImportEqualsDeclaration:
case ts.SyntaxKind.ImportDeclaration:
return false;
case ts.SyntaxKind.ModuleDeclaration:
return (statement as ts.ModuleDeclaration).name.kind === ts.SyntaxKind.Identifier;
default:
return true;
}
});
}
interface ModuleInfo {
dependencies: Set<string>;
// Anything from a `declare module "foo"`
declaredModules: string[];
// Every global symbol
globals: string[];
}
/**
* Given a file name, get the name of the module it declares.
* `foo/index.d.ts` declares "foo", `foo/bar.d.ts` declares "foo/bar", "foo/bar/index.d.ts" declares "foo/bar"
*/
function properModuleName(folderName: string, fileName: string): string {
const part = path.basename(fileName) === "index.d.ts" ? path.dirname(fileName) : withoutExtension(fileName, ".d.ts");
return part === "." ? folderName : joinPaths(folderName, part);
}
/**
* "foo/bar/baz" -> "foo"; "@foo/bar/baz" -> "@foo/bar"
* Note: Throws an error for references like
* "bar/v3" because referencing old versions of *other* packages is illegal;
* those directories won't exist in the published @types package.
*/
function rootName(importText: string, typeFiles: Map<string, unknown>, packageName: string): string {
let slash = importText.indexOf("/");
// Root of `@foo/bar/baz` is `@foo/bar`
if (importText.startsWith("@")) {
// Use second "/"
slash = importText.indexOf("/", slash + 1);
}
const root = importText.slice(0, slash);
const postImport = importText.slice(slash + 1);
if (slash > -1 && postImport.match(/v\d+$/) && !typeFiles.has(postImport + ".d.ts") && root !== packageName) {
throw new Error(`${importText}: do not directly import specific versions of another types package.
You should work with the latest version of ${root} instead.`);
}
return slash === -1 ? importText : root;
}
function withoutExtension(str: string, ext: string): string {
assert(str.endsWith(ext));
return str.slice(0, str.length - ext.length);
}
/** Returns a map from filename (path relative to `directory`) to the SourceFile we parsed for it. */
export function allReferencedFiles(
entryFilenames: ReadonlyArray<string>, fs: FS, packageName: string, baseDirectory: string,
): { types: Map<string, ts.SourceFile>, tests: Map<string, ts.SourceFile> } {
const seenReferences = new Set<string>();
const types = new Map<string, ts.SourceFile>();
const tests = new Map<string, ts.SourceFile>();
entryFilenames.forEach(text => recur({ text, exact: true }));
return { types, tests };
function recur({ text, exact }: Reference): void {
if (seenReferences.has(text)) {
return;
}
seenReferences.add(text);
const resolvedFilename = exact ? text : resolveModule(text, fs);
if (fs.exists(resolvedFilename)) {
const src = createSourceFile(resolvedFilename, readFileAndThrowOnBOM(resolvedFilename, fs));
if (resolvedFilename.endsWith(".d.ts")) {
types.set(resolvedFilename, src);
} else {
tests.set(resolvedFilename, src);
}
const refs = findReferencedFiles(
src,
packageName,
path.dirname(resolvedFilename),
normalizeSlashes(path.relative(baseDirectory, fs.debugPath())),
);
refs.forEach(recur);
}
}
}
function resolveModule(importSpecifier: string, fs: FS): string {
importSpecifier = importSpecifier.endsWith("/") ? importSpecifier.slice(0, importSpecifier.length - 1) : importSpecifier;
if (importSpecifier !== "." && importSpecifier !== "..") {
if (fs.exists(importSpecifier + ".d.ts")) {
return importSpecifier + ".d.ts";
}
if (fs.exists(importSpecifier + ".ts")) {
return importSpecifier + ".ts";
}
if (fs.exists(importSpecifier + ".tsx")) {
return importSpecifier + ".tsx";
}
}
return importSpecifier === "." ? "index.d.ts" : joinPaths(importSpecifier, "index.d.ts");
}
interface Reference {
/** <reference path> includes exact filename, so true. import "foo" may reference "foo.d.ts" or "foo/index.d.ts", so false. */
readonly exact: boolean;
text: string;
}
/**
* @param subDirectory The specific directory within the DefinitelyTyped directory we are in.
* For example, `baseDirectory` may be `react-router` and `subDirectory` may be `react-router/lib`.
* versionsBaseDirectory may be "" when not in typesVersions or ".." when inside `react-router/ts3.1`
*/
function findReferencedFiles(src: ts.SourceFile, packageName: string, subDirectory: string, baseDirectory: string) {
const refs: Reference[] = [];
for (const ref of src.referencedFiles) {
// Any <reference path="foo"> is assumed to be local
addReference({ text: ref.fileName, exact: true });
}
for (const ref of src.typeReferenceDirectives) {
// only <reference types="../packagename/x" /> references are local (or "packagename/x", though in 3.7 that doesn't work in DT).
if (ref.fileName.startsWith("../" + packageName + "/")) {
addReference({ text: ref.fileName, exact: false });
} else if (ref.fileName.startsWith(packageName + "/")) {
addReference({ text: convertToRelativeReference(ref.fileName), exact: false });
}
}
for (const ref of imports(src)) {
if (ref.startsWith(".")) {
addReference({ text: ref, exact: false });
}
if (ref.startsWith(packageName + "/")) {
addReference({ text: convertToRelativeReference(ref), exact: false });
}
}
return refs;
function addReference(ref: Reference): void {
// `path.normalize` may add windows slashes
const full = normalizeSlashes(path.normalize(joinPaths(subDirectory, assertNoWindowsSlashes(src.fileName, ref.text))));
// allow files in typesVersions directories (i.e. 'ts3.1') to reference files in parent directory
if (full.startsWith("../" + packageName + "/")) {
ref.text = full.slice(packageName.length + 4);
refs.push(ref);
return;
}
if (full.startsWith("..")
&& (baseDirectory === "" || path.normalize(joinPaths(baseDirectory, full)).startsWith(".."))) {
throw new Error(
`${src.fileName}: ` +
'Definitions must use global references to other packages, not parent ("../xxx") references.' +
`(Based on reference '${ref.text}')`);
}
ref.text = full;
refs.push(ref);
}
/** boring/foo -> ./foo when subDirectory === '.'; ../foo when it's === 'x'; ../../foo when it's 'x/y' */
function convertToRelativeReference(name: string) {
const relative = "." + "/..".repeat(subDirectory === "." ? 0 : subDirectory.split("/").length);
return relative + name.slice(packageName.length);
}
}
/**
* All strings referenced in `import` statements.
* Does *not* include <reference> directives.
*/
function* imports({ statements }: ts.SourceFile | ts.ModuleBlock): Iterable<string> {
for (const node of statements) {
switch (node.kind) {
case ts.SyntaxKind.ImportDeclaration:
case ts.SyntaxKind.ExportDeclaration: {
const { moduleSpecifier } = node as ts.ImportDeclaration | ts.ExportDeclaration;
if (moduleSpecifier && moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) {
yield (moduleSpecifier as ts.StringLiteral).text;
}
break;
}
case ts.SyntaxKind.ImportEqualsDeclaration: {
const { moduleReference } = node as ts.ImportEqualsDeclaration;
if (moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) {
yield parseRequire(moduleReference);
}
break;
}
case ts.SyntaxKind.ModuleDeclaration: {
const { name, body } = node as ts.ModuleDeclaration;
if (name.kind === ts.SyntaxKind.StringLiteral && body) {
yield* imports(body as ts.ModuleBlock);
}
break;
}
default:
}
}
}
function parseRequire(reference: ts.ExternalModuleReference): string {
const { expression } = reference;
if (!expression || !ts.isStringLiteral(expression)) {
throw new Error(`Bad 'import =' reference: ${reference.getText()}`);
}
return expression.text;
}
function isValueNamespace(ns: ts.ModuleDeclaration): boolean {
if (!ns.body) {
throw new Error("@types should not use shorthand ambient modules");
}
return ns.body.kind === ts.SyntaxKind.ModuleDeclaration
? isValueNamespace(ns.body as ts.ModuleDeclaration)
: (ns.body as ts.ModuleBlock).statements.some(statementDeclaresValue);
}
function statementDeclaresValue(statement: ts.Statement): boolean {
switch (statement.kind) {
case ts.SyntaxKind.VariableStatement:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.EnumDeclaration:
return true;
case ts.SyntaxKind.ModuleDeclaration:
return isValueNamespace(statement as ts.ModuleDeclaration);
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.TypeAliasDeclaration:
case ts.SyntaxKind.ImportEqualsDeclaration:
return false;
default:
throw new Error(`Forgot to implement ambient namespace statement ${ts.SyntaxKind[statement.kind]}`);
}
}
function assertNoWindowsSlashes(packageName: string, fileName: string): string {
if (hasWindowsSlashes(fileName)) {
throw new Error(`In ${packageName}: Use forward slash instead when referencing ${fileName}`);
}
return fileName;
}
export function getTestDependencies(
packageName: string,
typeFiles: Map<string, unknown>,
testFiles: Iterable<string>,
dependencies: ReadonlySet<string>,
fs: FS,
): Iterable<string> {
const testDependencies = new Set<string>();
for (const filename of testFiles) {
const content = readFileAndThrowOnBOM(filename, fs);
const sourceFile = createSourceFile(filename, content);
const { fileName, referencedFiles, typeReferenceDirectives } = sourceFile;
const filePath = () => path.join(packageName, fileName);
let hasImports = false;
let isModule = false;
let referencesSelf = false;
for (const { fileName: ref } of referencedFiles) {
throw new Error(`Test files should not use '<reference path="" />'. '${filePath()}' references '${ref}'.`);
}
for (const { fileName: referencedPackage } of typeReferenceDirectives) {
if (dependencies.has(referencedPackage)) {
throw new Error(
`'${filePath()}' unnecessarily references '${referencedPackage}', which is already referenced in the type definition.`);
}
if (referencedPackage === packageName) {
referencesSelf = true;
}
testDependencies.add(referencedPackage);
}
for (const imported of imports(sourceFile)) {
hasImports = true;
if (!imported.startsWith(".")) {
const dep = rootName(imported, typeFiles, packageName);
if (!dependencies.has(dep) && dep !== packageName) {
testDependencies.add(dep);
}
}
}
isModule = hasImports || (() => {
// FIXME: This results in files without imports to be walked twice,
// once in the `imports(...)` function, and once more here:
for (const node of sourceFile.statements) {
if (
node.kind === ts.SyntaxKind.ExportAssignment ||
node.kind === ts.SyntaxKind.ExportDeclaration
) {
return true;
}
}
return false;
})();
if (isModule && referencesSelf) {
throw new Error(`'${filePath()}' unnecessarily references the package. This can be removed.`);
}
}
return testDependencies;
}
export function createSourceFile(filename: string, content: string): ts.SourceFile {
return ts.createSourceFile(filename, content, ts.ScriptTarget.Latest, /*setParentNodes*/false);
}