@angular-modernizer/api 0.1.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/LICENSE +21 -0
- package/README.md +507 -0
- package/dist/analysis/complexity-metrics-calculator.d.ts +99 -0
- package/dist/analysis/complexity-metrics-calculator.d.ts.map +1 -0
- package/dist/analysis/complexity-metrics-calculator.js +195 -0
- package/dist/analysis/complexity-metrics-calculator.js.map +1 -0
- package/dist/analysis/dependency-analyzer.d.ts +227 -0
- package/dist/analysis/dependency-analyzer.d.ts.map +1 -0
- package/dist/analysis/dependency-analyzer.js +451 -0
- package/dist/analysis/dependency-analyzer.js.map +1 -0
- package/dist/analysis/external-template-loader.d.ts +44 -0
- package/dist/analysis/external-template-loader.d.ts.map +1 -0
- package/dist/analysis/external-template-loader.js +69 -0
- package/dist/analysis/external-template-loader.js.map +1 -0
- package/dist/analysis/selector-mapper.d.ts +77 -0
- package/dist/analysis/selector-mapper.d.ts.map +1 -0
- package/dist/analysis/selector-mapper.js +146 -0
- package/dist/analysis/selector-mapper.js.map +1 -0
- package/dist/analysis/service-detector.d.ts +164 -0
- package/dist/analysis/service-detector.d.ts.map +1 -0
- package/dist/analysis/service-detector.js +231 -0
- package/dist/analysis/service-detector.js.map +1 -0
- package/dist/analysis/service-pattern-recognizer.d.ts +247 -0
- package/dist/analysis/service-pattern-recognizer.d.ts.map +1 -0
- package/dist/analysis/service-pattern-recognizer.js +444 -0
- package/dist/analysis/service-pattern-recognizer.js.map +1 -0
- package/dist/analysis/symbol-locator.d.ts +81 -0
- package/dist/analysis/symbol-locator.d.ts.map +1 -0
- package/dist/analysis/symbol-locator.js +128 -0
- package/dist/analysis/symbol-locator.js.map +1 -0
- package/dist/analysis/template-analyzer-usage.d.ts +144 -0
- package/dist/analysis/template-analyzer-usage.d.ts.map +1 -0
- package/dist/analysis/template-analyzer-usage.js +474 -0
- package/dist/analysis/template-analyzer-usage.js.map +1 -0
- package/dist/analysis/template-analyzer.d.ts +80 -0
- package/dist/analysis/template-analyzer.d.ts.map +1 -0
- package/dist/analysis/template-analyzer.js +146 -0
- package/dist/analysis/template-analyzer.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/investigation/angular-symbol-finder.d.ts +76 -0
- package/dist/investigation/angular-symbol-finder.d.ts.map +1 -0
- package/dist/investigation/angular-symbol-finder.js +199 -0
- package/dist/investigation/angular-symbol-finder.js.map +1 -0
- package/dist/investigation/call-graph-builder.d.ts +85 -0
- package/dist/investigation/call-graph-builder.d.ts.map +1 -0
- package/dist/investigation/call-graph-builder.js +196 -0
- package/dist/investigation/call-graph-builder.js.map +1 -0
- package/dist/investigation/codebase-searcher.d.ts +99 -0
- package/dist/investigation/codebase-searcher.d.ts.map +1 -0
- package/dist/investigation/codebase-searcher.js +201 -0
- package/dist/investigation/codebase-searcher.js.map +1 -0
- package/dist/investigation/stack-trace-parser.d.ts +70 -0
- package/dist/investigation/stack-trace-parser.d.ts.map +1 -0
- package/dist/investigation/stack-trace-parser.js +117 -0
- package/dist/investigation/stack-trace-parser.js.map +1 -0
- package/dist/investigation/type-resolver.d.ts +59 -0
- package/dist/investigation/type-resolver.d.ts.map +1 -0
- package/dist/investigation/type-resolver.js +111 -0
- package/dist/investigation/type-resolver.js.map +1 -0
- package/dist/investigation/usage-finder.d.ts +69 -0
- package/dist/investigation/usage-finder.d.ts.map +1 -0
- package/dist/investigation/usage-finder.js +132 -0
- package/dist/investigation/usage-finder.js.map +1 -0
- package/dist/metrics/complexity-metrics.d.ts +196 -0
- package/dist/metrics/complexity-metrics.d.ts.map +1 -0
- package/dist/metrics/complexity-metrics.js +445 -0
- package/dist/metrics/complexity-metrics.js.map +1 -0
- package/dist/public-api.d.ts +77 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +50 -0
- package/dist/public-api.js.map +1 -0
- package/dist/transformation/import-manager.d.ts +276 -0
- package/dist/transformation/import-manager.d.ts.map +1 -0
- package/dist/transformation/import-manager.js +488 -0
- package/dist/transformation/import-manager.js.map +1 -0
- package/dist/transformation/ng-morph-adapter.d.ts +111 -0
- package/dist/transformation/ng-morph-adapter.d.ts.map +1 -0
- package/dist/transformation/ng-morph-adapter.js +179 -0
- package/dist/transformation/ng-morph-adapter.js.map +1 -0
- package/dist/transformation/ngmodule-manager.d.ts +145 -0
- package/dist/transformation/ngmodule-manager.d.ts.map +1 -0
- package/dist/transformation/ngmodule-manager.js +356 -0
- package/dist/transformation/ngmodule-manager.js.map +1 -0
- package/package.json +62 -0
- package/src/analysis/complexity-metrics-calculator.ts +253 -0
- package/src/analysis/dependency-analyzer.ts +720 -0
- package/src/analysis/external-template-loader.ts +79 -0
- package/src/analysis/selector-mapper.ts +186 -0
- package/src/analysis/service-detector.ts +280 -0
- package/src/analysis/service-pattern-recognizer.ts +545 -0
- package/src/analysis/symbol-locator.ts +170 -0
- package/src/analysis/template-analyzer-usage.ts +614 -0
- package/src/analysis/template-analyzer.ts +208 -0
- package/src/index.ts +51 -0
- package/src/investigation/angular-symbol-finder.ts +263 -0
- package/src/investigation/call-graph-builder.ts +298 -0
- package/src/investigation/codebase-searcher.ts +286 -0
- package/src/investigation/stack-trace-parser.ts +166 -0
- package/src/investigation/type-resolver.ts +144 -0
- package/src/investigation/usage-finder.ts +198 -0
- package/src/metrics/complexity-metrics.ts +617 -0
- package/src/public-api.ts +119 -0
- package/src/transformation/import-manager.ts +626 -0
- package/src/transformation/ng-morph-adapter.ts +252 -0
- package/src/transformation/ngmodule-manager.ts +450 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Dependency Analyzer
|
|
3
|
+
*
|
|
4
|
+
* The "All-Knowing Cartographer" - builds comprehensive symbol maps for Angular modules.
|
|
5
|
+
* Recursively walks NgModule imports to create a complete map of available symbols.
|
|
6
|
+
*
|
|
7
|
+
* Philosophy: "Trust the Map"
|
|
8
|
+
* - Builds the definitive map of what's available in a module's scope
|
|
9
|
+
* - Handles the critical edge case: `` on setters with aliases
|
|
10
|
+
* - Plugins use this map to make intelligent decisions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
SourceFile,
|
|
15
|
+
ClassDeclaration,
|
|
16
|
+
Decorator,
|
|
17
|
+
Project,
|
|
18
|
+
PropertyDeclaration,
|
|
19
|
+
SetAccessorDeclaration,
|
|
20
|
+
GetAccessorDeclaration,
|
|
21
|
+
} from 'ts-morph';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents a dependency between two artifacts.
|
|
25
|
+
*/
|
|
26
|
+
export interface Dependency {
|
|
27
|
+
/**
|
|
28
|
+
* Source file path.
|
|
29
|
+
*/
|
|
30
|
+
from: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Target file path.
|
|
34
|
+
*/
|
|
35
|
+
to: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Type of dependency.
|
|
39
|
+
*/
|
|
40
|
+
type: 'import' | 'component-usage' | 'directive-usage' | 'service-injection';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Symbol name being used.
|
|
44
|
+
*/
|
|
45
|
+
symbol?: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Additional metadata.
|
|
49
|
+
*/
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Represents a symbol in the Angular module system.
|
|
55
|
+
*/
|
|
56
|
+
export interface SymbolInfo {
|
|
57
|
+
/**
|
|
58
|
+
* Class name of the symbol.
|
|
59
|
+
*/
|
|
60
|
+
className: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Selector(s) for components/directives.
|
|
64
|
+
* For components: ['app-root']
|
|
65
|
+
* For directives: ['[myDirective]', 'myDirective']
|
|
66
|
+
*/
|
|
67
|
+
selectors: string[];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Input property names (including aliases).
|
|
71
|
+
* For ``('customValue') set value(...), this includes 'customValue'.
|
|
72
|
+
*/
|
|
73
|
+
inputs: string[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Output property names (including aliases).
|
|
77
|
+
*/
|
|
78
|
+
outputs: string[];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Type of Angular artifact.
|
|
82
|
+
*/
|
|
83
|
+
type: 'component' | 'directive' | 'pipe' | 'module';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Source file path.
|
|
87
|
+
*/
|
|
88
|
+
filePath: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Complete symbol map for a module's scope.
|
|
93
|
+
*/
|
|
94
|
+
export interface SymbolMap {
|
|
95
|
+
/**
|
|
96
|
+
* Symbols declared in this module.
|
|
97
|
+
*/
|
|
98
|
+
declared: Map<string, SymbolInfo>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Symbols imported into this module (from other modules).
|
|
102
|
+
*/
|
|
103
|
+
imported: Map<string, SymbolInfo>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Symbols exported by this module (available to consumers).
|
|
107
|
+
*/
|
|
108
|
+
exported: Map<string, SymbolInfo>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* All effectively available symbols in this module's scope.
|
|
112
|
+
* This is the union of declared + imported symbols.
|
|
113
|
+
*/
|
|
114
|
+
available: Map<string, SymbolInfo>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Dependency Analyzer - extracts dependency relationships and builds symbol maps.
|
|
119
|
+
*
|
|
120
|
+
* This is the "All-Knowing Cartographer" that creates comprehensive maps of
|
|
121
|
+
* what's available in an NgModule's scope.
|
|
122
|
+
*/
|
|
123
|
+
export class DependencyAnalyzer {
|
|
124
|
+
/**
|
|
125
|
+
* Analyze all dependencies from a source file.
|
|
126
|
+
* Extracts all named imports and creates dependency objects for each.
|
|
127
|
+
*
|
|
128
|
+
* @param sourceFile - The source file to analyze
|
|
129
|
+
* @returns Array of dependencies
|
|
130
|
+
*/
|
|
131
|
+
analyzeDependencies(sourceFile: SourceFile): Dependency[] {
|
|
132
|
+
const filePath = sourceFile.getFilePath();
|
|
133
|
+
|
|
134
|
+
return sourceFile.getImportDeclarations().flatMap((importDecl) => {
|
|
135
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
136
|
+
return importDecl.getNamedImports().map((namedImport) => ({
|
|
137
|
+
from: filePath,
|
|
138
|
+
to: moduleSpecifier,
|
|
139
|
+
type: 'import' as const,
|
|
140
|
+
symbol: namedImport.getName(),
|
|
141
|
+
}));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a file depends on another file.
|
|
147
|
+
*
|
|
148
|
+
* @param sourceFile - The source file
|
|
149
|
+
* @param targetPath - The target file path
|
|
150
|
+
* @returns True if dependency exists
|
|
151
|
+
*/
|
|
152
|
+
dependsOn(sourceFile: SourceFile, targetPath: string): boolean {
|
|
153
|
+
const dependencies = this.analyzeDependencies(sourceFile);
|
|
154
|
+
return dependencies.some((dep) => dep.to === targetPath);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build a comprehensive symbol map for an NgModule.
|
|
159
|
+
*
|
|
160
|
+
* This is the heart of the "Trust the Map" architecture.
|
|
161
|
+
* It creates the definitive map of what's available in a module's scope.
|
|
162
|
+
*
|
|
163
|
+
* @param moduleFile - The source file containing the NgModule
|
|
164
|
+
* @param project - Optional ts-morph Project to resolve symbols across files
|
|
165
|
+
* @returns Complete symbol map
|
|
166
|
+
*/
|
|
167
|
+
buildSymbolMap(moduleFile: SourceFile, project?: Project): SymbolMap {
|
|
168
|
+
const symbolMap: SymbolMap = {
|
|
169
|
+
declared: new Map(),
|
|
170
|
+
imported: new Map(),
|
|
171
|
+
exported: new Map(),
|
|
172
|
+
available: new Map(),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Find the NgModule class
|
|
176
|
+
const moduleClass = this.findNgModuleClass(moduleFile);
|
|
177
|
+
if (!moduleClass) {
|
|
178
|
+
return symbolMap;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Extract symbols from the @NgModule decorator
|
|
182
|
+
const decorator = this.findDecorator(moduleClass, 'NgModule');
|
|
183
|
+
if (!decorator) {
|
|
184
|
+
return symbolMap;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Parse the decorator to find declarations, imports, exports
|
|
188
|
+
const decoratorText = decorator.getText();
|
|
189
|
+
|
|
190
|
+
// Extract declared symbols
|
|
191
|
+
const declarations = this.extractArrayProperty(
|
|
192
|
+
decoratorText,
|
|
193
|
+
'declarations',
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
declarations.forEach((className) => {
|
|
197
|
+
let symbolInfo = this.extractSymbolInfo(moduleFile, className);
|
|
198
|
+
|
|
199
|
+
if (!symbolInfo && project) {
|
|
200
|
+
const foundFile = project
|
|
201
|
+
.getSourceFiles()
|
|
202
|
+
.find((sf) => this.extractSymbolInfo(sf, className));
|
|
203
|
+
symbolInfo = foundFile
|
|
204
|
+
? this.extractSymbolInfo(foundFile, className)
|
|
205
|
+
: undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (symbolInfo) {
|
|
209
|
+
symbolMap.declared.set(className, symbolInfo);
|
|
210
|
+
symbolMap.available.set(className, symbolInfo);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Extract imported symbols (from other modules)
|
|
215
|
+
const imports = this.extractArrayProperty(decoratorText, 'imports');
|
|
216
|
+
const knownModules = new Set(['CommonModule', 'FormsModule']);
|
|
217
|
+
|
|
218
|
+
imports.forEach((moduleName) => {
|
|
219
|
+
if (knownModules.has(moduleName)) {
|
|
220
|
+
const builtInSymbol: SymbolInfo = {
|
|
221
|
+
className: moduleName,
|
|
222
|
+
selectors: [],
|
|
223
|
+
inputs: [],
|
|
224
|
+
outputs: [],
|
|
225
|
+
type: 'module',
|
|
226
|
+
filePath: '@angular/common',
|
|
227
|
+
};
|
|
228
|
+
symbolMap.imported.set(moduleName, builtInSymbol);
|
|
229
|
+
symbolMap.available.set(moduleName, builtInSymbol);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (project && !knownModules.has(moduleName)) {
|
|
233
|
+
const importedModuleSymbols = this.loadModuleExports(
|
|
234
|
+
moduleName,
|
|
235
|
+
project,
|
|
236
|
+
moduleFile,
|
|
237
|
+
);
|
|
238
|
+
importedModuleSymbols.forEach((symbolInfo, symbolName) => {
|
|
239
|
+
symbolMap.imported.set(symbolName, symbolInfo);
|
|
240
|
+
symbolMap.available.set(symbolName, symbolInfo);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Extract exported symbols
|
|
246
|
+
const exports = this.extractArrayProperty(decoratorText, 'exports');
|
|
247
|
+
exports.forEach((className) => {
|
|
248
|
+
const symbolInfo =
|
|
249
|
+
symbolMap.declared.get(className) ?? symbolMap.imported.get(className);
|
|
250
|
+
if (symbolInfo) {
|
|
251
|
+
symbolMap.exported.set(className, symbolInfo);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return symbolMap;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Load exported symbols from an imported module.
|
|
260
|
+
* Recursively resolves module imports to build complete symbol map.
|
|
261
|
+
*
|
|
262
|
+
* @param moduleName - The module class name to load
|
|
263
|
+
* @param project - The ts-morph project
|
|
264
|
+
* @param _currentFile - The current source file (reserved for future use in resolving relative imports)
|
|
265
|
+
* @returns Map of exported symbols
|
|
266
|
+
*/
|
|
267
|
+
private loadModuleExports(
|
|
268
|
+
moduleName: string,
|
|
269
|
+
project: Project,
|
|
270
|
+
_currentFile: SourceFile,
|
|
271
|
+
): Map<string, SymbolInfo> {
|
|
272
|
+
const exports = new Map<string, SymbolInfo>();
|
|
273
|
+
const allSourceFiles = project.getSourceFiles();
|
|
274
|
+
|
|
275
|
+
// Find the module class across all source files
|
|
276
|
+
const moduleResult = allSourceFiles
|
|
277
|
+
.map((sf) => ({ file: sf, class: sf.getClass(moduleName) }))
|
|
278
|
+
.find(({ class: cls }) => cls && this.findDecorator(cls, 'NgModule'));
|
|
279
|
+
|
|
280
|
+
if (!moduleResult?.class) {
|
|
281
|
+
return exports;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const { file: moduleFile, class: moduleClass } = moduleResult;
|
|
285
|
+
|
|
286
|
+
// Get the @NgModule decorator
|
|
287
|
+
const decorator = this.findDecorator(moduleClass, 'NgModule');
|
|
288
|
+
if (!decorator) {
|
|
289
|
+
return exports;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Extract exported symbols from the module
|
|
293
|
+
const exportedSymbols = this.extractArrayProperty(
|
|
294
|
+
decorator.getText(),
|
|
295
|
+
'exports',
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
exportedSymbols.forEach((symbolName) => {
|
|
299
|
+
let symbolInfo = this.extractSymbolInfo(moduleFile, symbolName);
|
|
300
|
+
|
|
301
|
+
if (!symbolInfo) {
|
|
302
|
+
const foundFile = allSourceFiles.find((sf) =>
|
|
303
|
+
this.extractSymbolInfo(sf, symbolName),
|
|
304
|
+
);
|
|
305
|
+
symbolInfo = foundFile
|
|
306
|
+
? this.extractSymbolInfo(foundFile, symbolName)
|
|
307
|
+
: undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (symbolInfo) {
|
|
311
|
+
exports.set(symbolName, symbolInfo);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return exports;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get all transitive dependencies for a source file.
|
|
320
|
+
* Follows dependency chain recursively.
|
|
321
|
+
*
|
|
322
|
+
* @param sourceFile - The source file
|
|
323
|
+
* @param project - The ts-morph project
|
|
324
|
+
* @param visited - Set of already visited files (for cycle detection)
|
|
325
|
+
* @returns Array of all transitive dependencies
|
|
326
|
+
*/
|
|
327
|
+
getTransitiveDependencies(
|
|
328
|
+
sourceFile: SourceFile,
|
|
329
|
+
project: Project,
|
|
330
|
+
visited = new Set<string>(),
|
|
331
|
+
): Dependency[] {
|
|
332
|
+
const filePath = sourceFile.getFilePath();
|
|
333
|
+
|
|
334
|
+
// Prevent infinite recursion
|
|
335
|
+
if (visited.has(filePath)) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
visited.add(filePath);
|
|
339
|
+
|
|
340
|
+
const directDependencies = this.analyzeDependencies(sourceFile);
|
|
341
|
+
const localDeps = directDependencies.filter(
|
|
342
|
+
(dep) => dep.to.startsWith('.') || dep.to.startsWith('/'),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const transitiveDeps = localDeps.flatMap((dep) => {
|
|
346
|
+
const depPath = dep.to.replace(/^\.\//, '').replace(/\.ts$/, '');
|
|
347
|
+
const depSourceFile = project.getSourceFiles().find((sf) => {
|
|
348
|
+
const sfPath = sf.getFilePath();
|
|
349
|
+
return (
|
|
350
|
+
sfPath.includes(depPath + '.ts') ||
|
|
351
|
+
sfPath.endsWith('/' + depPath + '.ts')
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return depSourceFile
|
|
356
|
+
? this.getTransitiveDependencies(depSourceFile, project, visited)
|
|
357
|
+
: [];
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return [...directDependencies, ...transitiveDeps];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Detect circular dependencies in a project.
|
|
365
|
+
*
|
|
366
|
+
* @param project - The ts-morph project
|
|
367
|
+
* @returns Array of circular dependency chains
|
|
368
|
+
*/
|
|
369
|
+
detectCircularDependencies(
|
|
370
|
+
project: Project,
|
|
371
|
+
): { cycle: string[]; severity: 'warning' | 'error' }[] {
|
|
372
|
+
const cycles: { cycle: string[]; severity: 'warning' | 'error' }[] = [];
|
|
373
|
+
const sourceFiles = project.getSourceFiles();
|
|
374
|
+
|
|
375
|
+
for (const sourceFile of sourceFiles) {
|
|
376
|
+
const filePath = sourceFile.getFilePath();
|
|
377
|
+
const visited = new Set<string>();
|
|
378
|
+
const path: string[] = [];
|
|
379
|
+
|
|
380
|
+
this.detectCyclesFromFile(
|
|
381
|
+
sourceFile,
|
|
382
|
+
project,
|
|
383
|
+
visited,
|
|
384
|
+
path,
|
|
385
|
+
cycles,
|
|
386
|
+
filePath,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Deduplicate cycles (same cycle can be found from different starting points)
|
|
391
|
+
return this.deduplicateCycles(cycles);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Detect cycles starting from a specific file.
|
|
396
|
+
* Recursively traverses local dependencies to find circular import chains.
|
|
397
|
+
* Severity based on cycle length: >3 files = error, ≤3 files = warning.
|
|
398
|
+
*/
|
|
399
|
+
private detectCyclesFromFile(
|
|
400
|
+
sourceFile: SourceFile,
|
|
401
|
+
project: Project,
|
|
402
|
+
visited: Set<string>,
|
|
403
|
+
path: string[],
|
|
404
|
+
cycles: { cycle: string[]; severity: 'warning' | 'error' }[],
|
|
405
|
+
targetFile: string,
|
|
406
|
+
): void {
|
|
407
|
+
const filePath = sourceFile.getFilePath();
|
|
408
|
+
|
|
409
|
+
// If we've returned to the target file, we found a cycle
|
|
410
|
+
if (path.length > 0 && filePath === targetFile) {
|
|
411
|
+
cycles.push({
|
|
412
|
+
cycle: [...path, filePath],
|
|
413
|
+
severity: path.length > 3 ? 'error' : 'warning',
|
|
414
|
+
});
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Prevent infinite recursion on already visited paths
|
|
419
|
+
if (visited.has(filePath)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
visited.add(filePath);
|
|
423
|
+
path.push(filePath);
|
|
424
|
+
|
|
425
|
+
// Follow dependencies - filter to local project files only (not node_modules)
|
|
426
|
+
const localDeps = this.analyzeDependencies(sourceFile).filter(
|
|
427
|
+
(dep) => dep.to.startsWith('.') || dep.to.startsWith('/'),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
localDeps.forEach((dep) => {
|
|
431
|
+
// Normalize import path: strip leading ./ and .ts extension
|
|
432
|
+
const depPath = dep.to.replace(/^\.\//, '').replace(/\.ts$/, '');
|
|
433
|
+
const depSourceFile = project.getSourceFiles().find((sf) => {
|
|
434
|
+
const sfPath = sf.getFilePath();
|
|
435
|
+
// Handle both relative imports with and without .ts extension
|
|
436
|
+
return (
|
|
437
|
+
sfPath.includes(depPath + '.ts') ||
|
|
438
|
+
sfPath.endsWith('/' + depPath + '.ts')
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (depSourceFile) {
|
|
443
|
+
// Recurse with fresh visited set and path copy to track independent branches
|
|
444
|
+
this.detectCyclesFromFile(
|
|
445
|
+
depSourceFile,
|
|
446
|
+
project,
|
|
447
|
+
new Set(visited),
|
|
448
|
+
[...path],
|
|
449
|
+
cycles,
|
|
450
|
+
targetFile,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Deduplicate cycle arrays (same cycle in different order).
|
|
458
|
+
* Example: [A→B→C→A] and [B→C→A→B] represent the same cycle.
|
|
459
|
+
* Uses sorted paths to create unique signatures for comparison.
|
|
460
|
+
*/
|
|
461
|
+
private deduplicateCycles(
|
|
462
|
+
cycles: { cycle: string[]; severity: 'warning' | 'error' }[],
|
|
463
|
+
): { cycle: string[]; severity: 'warning' | 'error' }[] {
|
|
464
|
+
const seen = new Set<string>();
|
|
465
|
+
|
|
466
|
+
return cycles.filter((item) => {
|
|
467
|
+
// Normalize cycle by sorting paths and creating unique signature
|
|
468
|
+
const sorted = [...item.cycle].sort();
|
|
469
|
+
const signature = sorted.join('->');
|
|
470
|
+
|
|
471
|
+
if (seen.has(signature)) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
seen.add(signature);
|
|
475
|
+
return true;
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Extract comprehensive symbol information from a class.
|
|
481
|
+
* Identifies Angular artifact types (``, ``, ``) and extracts metadata.
|
|
482
|
+
*
|
|
483
|
+
* CRITICAL: Handles the edge case of `` on setters with aliases.
|
|
484
|
+
*
|
|
485
|
+
* @param sourceFile - The source file
|
|
486
|
+
* @param className - The class name to analyze
|
|
487
|
+
* @returns Symbol information or undefined
|
|
488
|
+
*/
|
|
489
|
+
extractSymbolInfo(
|
|
490
|
+
sourceFile: SourceFile,
|
|
491
|
+
className: string,
|
|
492
|
+
): SymbolInfo | undefined {
|
|
493
|
+
const classDecl = sourceFile.getClass(className);
|
|
494
|
+
if (!classDecl) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const filePath = sourceFile.getFilePath();
|
|
499
|
+
|
|
500
|
+
// Map decorator types to extraction methods
|
|
501
|
+
const decoratorHandlers: {
|
|
502
|
+
name: string;
|
|
503
|
+
extract: (
|
|
504
|
+
cls: ClassDeclaration,
|
|
505
|
+
dec: Decorator,
|
|
506
|
+
path: string,
|
|
507
|
+
) => SymbolInfo;
|
|
508
|
+
}[] = [
|
|
509
|
+
{ name: 'Component', extract: this.extractComponentInfo.bind(this) },
|
|
510
|
+
{ name: 'Directive', extract: this.extractDirectiveInfo.bind(this) },
|
|
511
|
+
{ name: 'Pipe', extract: this.extractPipeInfo.bind(this) },
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
for (const { name, extract } of decoratorHandlers) {
|
|
515
|
+
const decorator = this.findDecorator(classDecl, name);
|
|
516
|
+
if (decorator) {
|
|
517
|
+
return extract(classDecl, decorator, filePath);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Extract component information including selectors and inputs.
|
|
526
|
+
*/
|
|
527
|
+
private extractComponentInfo(
|
|
528
|
+
classDecl: ClassDeclaration,
|
|
529
|
+
decorator: Decorator,
|
|
530
|
+
filePath: string,
|
|
531
|
+
): SymbolInfo {
|
|
532
|
+
const decoratorText = decorator.getText();
|
|
533
|
+
const selector = this.extractStringProperty(decoratorText, 'selector');
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
className: classDecl.getName() ?? 'UnknownComponent',
|
|
537
|
+
selectors: selector ? [selector] : [],
|
|
538
|
+
inputs: this.extractInputs(classDecl),
|
|
539
|
+
outputs: this.extractOutputs(classDecl),
|
|
540
|
+
type: 'component',
|
|
541
|
+
filePath,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Extract directive information including selectors and inputs.
|
|
547
|
+
*/
|
|
548
|
+
private extractDirectiveInfo(
|
|
549
|
+
classDecl: ClassDeclaration,
|
|
550
|
+
decorator: Decorator,
|
|
551
|
+
filePath: string,
|
|
552
|
+
): SymbolInfo {
|
|
553
|
+
const decoratorText = decorator.getText();
|
|
554
|
+
const selector = this.extractStringProperty(decoratorText, 'selector');
|
|
555
|
+
|
|
556
|
+
// For attribute selectors like '[myDirective]', we want both forms
|
|
557
|
+
const selectors: string[] = [];
|
|
558
|
+
if (selector) {
|
|
559
|
+
selectors.push(selector);
|
|
560
|
+
// If it's an attribute selector, also add the bare name
|
|
561
|
+
const bareSelector = selector.replace(/^\[|\]$/g, '');
|
|
562
|
+
if (bareSelector !== selector) {
|
|
563
|
+
selectors.push(bareSelector);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
className: classDecl.getName() ?? 'UnknownDirective',
|
|
569
|
+
selectors,
|
|
570
|
+
inputs: this.extractInputs(classDecl),
|
|
571
|
+
outputs: this.extractOutputs(classDecl),
|
|
572
|
+
type: 'directive',
|
|
573
|
+
filePath,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Extract pipe information.
|
|
579
|
+
*/
|
|
580
|
+
private extractPipeInfo(
|
|
581
|
+
classDecl: ClassDeclaration,
|
|
582
|
+
decorator: Decorator,
|
|
583
|
+
filePath: string,
|
|
584
|
+
): SymbolInfo {
|
|
585
|
+
const decoratorText = decorator.getText();
|
|
586
|
+
const name = this.extractStringProperty(decoratorText, 'name');
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
className: classDecl.getName() ?? 'UnknownPipe',
|
|
590
|
+
selectors: name ? [name] : [],
|
|
591
|
+
inputs: [],
|
|
592
|
+
outputs: [],
|
|
593
|
+
type: 'pipe',
|
|
594
|
+
filePath,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Extract all `` properties from a class.
|
|
600
|
+
*
|
|
601
|
+
* CRITICAL: Handles the edge case of `` on setters with aliases.
|
|
602
|
+
* Example: ``('customValue') set value(val: any) { ... }
|
|
603
|
+
* This should extract 'customValue', not 'value'.
|
|
604
|
+
*/
|
|
605
|
+
private extractInputs(classDecl: ClassDeclaration): string[] {
|
|
606
|
+
// Helper to extract decorated names with optional aliases
|
|
607
|
+
const extractDecorated = (
|
|
608
|
+
nodes: (
|
|
609
|
+
| PropertyDeclaration
|
|
610
|
+
| SetAccessorDeclaration
|
|
611
|
+
| GetAccessorDeclaration
|
|
612
|
+
)[],
|
|
613
|
+
decoratorName: string,
|
|
614
|
+
): string[] => {
|
|
615
|
+
const results: string[] = [];
|
|
616
|
+
nodes.forEach((node) => {
|
|
617
|
+
const decorator = this.findDecorator(node, decoratorName);
|
|
618
|
+
if (decorator) {
|
|
619
|
+
const alias = this.extractDecoratorAlias(decorator);
|
|
620
|
+
results.push(alias ?? node.getName());
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
return results;
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// Extract from properties, setters (CRITICAL for fixture test!), and getters
|
|
627
|
+
return [
|
|
628
|
+
...extractDecorated(classDecl.getProperties(), 'Input'),
|
|
629
|
+
...extractDecorated(classDecl.getSetAccessors(), 'Input'),
|
|
630
|
+
...extractDecorated(classDecl.getGetAccessors(), 'Input'),
|
|
631
|
+
];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Extract all `` properties from a class.
|
|
636
|
+
*/
|
|
637
|
+
private extractOutputs(classDecl: ClassDeclaration): string[] {
|
|
638
|
+
const outputs: string[] = [];
|
|
639
|
+
|
|
640
|
+
classDecl.getProperties().forEach((prop) => {
|
|
641
|
+
const outputDecorator = this.findDecorator(prop, 'Output');
|
|
642
|
+
if (outputDecorator) {
|
|
643
|
+
const alias = this.extractDecoratorAlias(outputDecorator);
|
|
644
|
+
outputs.push(alias ?? prop.getName());
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
return outputs;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Extract alias from a decorator.
|
|
653
|
+
* Example: ``('customValue') -> 'customValue'
|
|
654
|
+
*/
|
|
655
|
+
private extractDecoratorAlias(decorator: Decorator): string | undefined {
|
|
656
|
+
const args = decorator.getArguments();
|
|
657
|
+
const firstArg = args[0];
|
|
658
|
+
|
|
659
|
+
// Remove quotes from string literal if argument exists
|
|
660
|
+
return firstArg?.getText().replace(/^['"`]|['"`]$/g, '');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Find a decorator on a node by name.
|
|
665
|
+
*/
|
|
666
|
+
private findDecorator(
|
|
667
|
+
node:
|
|
668
|
+
| ClassDeclaration
|
|
669
|
+
| PropertyDeclaration
|
|
670
|
+
| SetAccessorDeclaration
|
|
671
|
+
| GetAccessorDeclaration,
|
|
672
|
+
name: string,
|
|
673
|
+
): Decorator | undefined {
|
|
674
|
+
const decorators = node.getDecorators();
|
|
675
|
+
return decorators.find((d: Decorator) => d.getName() === name);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Find the NgModule class in a source file.
|
|
680
|
+
*/
|
|
681
|
+
private findNgModuleClass(
|
|
682
|
+
sourceFile: SourceFile,
|
|
683
|
+
): ClassDeclaration | undefined {
|
|
684
|
+
const classes = sourceFile.getClasses();
|
|
685
|
+
return classes.find(
|
|
686
|
+
(cls) => this.findDecorator(cls, 'NgModule') !== undefined,
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Extract a string property value from decorator text.
|
|
692
|
+
* Example: selector: 'app-root' -> 'app-root'
|
|
693
|
+
*/
|
|
694
|
+
private extractStringProperty(
|
|
695
|
+
text: string,
|
|
696
|
+
propertyName: string,
|
|
697
|
+
): string | undefined {
|
|
698
|
+
const regex = new RegExp(`${propertyName}\\s*:\\s*['"\`]([^'"\`]+)['"\`]`);
|
|
699
|
+
const match = text.match(regex);
|
|
700
|
+
return match ? match[1] : undefined;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Extract an array property value from decorator text.
|
|
705
|
+
* Example: declarations: [Foo, Bar] -> ['Foo', 'Bar']
|
|
706
|
+
*/
|
|
707
|
+
private extractArrayProperty(text: string, propertyName: string): string[] {
|
|
708
|
+
const regex = new RegExp(`${propertyName}\\s*:\\s*\\[([^\\]]*)\\]`);
|
|
709
|
+
const match = text.match(regex);
|
|
710
|
+
if (!match?.[1]) {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const arrayContent = match[1];
|
|
715
|
+
return arrayContent
|
|
716
|
+
.split(',')
|
|
717
|
+
.map((item) => item.trim())
|
|
718
|
+
.filter((item) => item.length > 0);
|
|
719
|
+
}
|
|
720
|
+
}
|