-
Notifications
You must be signed in to change notification settings - Fork 618
/
Copy pathDependencyAnalyzer.ts
282 lines (252 loc) · 9.81 KB
/
DependencyAnalyzer.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
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type * as ts from 'typescript';
import { Path } from './Path';
import type { PackletAnalyzer } from './PackletAnalyzer';
enum RefFileKind {
Import,
ReferenceFile,
TypeReferenceDirective
}
// TypeScript compiler internal:
// Version range: >= 3.6.0, <= 4.2.0
// https://github.com/microsoft/TypeScript/blob/5ecdcef4cecfcdc86bd681b377636422447507d7/src/compiler/program.ts#L541
interface IRefFile {
// The absolute path of the module that was imported.
// (Normalized to an all lowercase ts.Path string.)
referencedFileName: string;
// The kind of reference.
kind: RefFileKind;
// An index indicating the order in which items occur in a compound expression
index: number;
// The absolute path of the source file containing the import statement.
// (Normalized to an all lowercase ts.Path string.)
file: string;
}
// TypeScript compiler internal:
// Version range: > 4.2.0
// https://github.com/microsoft/TypeScript/blob/2eca17d7c1a3fb2b077f3a910d5019d74b6f07a0/src/compiler/types.ts#L3693
enum FileIncludeKind {
RootFile,
SourceFromProjectReference,
OutputFromProjectReference,
Import,
ReferenceFile,
TypeReferenceDirective,
LibFile,
LibReferenceDirective,
AutomaticTypeDirectiveFile
}
// TypeScript compiler internal:
// Version range: > 4.2.0
// https://github.com/microsoft/TypeScript/blob/2eca17d7c1a3fb2b077f3a910d5019d74b6f07a0/src/compiler/types.ts#L3748
interface IFileIncludeReason {
kind: FileIncludeKind;
file: string | undefined;
}
interface ITsProgramInternals extends ts.Program {
// TypeScript compiler internal:
// Version range: >= 3.6.0, <= 4.2.0
// https://github.com/microsoft/TypeScript/blob/5ecdcef4cecfcdc86bd681b377636422447507d7/src/compiler/types.ts#L3723
getRefFileMap?: () => Map<string, IRefFile[]> | undefined;
// TypeScript compiler internal:
// Version range: > 4.2.0
// https://github.com/microsoft/TypeScript/blob/2eca17d7c1a3fb2b077f3a910d5019d74b6f07a0/src/compiler/types.ts#L3871
getFileIncludeReasons?: () => Map<string, IFileIncludeReason[]>;
}
/**
* Represents a packlet that imports another packlet.
*/
export interface IPackletImport {
/**
* The name of the packlet being imported.
*/
packletName: string;
/**
* The absolute path of the file that imports the packlet.
*/
fromFilePath: string;
}
/**
* Used to build a linked list of imports that represent a circular dependency.
*/
interface IImportListNode extends IPackletImport {
/**
* The previous link in the linked list.
*/
previousNode: IImportListNode | undefined;
}
export class DependencyAnalyzer {
/**
* @param packletName - the packlet to be checked next in our traversal
* @param startingPackletName - the packlet that we started with; if the traversal reaches this packlet,
* then a circular dependency has been detected
* @param refFileMap - the compiler's `refFileMap` data structure describing import relationships
* @param fileIncludeReasonsMap - the compiler's data structure describing import relationships
* @param program - the compiler's `ts.Program` object
* @param packletsFolderPath - the absolute path of the "src/packlets" folder.
* @param visitedPacklets - the set of packlets that have already been visited in this traversal
* @param previousNode - a linked list of import statements that brought us to this step in the traversal
*/
private static _walkImports(
packletName: string,
startingPackletName: string,
refFileMap: Map<string, IRefFile[]> | undefined,
fileIncludeReasonsMap: Map<string, IFileIncludeReason[]> | undefined,
program: ts.Program,
packletsFolderPath: string,
visitedPacklets: Set<string>,
previousNode: IImportListNode | undefined
): IImportListNode | undefined {
visitedPacklets.add(packletName);
const packletEntryPoint: string = Path.join(packletsFolderPath, packletName, 'index');
const tsSourceFile: ts.SourceFile | undefined =
program.getSourceFile(packletEntryPoint + '.ts') || program.getSourceFile(packletEntryPoint + '.tsx');
if (!tsSourceFile) {
return undefined;
}
const referencingFilePaths: string[] = [];
if (refFileMap) {
// TypeScript version range: >= 3.6.0, <= 4.2.0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const refFiles: IRefFile[] | undefined = refFileMap.get((tsSourceFile as any).path);
if (refFiles) {
for (const refFile of refFiles) {
if (refFile.kind === RefFileKind.Import) {
referencingFilePaths.push(refFile.file);
}
}
}
} else if (fileIncludeReasonsMap) {
// Typescript version range: > 4.2.0
const fileIncludeReasons: IFileIncludeReason[] | undefined = fileIncludeReasonsMap.get(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(tsSourceFile as any).path
);
if (fileIncludeReasons) {
for (const fileIncludeReason of fileIncludeReasons) {
if (fileIncludeReason.kind === FileIncludeKind.Import) {
if (fileIncludeReason.file) {
referencingFilePaths.push(fileIncludeReason.file);
}
}
}
}
}
for (const referencingFilePath of referencingFilePaths) {
// Is it a reference to a packlet?
if (Path.isUnder(referencingFilePath, packletsFolderPath)) {
const referencingRelativePath: string = Path.relative(packletsFolderPath, referencingFilePath);
const referencingPathParts: string[] = referencingRelativePath.split(/[\/\\]+/);
const referencingPackletName: string = referencingPathParts[0];
// Did we return to where we started from?
if (referencingPackletName === startingPackletName) {
// Ignore the degenerate case where the starting node imports itself,
// since @rushstack/packlets/mechanics will already report that.
if (previousNode) {
// Make a new linked list node to record this step of the traversal
const importListNode: IImportListNode = {
previousNode: previousNode,
fromFilePath: referencingFilePath,
packletName: packletName
};
// The traversal has returned to the packlet that we started from;
// this means we have detected a circular dependency
return importListNode;
}
}
// Have we already analyzed this packlet?
if (!visitedPacklets.has(referencingPackletName)) {
// Make a new linked list node to record this step of the traversal
const importListNode: IImportListNode = {
previousNode: previousNode,
fromFilePath: referencingFilePath,
packletName: packletName
};
const result: IImportListNode | undefined = DependencyAnalyzer._walkImports(
referencingPackletName,
startingPackletName,
refFileMap,
fileIncludeReasonsMap,
program,
packletsFolderPath,
visitedPacklets,
importListNode
);
if (result) {
return result;
}
}
}
}
return undefined;
}
/**
* For the specified packlet, trace all modules that import it, looking for a circular dependency
* between packlets. If found, an array is returned describing the import statements that cause
* the problem.
*
* @remarks
* For example, suppose we have files like this:
*
* ```
* src/packlets/logging/index.ts
* src/packlets/logging/Logger.ts --> imports "../data-model"
* src/packlets/data-model/index.ts
* src/packlets/data-model/DataModel.ts --> imports "../logging"
* ```
*
* The returned array would be:
* ```ts
* [
* { packletName: "logging", fromFilePath: "/path/to/src/packlets/data-model/DataModel.ts" },
* { packletName: "data-model", fromFilePath: "/path/to/src/packlets/logging/Logger.ts" },
* ]
* ```
*
* If there is more than one circular dependency chain, only the first one that is encountered
* will be returned.
*/
public static checkEntryPointForCircularImport(
packletName: string,
packletAnalyzer: PackletAnalyzer,
program: ts.Program
): IPackletImport[] | undefined {
const programInternals: ITsProgramInternals = program;
let refFileMap: Map<string, IRefFile[]> | undefined;
let fileIncludeReasonsMap: Map<string, IFileIncludeReason[]> | undefined;
if (programInternals.getRefFileMap) {
// TypeScript version range: >= 3.6.0, <= 4.2.0
refFileMap = programInternals.getRefFileMap();
} else if (programInternals.getFileIncludeReasons) {
// Typescript version range: > 4.2.0
fileIncludeReasonsMap = programInternals.getFileIncludeReasons();
} else {
// If you encounter this error, please report a bug
throw new Error(
'Your TypeScript compiler version is not supported; please upgrade @rushstack/eslint-plugin-packlets' +
' or report a GitHub issue'
);
}
const visitedPacklets: Set<string> = new Set();
const listNode: IImportListNode | undefined = DependencyAnalyzer._walkImports(
packletName,
packletName,
refFileMap,
fileIncludeReasonsMap,
program,
packletAnalyzer.packletsFolderPath!,
visitedPacklets,
undefined // previousNode
);
if (listNode) {
// Convert the linked list to an array
const packletImports: IPackletImport[] = [];
for (let current: IImportListNode | undefined = listNode; current; current = current.previousNode) {
packletImports.push({ fromFilePath: current.fromFilePath, packletName: current.packletName });
}
return packletImports;
}
return undefined;
}
}