ng-di-graph 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.
@@ -0,0 +1,1907 @@
1
+ #!/usr/bin/env node
2
+ let commander = require("commander");
3
+ let node_fs = require("node:fs");
4
+ let node_path = require("node:path");
5
+ let ts_morph = require("ts-morph");
6
+
7
+ //#region src/core/error-handler.ts
8
+ /**
9
+ * Comprehensive error handling infrastructure for ng-di-graph CLI
10
+ * Implements FR-10 requirements from PRD Section 13
11
+ */
12
+ /**
13
+ * Exit codes for ng-di-graph CLI
14
+ * Implements PRD Section 13 error handling requirements
15
+ */
16
+ let ExitCodes = /* @__PURE__ */ function(ExitCodes$1) {
17
+ ExitCodes$1[ExitCodes$1["SUCCESS"] = 0] = "SUCCESS";
18
+ ExitCodes$1[ExitCodes$1["GENERAL_ERROR"] = 1] = "GENERAL_ERROR";
19
+ ExitCodes$1[ExitCodes$1["INVALID_ARGUMENTS"] = 2] = "INVALID_ARGUMENTS";
20
+ ExitCodes$1[ExitCodes$1["TSCONFIG_ERROR"] = 3] = "TSCONFIG_ERROR";
21
+ ExitCodes$1[ExitCodes$1["PARSING_ERROR"] = 4] = "PARSING_ERROR";
22
+ ExitCodes$1[ExitCodes$1["TYPE_RESOLUTION_ERROR"] = 5] = "TYPE_RESOLUTION_ERROR";
23
+ ExitCodes$1[ExitCodes$1["MEMORY_ERROR"] = 6] = "MEMORY_ERROR";
24
+ ExitCodes$1[ExitCodes$1["FILE_NOT_FOUND"] = 7] = "FILE_NOT_FOUND";
25
+ ExitCodes$1[ExitCodes$1["PERMISSION_ERROR"] = 8] = "PERMISSION_ERROR";
26
+ return ExitCodes$1;
27
+ }({});
28
+ /**
29
+ * Custom error class for ng-di-graph CLI
30
+ * Provides structured error information with context
31
+ */
32
+ var CliError = class CliError extends Error {
33
+ constructor(message, code, filePath, context) {
34
+ super(message);
35
+ this.code = code;
36
+ this.filePath = filePath;
37
+ this.context = context;
38
+ this.name = "CliError";
39
+ Object.setPrototypeOf(this, CliError.prototype);
40
+ }
41
+ /**
42
+ * Check if error is fatal (should exit immediately)
43
+ */
44
+ isFatal() {
45
+ return [
46
+ "TSCONFIG_NOT_FOUND",
47
+ "TSCONFIG_INVALID",
48
+ "PROJECT_LOAD_FAILED",
49
+ "MEMORY_LIMIT_EXCEEDED",
50
+ "INTERNAL_ERROR",
51
+ "INVALID_ARGUMENTS",
52
+ "PERMISSION_DENIED",
53
+ "COMPILATION_ERROR"
54
+ ].includes(this.code);
55
+ }
56
+ /**
57
+ * Check if error is recoverable (can continue processing)
58
+ */
59
+ isRecoverable() {
60
+ return !this.isFatal();
61
+ }
62
+ };
63
+ /**
64
+ * ErrorHandler - Static utility class for error handling
65
+ * Provides centralized error formatting and exit code management
66
+ */
67
+ var ErrorHandler = class ErrorHandler {
68
+ /**
69
+ * Map error code to exit code
70
+ * @param error CliError instance or null
71
+ * @returns Exit code for process.exit()
72
+ */
73
+ static classifyExitCode(error) {
74
+ if (!error) return ExitCodes.SUCCESS;
75
+ switch (error.code) {
76
+ case "TSCONFIG_NOT_FOUND":
77
+ case "TSCONFIG_INVALID":
78
+ case "PROJECT_LOAD_FAILED": return ExitCodes.TSCONFIG_ERROR;
79
+ case "FILE_PARSE_ERROR":
80
+ case "COMPILATION_ERROR": return ExitCodes.PARSING_ERROR;
81
+ case "TYPE_RESOLUTION_ERROR":
82
+ case "DEPENDENCY_NOT_FOUND": return ExitCodes.TYPE_RESOLUTION_ERROR;
83
+ case "MEMORY_LIMIT_EXCEEDED": return ExitCodes.MEMORY_ERROR;
84
+ case "FILE_NOT_FOUND": return ExitCodes.FILE_NOT_FOUND;
85
+ case "PERMISSION_DENIED": return ExitCodes.PERMISSION_ERROR;
86
+ case "INVALID_ARGUMENTS": return ExitCodes.INVALID_ARGUMENTS;
87
+ default: return ExitCodes.GENERAL_ERROR;
88
+ }
89
+ }
90
+ /**
91
+ * Format error for user-friendly display
92
+ * @param error CliError to format
93
+ * @param verbose Include stack traces and detailed info
94
+ * @returns Formatted error message string
95
+ */
96
+ static formatError(error, verbose = false) {
97
+ const lines = [];
98
+ if (error.isFatal()) lines.push("❌ Fatal Error");
99
+ else if (error.code === "TYPE_RESOLUTION_ERROR") lines.push("⚠️ Type Resolution Warning");
100
+ else lines.push("⚠️ Warning");
101
+ lines.push("");
102
+ lines.push(`Message: ${error.message}`);
103
+ if (error.filePath) lines.push(`File: ${error.filePath}`);
104
+ lines.push(`Code: ${error.code}`);
105
+ if (error.context && Object.keys(error.context).length > 0) {
106
+ lines.push("");
107
+ lines.push("Context:");
108
+ for (const [key, value] of Object.entries(error.context)) lines.push(` ${key}: ${JSON.stringify(value)}`);
109
+ }
110
+ const guidance = ErrorHandler.getRecoveryGuidance(error);
111
+ if (guidance) {
112
+ lines.push("");
113
+ lines.push("💡 Suggestions:");
114
+ for (const line of guidance.split("\n")) if (line.trim()) lines.push(` • ${line.trim()}`);
115
+ }
116
+ if (verbose && error.stack) {
117
+ lines.push("");
118
+ lines.push("🔍 Stack Trace:");
119
+ lines.push(error.stack);
120
+ }
121
+ lines.push("");
122
+ lines.push("Run with --help for usage information");
123
+ lines.push("Use --verbose for detailed debugging information");
124
+ return lines.join("\n");
125
+ }
126
+ /**
127
+ * Get actionable recovery guidance for error
128
+ * @param error CliError to provide guidance for
129
+ * @returns Multi-line guidance string
130
+ */
131
+ static getRecoveryGuidance(error) {
132
+ switch (error.code) {
133
+ case "TSCONFIG_NOT_FOUND": return `Check that the file path is correct
134
+ Ensure the tsconfig.json file exists
135
+ Try using an absolute path instead of relative
136
+ Use --project flag to specify correct path`;
137
+ case "TSCONFIG_INVALID": return `Validate JSON syntax with a JSON validator
138
+ Check TypeScript compiler options are valid
139
+ Ensure all referenced files exist
140
+ Try with a minimal tsconfig.json first`;
141
+ case "PROJECT_LOAD_FAILED": return `Check TypeScript compilation errors
142
+ Ensure all dependencies are installed
143
+ Verify import paths are correct
144
+ Try cleaning node_modules and reinstalling`;
145
+ case "FILE_PARSE_ERROR": return `Check TypeScript syntax in the problematic file
146
+ Ensure all imports are properly resolved
147
+ Try excluding the file from tsconfig if not needed
148
+ Use --verbose to see detailed parsing errors`;
149
+ case "TYPE_RESOLUTION_ERROR": return `Consider adding explicit type annotations
150
+ Check import statements are correct
151
+ Verify the dependency is properly exported
152
+ Use 'any' type as temporary workaround if needed`;
153
+ case "DEPENDENCY_NOT_FOUND": return `Check import statements in ${error.filePath || "the file"}
154
+ Verify all dependencies are properly installed
155
+ Ensure the dependency is exported from its module
156
+ Consider using --verbose for detailed analysis`;
157
+ case "MEMORY_LIMIT_EXCEEDED": return `Try processing smaller portions of the codebase
158
+ Use --entry filtering to limit scope
159
+ Consider increasing available memory (NODE_OPTIONS="--max-old-space-size=4096")
160
+ Process files in batches rather than all at once`;
161
+ case "ANONYMOUS_CLASS_SKIPPED": return `Consider giving the class a name for better tracking
162
+ This is a non-fatal warning - processing will continue
163
+ Anonymous classes cannot be included in dependency graphs`;
164
+ case "OUTPUT_WRITE_ERROR": return `Check file permissions for the output location
165
+ Ensure the output directory exists
166
+ Try writing to a different location
167
+ Use stdout instead of file output as workaround`;
168
+ case "PERMISSION_DENIED": return `Check file and directory permissions
169
+ Try running with appropriate user privileges
170
+ Ensure you have read access to source files
171
+ Verify write access to output location`;
172
+ case "COMPILATION_ERROR": return `Fix TypeScript compilation errors
173
+ Check compiler options in tsconfig.json
174
+ Ensure all type definitions are available
175
+ Try running tsc directly to see detailed errors`;
176
+ case "INVALID_ARGUMENTS": return `Check CLI argument syntax
177
+ Review --help for valid options
178
+ Ensure all required arguments are provided
179
+ Verify argument values are in correct format`;
180
+ case "FILE_NOT_FOUND": return `Verify file path is correct
181
+ Check file exists in the specified location
182
+ Ensure file permissions allow reading
183
+ Try using absolute path instead of relative`;
184
+ default: return `Review the error message for specific details
185
+ Try running with --verbose for more information
186
+ Check project configuration and dependencies
187
+ Consider filing an issue if the problem persists`;
188
+ }
189
+ }
190
+ /**
191
+ * Handle error and exit process (never returns)
192
+ * @param error CliError to handle
193
+ * @param verbose Include verbose error information
194
+ */
195
+ static handleError(error, verbose = false) {
196
+ const formattedError = ErrorHandler.formatError(error, verbose);
197
+ console.error(formattedError);
198
+ const exitCode = ErrorHandler.classifyExitCode(error);
199
+ process.exit(exitCode);
200
+ }
201
+ /**
202
+ * Factory method to create CliError
203
+ * @param message Error message
204
+ * @param code Error code
205
+ * @param filePath Optional file path
206
+ * @param context Optional context object
207
+ * @returns CliError instance
208
+ */
209
+ static createError(message, code, filePath, context) {
210
+ return new CliError(message, code, filePath, context);
211
+ }
212
+ /**
213
+ * Output warning without exiting
214
+ * @param message Warning message
215
+ * @param filePath Optional file path
216
+ */
217
+ static warn(message, filePath) {
218
+ console.warn(`⚠️ Warning: ${message}${filePath ? ` (${filePath})` : ""}`);
219
+ }
220
+ };
221
+
222
+ //#endregion
223
+ //#region src/core/logger.ts
224
+ /**
225
+ * Logger Infrastructure for ng-di-graph
226
+ * Provides structured logging with categories, performance timing, and memory tracking
227
+ * Implements FR-12: Verbose mode support
228
+ *
229
+ * Usage:
230
+ * ```typescript
231
+ * const logger = createLogger(verbose);
232
+ * logger?.info(LogCategory.FILE_PROCESSING, 'Processing file', { filePath: '/test.ts' });
233
+ * logger?.time('operation');
234
+ * // ... operation ...
235
+ * const elapsed = logger?.timeEnd('operation');
236
+ * const stats = logger?.getStats();
237
+ * ```
238
+ */
239
+ /**
240
+ * Log category enumeration for structured logging
241
+ * Each category represents a distinct phase of ng-di-graph processing
242
+ */
243
+ let LogCategory = /* @__PURE__ */ function(LogCategory$1) {
244
+ LogCategory$1["FILE_PROCESSING"] = "file-processing";
245
+ LogCategory$1["AST_ANALYSIS"] = "ast-analysis";
246
+ LogCategory$1["TYPE_RESOLUTION"] = "type-resolution";
247
+ LogCategory$1["GRAPH_CONSTRUCTION"] = "graph-construction";
248
+ LogCategory$1["FILTERING"] = "filtering";
249
+ LogCategory$1["ERROR_RECOVERY"] = "error-recovery";
250
+ LogCategory$1["PERFORMANCE"] = "performance";
251
+ return LogCategory$1;
252
+ }({});
253
+ /**
254
+ * Factory function for Logger creation
255
+ * Returns undefined when verbose is false (no-op pattern)
256
+ *
257
+ * @param verbose - Enable verbose logging
258
+ * @returns Logger instance or undefined
259
+ *
260
+ * @example
261
+ * ```typescript
262
+ * const logger = createLogger(cliOptions.verbose);
263
+ * logger?.info(LogCategory.FILE_PROCESSING, 'Starting processing');
264
+ * ```
265
+ */
266
+ function createLogger(verbose) {
267
+ return verbose ? new LoggerImpl() : void 0;
268
+ }
269
+ /**
270
+ * Logger implementation (internal)
271
+ * Provides structured logging with minimal performance overhead
272
+ */
273
+ var LoggerImpl = class {
274
+ constructor() {
275
+ this._timers = /* @__PURE__ */ new Map();
276
+ this._stats = {
277
+ totalLogs: 0,
278
+ categoryCounts: {},
279
+ performanceMetrics: {
280
+ totalTime: 0,
281
+ fileProcessingTime: 0,
282
+ graphBuildingTime: 0,
283
+ outputGenerationTime: 0
284
+ },
285
+ memoryUsage: {
286
+ peakUsage: 0,
287
+ currentUsage: 0
288
+ }
289
+ };
290
+ this._peakMemory = 0;
291
+ }
292
+ debug(category, message, context) {
293
+ this._log("DEBUG", category, message, context);
294
+ }
295
+ info(category, message, context) {
296
+ this._log("INFO", category, message, context);
297
+ }
298
+ warn(category, message, context) {
299
+ this._log("WARN", category, message, context);
300
+ }
301
+ error(category, message, context) {
302
+ this._log("ERROR", category, message, context);
303
+ }
304
+ time(label) {
305
+ this._timers.set(label, { startTime: performance.now() });
306
+ }
307
+ timeEnd(label) {
308
+ const timer = this._timers.get(label);
309
+ if (!timer) throw new Error(`Timer '${label}' does not exist`);
310
+ const elapsed = performance.now() - timer.startTime;
311
+ this._timers.delete(label);
312
+ return elapsed;
313
+ }
314
+ getStats() {
315
+ const memUsage = process.memoryUsage();
316
+ this._stats.memoryUsage.currentUsage = memUsage.heapUsed;
317
+ return { ...this._stats };
318
+ }
319
+ _log(level, category, message, context) {
320
+ this._stats.totalLogs++;
321
+ this._stats.categoryCounts[category] = (this._stats.categoryCounts[category] || 0) + 1;
322
+ const memUsage = process.memoryUsage().heapUsed;
323
+ if (memUsage > this._peakMemory) {
324
+ this._peakMemory = memUsage;
325
+ this._stats.memoryUsage.peakUsage = memUsage;
326
+ }
327
+ const formattedLog = this._formatLog(level, category, message, context);
328
+ console.error(formattedLog);
329
+ }
330
+ _formatLog(level, category, message, context) {
331
+ return `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] [${category}] ${message}${context ? ` ${JSON.stringify(context)}` : ""}`;
332
+ }
333
+ };
334
+
335
+ //#endregion
336
+ //#region src/core/graph-builder.ts
337
+ /**
338
+ * Validates input parameters for the buildGraph function
339
+ * @param parsedClasses Array to validate
340
+ * @throws Error if validation fails
341
+ */
342
+ function validateInput(parsedClasses) {
343
+ if (parsedClasses == null) throw new Error("parsedClasses parameter cannot be null or undefined");
344
+ for (let i = 0; i < parsedClasses.length; i++) {
345
+ const parsedClass = parsedClasses[i];
346
+ if (typeof parsedClass.name !== "string") throw new Error("ParsedClass must have a valid name property");
347
+ if (parsedClass.name.trim() === "") throw new Error("ParsedClass name cannot be empty");
348
+ if (typeof parsedClass.kind !== "string") throw new Error("ParsedClass must have a valid kind property");
349
+ if (parsedClass.dependencies == null) throw new Error("ParsedClass must have a dependencies array");
350
+ if (!Array.isArray(parsedClass.dependencies)) throw new Error("ParsedClass dependencies must be an array");
351
+ for (const dependency of parsedClass.dependencies) if (typeof dependency.token !== "string") throw new Error("ParsedDependency must have a valid token property");
352
+ }
353
+ }
354
+ /**
355
+ * Detects circular dependencies using DFS (Depth-First Search)
356
+ * @param edges Array of edges representing the dependency graph
357
+ * @param nodes Array of nodes in the graph
358
+ * @returns Array of circular dependency paths and edges marked as circular
359
+ */
360
+ function detectCircularDependencies(edges, nodes) {
361
+ const circularDependencies = [];
362
+ const circularEdges = /* @__PURE__ */ new Set();
363
+ const adjacencyList = /* @__PURE__ */ new Map();
364
+ for (const node of nodes) adjacencyList.set(node.id, []);
365
+ for (const edge of edges) {
366
+ if (!adjacencyList.has(edge.from)) adjacencyList.set(edge.from, []);
367
+ const neighbors = adjacencyList.get(edge.from);
368
+ if (neighbors) neighbors.push(edge.to);
369
+ }
370
+ const recursionStack = /* @__PURE__ */ new Set();
371
+ const processedNodes = /* @__PURE__ */ new Set();
372
+ /**
373
+ * DFS helper function to detect cycles
374
+ */
375
+ function dfs(node, path) {
376
+ if (processedNodes.has(node)) return;
377
+ if (recursionStack.has(node)) {
378
+ const cycleStartIndex = path.indexOf(node);
379
+ const cyclePath = [...path.slice(cycleStartIndex), node];
380
+ circularDependencies.push(cyclePath);
381
+ for (let i = 0; i < cyclePath.length - 1; i++) {
382
+ const edgeKey = `${cyclePath[i]}->${cyclePath[i + 1]}`;
383
+ circularEdges.add(edgeKey);
384
+ }
385
+ return;
386
+ }
387
+ recursionStack.add(node);
388
+ const newPath = [...path, node];
389
+ const neighbors = adjacencyList.get(node) || [];
390
+ for (const neighbor of neighbors) dfs(neighbor, newPath);
391
+ recursionStack.delete(node);
392
+ processedNodes.add(node);
393
+ }
394
+ for (const node of nodes) if (!processedNodes.has(node.id)) dfs(node.id, []);
395
+ return {
396
+ circularDependencies,
397
+ circularEdges
398
+ };
399
+ }
400
+ /**
401
+ * Builds a dependency graph from parsed Angular classes
402
+ * @param parsedClasses Array of parsed classes with their dependencies
403
+ * @param logger Optional Logger instance for verbose mode logging
404
+ * @returns Graph containing nodes and edges representing the dependency relationships
405
+ */
406
+ function buildGraph(parsedClasses, logger) {
407
+ logger?.time("buildGraph");
408
+ logger?.info(LogCategory.GRAPH_CONSTRUCTION, "Starting graph construction", { classCount: parsedClasses.length });
409
+ validateInput(parsedClasses);
410
+ const nodeMap = /* @__PURE__ */ new Map();
411
+ const edges = [];
412
+ for (const parsedClass of parsedClasses) if (!nodeMap.has(parsedClass.name)) nodeMap.set(parsedClass.name, {
413
+ id: parsedClass.name,
414
+ kind: parsedClass.kind
415
+ });
416
+ logger?.info(LogCategory.GRAPH_CONSTRUCTION, `Created ${nodeMap.size} nodes`, { nodeCount: nodeMap.size });
417
+ let unknownNodeCount = 0;
418
+ for (const parsedClass of parsedClasses) for (const dependency of parsedClass.dependencies) {
419
+ if (!nodeMap.has(dependency.token)) {
420
+ nodeMap.set(dependency.token, {
421
+ id: dependency.token,
422
+ kind: "unknown"
423
+ });
424
+ unknownNodeCount++;
425
+ logger?.warn(LogCategory.GRAPH_CONSTRUCTION, `Created unknown node: ${dependency.token}`, {
426
+ nodeId: dependency.token,
427
+ referencedBy: parsedClass.name
428
+ });
429
+ }
430
+ const edge = {
431
+ from: parsedClass.name,
432
+ to: dependency.token
433
+ };
434
+ if (dependency.flags) edge.flags = dependency.flags;
435
+ edges.push(edge);
436
+ }
437
+ logger?.info(LogCategory.GRAPH_CONSTRUCTION, `Created ${edges.length} edges`, {
438
+ edgeCount: edges.length,
439
+ unknownNodeCount
440
+ });
441
+ const nodes = Array.from(nodeMap.values()).sort((a, b) => a.id.localeCompare(b.id));
442
+ edges.sort((a, b) => {
443
+ const fromCompare = a.from.localeCompare(b.from);
444
+ if (fromCompare !== 0) return fromCompare;
445
+ return a.to.localeCompare(b.to);
446
+ });
447
+ logger?.time("circularDetection");
448
+ const { circularDependencies, circularEdges } = detectCircularDependencies(edges, nodes);
449
+ const circularDetectionTime = logger?.timeEnd("circularDetection");
450
+ if (circularDependencies.length > 0) logger?.warn(LogCategory.GRAPH_CONSTRUCTION, `Circular dependencies detected: ${circularDependencies.length}`, {
451
+ circularCount: circularDependencies.length,
452
+ cycles: circularDependencies,
453
+ detectionTime: circularDetectionTime
454
+ });
455
+ else logger?.info(LogCategory.GRAPH_CONSTRUCTION, "No circular dependencies detected", { detectionTime: circularDetectionTime });
456
+ for (const edge of edges) {
457
+ const edgeKey = `${edge.from}->${edge.to}`;
458
+ if (circularEdges.has(edgeKey)) edge.isCircular = true;
459
+ }
460
+ const duration = logger?.timeEnd("buildGraph");
461
+ logger?.info(LogCategory.GRAPH_CONSTRUCTION, "Graph construction complete", {
462
+ duration,
463
+ nodeCount: nodes.length,
464
+ edgeCount: edges.length,
465
+ circularCount: circularDependencies.length
466
+ });
467
+ return {
468
+ nodes,
469
+ edges,
470
+ circularDependencies
471
+ };
472
+ }
473
+
474
+ //#endregion
475
+ //#region src/core/graph-filter.ts
476
+ /**
477
+ * Helper function to validate entry points and perform traversal
478
+ * @param entryPoints Array of entry point names to validate and traverse from
479
+ * @param graph The graph containing all nodes
480
+ * @param adjacencyList The adjacency list for traversal
481
+ * @param resultSet The set to collect traversal results
482
+ * @param options CLI options for verbose output
483
+ */
484
+ function validateAndTraverseEntryPoints(entryPoints, graph, adjacencyList, resultSet, options) {
485
+ for (const entryPoint of entryPoints) if (graph.nodes.some((n) => n.id === entryPoint)) traverseFromEntry(entryPoint, adjacencyList, resultSet);
486
+ else if (options.verbose) console.warn(`Entry point '${entryPoint}' not found in graph`);
487
+ }
488
+ /**
489
+ * Filters a graph based on entry points and traversal direction
490
+ * @param graph The graph to filter
491
+ * @param options CLI options containing entry points and direction
492
+ * @returns Filtered graph containing only nodes reachable from entry points
493
+ */
494
+ function filterGraph(graph, options) {
495
+ if (!options.entry || options.entry.length === 0) return graph;
496
+ const includedNodeIds = /* @__PURE__ */ new Set();
497
+ if (options.direction === "both") {
498
+ const upstreamAdjacencyList = buildAdjacencyList(graph, "upstream");
499
+ const upstreamNodes = /* @__PURE__ */ new Set();
500
+ validateAndTraverseEntryPoints(options.entry, graph, upstreamAdjacencyList, upstreamNodes, options);
501
+ const downstreamAdjacencyList = buildAdjacencyList(graph, "downstream");
502
+ const downstreamNodes = /* @__PURE__ */ new Set();
503
+ validateAndTraverseEntryPoints(options.entry, graph, downstreamAdjacencyList, downstreamNodes, options);
504
+ const combinedNodes = new Set([...upstreamNodes, ...downstreamNodes]);
505
+ for (const nodeId of combinedNodes) includedNodeIds.add(nodeId);
506
+ } else {
507
+ const adjacencyList = buildAdjacencyList(graph, options.direction);
508
+ validateAndTraverseEntryPoints(options.entry, graph, adjacencyList, includedNodeIds, options);
509
+ }
510
+ const filteredNodes = graph.nodes.filter((node) => includedNodeIds.has(node.id));
511
+ const filteredEdges = graph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to));
512
+ const filteredCircularDeps = graph.circularDependencies.filter((cycle) => {
513
+ if (cycle.length < 2) return false;
514
+ const isSelfLoop = cycle.length === 2 && cycle[0] === cycle[1];
515
+ const isProperCycleWithClosing = cycle.length >= 3 && cycle[0] === cycle[cycle.length - 1];
516
+ const isProperCycleWithoutClosing = cycle.length >= 3 && cycle[0] !== cycle[cycle.length - 1];
517
+ if (!isSelfLoop && !isProperCycleWithClosing && !isProperCycleWithoutClosing) return false;
518
+ if (!cycle.every((nodeId) => includedNodeIds.has(nodeId))) return false;
519
+ const edgesToCheck = isProperCycleWithClosing ? cycle.length - 1 : cycle.length;
520
+ for (let i = 0; i < edgesToCheck; i++) {
521
+ const fromNode = cycle[i];
522
+ const toNode = isProperCycleWithClosing ? cycle[i + 1] : cycle[(i + 1) % cycle.length];
523
+ if (!graph.edges.some((edge) => edge.from === fromNode && edge.to === toNode)) return false;
524
+ }
525
+ return true;
526
+ });
527
+ if (options.verbose) {
528
+ console.log(`Filtered graph: ${filteredNodes.length} nodes, ${filteredEdges.length} edges`);
529
+ console.log(`Entry points: ${options.entry.join(", ")}`);
530
+ }
531
+ return {
532
+ nodes: filteredNodes,
533
+ edges: filteredEdges,
534
+ circularDependencies: filteredCircularDeps
535
+ };
536
+ }
537
+ /**
538
+ * Traverses the graph from a starting node using DFS
539
+ * @param startNode The node to start traversal from
540
+ * @param adjacencyList The adjacency list representation of the graph
541
+ * @param visited Set to track visited nodes
542
+ */
543
+ function traverseFromEntry(startNode, adjacencyList, visited) {
544
+ const stack = [startNode];
545
+ while (stack.length > 0) {
546
+ const currentNode = stack.pop();
547
+ if (!currentNode) break;
548
+ if (visited.has(currentNode)) continue;
549
+ visited.add(currentNode);
550
+ const neighbors = adjacencyList.get(currentNode) || [];
551
+ for (const neighbor of neighbors) if (!visited.has(neighbor)) stack.push(neighbor);
552
+ }
553
+ }
554
+ /**
555
+ * Builds an adjacency list from the graph based on direction
556
+ * @param graph The graph to build adjacency list from
557
+ * @param direction The direction to follow edges
558
+ * @returns Map representing adjacency list
559
+ */
560
+ function buildAdjacencyList(graph, direction) {
561
+ const adjacencyList = /* @__PURE__ */ new Map();
562
+ for (const node of graph.nodes) adjacencyList.set(node.id, []);
563
+ for (const edge of graph.edges) if (direction === "downstream") {
564
+ const neighbors = adjacencyList.get(edge.from) || [];
565
+ neighbors.push(edge.to);
566
+ adjacencyList.set(edge.from, neighbors);
567
+ } else if (direction === "upstream") {
568
+ const neighbors = adjacencyList.get(edge.to) || [];
569
+ neighbors.push(edge.from);
570
+ adjacencyList.set(edge.to, neighbors);
571
+ }
572
+ return adjacencyList;
573
+ }
574
+
575
+ //#endregion
576
+ //#region src/core/output-handler.ts
577
+ /**
578
+ * Handles output writing to stdout or files
579
+ * Supports directory creation and error handling
580
+ */
581
+ var OutputHandler = class {
582
+ /**
583
+ * Write output content to stdout or file
584
+ * @param content The content to write
585
+ * @param filePath Optional file path. If not provided, writes to stdout
586
+ */
587
+ async writeOutput(content, filePath) {
588
+ if (!filePath) {
589
+ process.stdout.write(content);
590
+ return;
591
+ }
592
+ try {
593
+ const dir = (0, node_path.dirname)(filePath);
594
+ if (!(0, node_fs.existsSync)(dir)) (0, node_fs.mkdirSync)(dir, { recursive: true });
595
+ (0, node_fs.writeFileSync)(filePath, content, "utf-8");
596
+ } catch (error) {
597
+ throw new Error(`Failed to write output file: ${error instanceof Error ? error.message : "Unknown error"}`);
598
+ }
599
+ }
600
+ };
601
+
602
+ //#endregion
603
+ //#region src/core/parser.ts
604
+ /**
605
+ * AngularParser - Core TypeScript AST parsing using ts-morph
606
+ * Implements FR-01: ts-morph project loading with comprehensive error handling
607
+ * Implements FR-02: Decorated class collection
608
+ * Implements FR-03: Constructor token resolution
609
+ */
610
+ const GLOBAL_WARNING_KEYS = /* @__PURE__ */ new Set();
611
+ var AngularParser = class {
612
+ constructor(_options, _logger) {
613
+ this._options = _options;
614
+ this._logger = _logger;
615
+ this._typeResolutionCache = /* @__PURE__ */ new Map();
616
+ this._circularTypeRefs = /* @__PURE__ */ new Set();
617
+ this._structuredWarnings = {
618
+ categories: {
619
+ typeResolution: [],
620
+ skippedTypes: [],
621
+ unresolvedImports: [],
622
+ circularReferences: [],
623
+ performance: []
624
+ },
625
+ totalCount: 0
626
+ };
627
+ }
628
+ /**
629
+ * Reset global warning deduplication state (useful for testing)
630
+ */
631
+ static resetWarningState() {
632
+ GLOBAL_WARNING_KEYS.clear();
633
+ }
634
+ /**
635
+ * Get structured warnings for analysis (Task 3.3)
636
+ * Returns a copy of the structured warnings collected during parsing
637
+ * @returns StructuredWarnings object with categorized warnings
638
+ */
639
+ getStructuredWarnings() {
640
+ return {
641
+ categories: {
642
+ typeResolution: [...this._structuredWarnings.categories.typeResolution],
643
+ skippedTypes: [...this._structuredWarnings.categories.skippedTypes],
644
+ unresolvedImports: [...this._structuredWarnings.categories.unresolvedImports],
645
+ circularReferences: [...this._structuredWarnings.categories.circularReferences],
646
+ performance: [...this._structuredWarnings.categories.performance]
647
+ },
648
+ totalCount: this._structuredWarnings.totalCount
649
+ };
650
+ }
651
+ /**
652
+ * Add structured warning to collection (Task 3.3)
653
+ * Includes global deduplication for both structured warnings and console output
654
+ * @param category Warning category
655
+ * @param warning Warning object
656
+ */
657
+ addStructuredWarning(category, warning) {
658
+ const warnKey = `${category}_${warning.type}_${warning.file}_${warning.message}`;
659
+ if (!GLOBAL_WARNING_KEYS.has(warnKey)) {
660
+ this._structuredWarnings.categories[category].push(warning);
661
+ this._structuredWarnings.totalCount++;
662
+ const location = warning.line ? `${warning.file}:${warning.line}:${warning.column}` : warning.file;
663
+ console.warn(`[${warning.severity.toUpperCase()}] ${warning.message} (${location})`);
664
+ if (warning.suggestion && this._options.verbose) console.warn(` Suggestion: ${warning.suggestion}`);
665
+ GLOBAL_WARNING_KEYS.add(warnKey);
666
+ }
667
+ }
668
+ /**
669
+ * Load TypeScript project using ts-morph
670
+ * Implements FR-01 with error handling from PRD Section 13
671
+ */
672
+ loadProject() {
673
+ if (!(0, node_fs.existsSync)(this._options.project)) throw ErrorHandler.createError(`tsconfig.json not found at: ${this._options.project}`, "TSCONFIG_NOT_FOUND", this._options.project);
674
+ try {
675
+ try {
676
+ const configContent = (0, node_fs.readFileSync)(this._options.project, "utf8");
677
+ JSON.parse(configContent);
678
+ } catch (jsonError) {
679
+ const errorMessage = jsonError instanceof Error ? jsonError.message : String(jsonError);
680
+ throw ErrorHandler.createError(`Invalid tsconfig.json: ${errorMessage}`, "TSCONFIG_INVALID", this._options.project);
681
+ }
682
+ this._project = new ts_morph.Project({ tsConfigFilePath: this._options.project });
683
+ if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
684
+ this._project.getSourceFiles();
685
+ const diagnostics = this._project.getProgram().getConfigFileParsingDiagnostics();
686
+ if (diagnostics.length > 0) {
687
+ const message = diagnostics[0].getMessageText();
688
+ throw ErrorHandler.createError(`TypeScript configuration error: ${message}`, "PROJECT_LOAD_FAILED", this._options.project, { diagnosticCount: diagnostics.length });
689
+ }
690
+ } catch (error) {
691
+ if (error instanceof CliError) throw error;
692
+ if (error instanceof Error) {
693
+ if (error.message.includes("JSON") || error.message.includes("Unexpected token") || error.message.includes("expected")) throw ErrorHandler.createError(`Invalid tsconfig.json: ${error.message}`, "TSCONFIG_INVALID", this._options.project);
694
+ if (error.message.includes("TypeScript") || error.message.includes("Compiler option")) throw ErrorHandler.createError(`TypeScript compilation failed: ${error.message}`, "COMPILATION_ERROR", this._options.project);
695
+ throw ErrorHandler.createError(`Failed to load TypeScript project: ${error.message}`, "PROJECT_LOAD_FAILED", this._options.project);
696
+ }
697
+ throw ErrorHandler.createError("Failed to load TypeScript project due to unknown error", "PROJECT_LOAD_FAILED", this._options.project);
698
+ }
699
+ }
700
+ /**
701
+ * Get the loaded ts-morph Project instance
702
+ * @returns Project instance
703
+ * @throws Error if project not loaded
704
+ */
705
+ getProject() {
706
+ if (!this._project) throw new Error("Project not loaded. Call loadProject() first.");
707
+ return this._project;
708
+ }
709
+ /**
710
+ * Parse decorated classes from the loaded project
711
+ * Auto-loads project if not already loaded
712
+ * @returns Promise of parsed class information
713
+ */
714
+ async parseClasses() {
715
+ if (!this._project) this.loadProject();
716
+ return this.findDecoratedClasses();
717
+ }
718
+ /**
719
+ * Find all classes decorated with @Injectable, @Component, or @Directive
720
+ * Implements FR-02: Decorated Class Collection
721
+ * @returns Promise<ParsedClass[]> List of decorated classes
722
+ */
723
+ async findDecoratedClasses() {
724
+ if (!this._project) this.loadProject();
725
+ if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
726
+ const decoratedClasses = [];
727
+ const sourceFiles = this._project.getSourceFiles();
728
+ let processedFiles = 0;
729
+ let skippedFiles = 0;
730
+ this._circularTypeRefs.clear();
731
+ this._logger?.time("findDecoratedClasses");
732
+ this._logger?.info(LogCategory.FILE_PROCESSING, "Starting file processing", { fileCount: sourceFiles.length });
733
+ if (this._options.verbose) console.log(`Processing ${sourceFiles.length} source files`);
734
+ for (const sourceFile of sourceFiles) {
735
+ const filePath = sourceFile.getFilePath();
736
+ try {
737
+ if (this._options.verbose) console.log(`🔍 Parsing file: ${filePath}`);
738
+ this._logger?.debug(LogCategory.FILE_PROCESSING, "Processing file", { filePath });
739
+ const classes = sourceFile.getClasses();
740
+ if (this._options.verbose) console.log(`File: ${filePath}, Classes: ${classes.length}`);
741
+ this._logger?.debug(LogCategory.AST_ANALYSIS, "Analyzing classes in file", {
742
+ filePath,
743
+ classCount: classes.length
744
+ });
745
+ for (const classDeclaration of classes) {
746
+ const parsedClass = this.parseClassDeclaration(classDeclaration);
747
+ if (parsedClass) {
748
+ decoratedClasses.push(parsedClass);
749
+ if (this._options.verbose) console.log(`Found decorated class: ${parsedClass.name} (${parsedClass.kind})`);
750
+ this._logger?.info(LogCategory.AST_ANALYSIS, "Found decorated class", {
751
+ className: parsedClass.name,
752
+ kind: parsedClass.kind,
753
+ filePath
754
+ });
755
+ }
756
+ }
757
+ this.detectAnonymousClasses(sourceFile);
758
+ processedFiles++;
759
+ } catch (error) {
760
+ skippedFiles++;
761
+ if (error instanceof CliError) {
762
+ if (!error.isFatal()) {
763
+ ErrorHandler.warn(error.message, filePath);
764
+ continue;
765
+ }
766
+ throw error;
767
+ }
768
+ ErrorHandler.warn(`Failed to parse file (skipping): ${error instanceof Error ? error.message : "Unknown error"}`, filePath);
769
+ }
770
+ }
771
+ if (this._options.verbose) console.log(`✅ Processed ${processedFiles} files, skipped ${skippedFiles} files`);
772
+ const elapsed = this._logger?.timeEnd("findDecoratedClasses") || 0;
773
+ this._logger?.info(LogCategory.PERFORMANCE, "File processing complete", {
774
+ totalClasses: decoratedClasses.length,
775
+ processedFiles,
776
+ skippedFiles,
777
+ timing: elapsed
778
+ });
779
+ if (decoratedClasses.length === 0) ErrorHandler.warn("No decorated classes found in the project");
780
+ return decoratedClasses;
781
+ }
782
+ /**
783
+ * Parse a single class declaration for Angular decorators
784
+ * @param classDeclaration ts-morph ClassDeclaration
785
+ * @returns ParsedClass if decorated with Angular decorator, null otherwise
786
+ */
787
+ parseClassDeclaration(classDeclaration) {
788
+ const className = classDeclaration.getName();
789
+ if (!className) {
790
+ console.warn("Warning: Skipping anonymous class - classes must be named for dependency injection analysis");
791
+ return null;
792
+ }
793
+ const decorators = classDeclaration.getDecorators();
794
+ if (this._options.verbose) {
795
+ const decoratorNames = decorators.map((d) => this.getDecoratorName(d)).join(", ");
796
+ console.log(`Class: ${className}, Decorators: ${decorators.length} [${decoratorNames}]`);
797
+ }
798
+ const angularDecorator = this.findAngularDecorator(decorators);
799
+ if (!angularDecorator) {
800
+ if (this._options.verbose && decorators.length > 0) console.log(` No Angular decorator found for ${className}`);
801
+ return null;
802
+ }
803
+ return {
804
+ name: className,
805
+ kind: this.determineNodeKind(angularDecorator),
806
+ filePath: classDeclaration.getSourceFile().getFilePath(),
807
+ dependencies: this.extractConstructorDependencies(classDeclaration)
808
+ };
809
+ }
810
+ /**
811
+ * Find Angular decorator (@Injectable, @Component, @Directive) from list of decorators
812
+ * @param decorators Array of decorators from ts-morph
813
+ * @returns Angular decorator if found, null otherwise
814
+ */
815
+ findAngularDecorator(decorators) {
816
+ for (const decorator of decorators) {
817
+ const decoratorName = this.getDecoratorName(decorator);
818
+ if (decoratorName === "Injectable" || decoratorName === "Component" || decoratorName === "Directive") return decorator;
819
+ }
820
+ return null;
821
+ }
822
+ /**
823
+ * Extract decorator name from ts-morph Decorator
824
+ * Handles various import patterns and aliases
825
+ * @param decorator ts-morph Decorator
826
+ * @returns Decorator name string
827
+ */
828
+ getDecoratorName(decorator) {
829
+ const callExpression = decorator.getCallExpression();
830
+ if (!callExpression) return "";
831
+ const expression = callExpression.getExpression();
832
+ if (expression.getKind() === ts_morph.SyntaxKind.Identifier) {
833
+ const decoratorName = expression.asKindOrThrow(ts_morph.SyntaxKind.Identifier).getText();
834
+ return this.resolveDecoratorAlias(decorator.getSourceFile(), decoratorName) || decoratorName;
835
+ }
836
+ return "";
837
+ }
838
+ /**
839
+ * Resolve decorator alias from import declarations with basic caching
840
+ * @param sourceFile Source file containing the decorator
841
+ * @param decoratorName Raw decorator name from AST
842
+ * @returns Original decorator name if alias found, null otherwise
843
+ */
844
+ resolveDecoratorAlias(sourceFile, decoratorName) {
845
+ const importDeclarations = sourceFile.getImportDeclarations();
846
+ for (const importDecl of importDeclarations) if (importDecl.getModuleSpecifierValue() === "@angular/core") {
847
+ const namedImports = importDecl.getNamedImports();
848
+ for (const namedImport of namedImports) {
849
+ const alias = namedImport.getAliasNode();
850
+ if (alias && alias.getText() === decoratorName) return namedImport.getName();
851
+ if (!alias && namedImport.getName() === decoratorName) return decoratorName;
852
+ }
853
+ }
854
+ return null;
855
+ }
856
+ /**
857
+ * Determine NodeKind from Angular decorator
858
+ * @param decorator Angular decorator
859
+ * @returns NodeKind mapping
860
+ */
861
+ determineNodeKind(decorator) {
862
+ switch (this.getDecoratorName(decorator)) {
863
+ case "Injectable": return "service";
864
+ case "Component": return "component";
865
+ case "Directive": return "directive";
866
+ default: return "unknown";
867
+ }
868
+ }
869
+ /**
870
+ * Detect and warn about anonymous class expressions
871
+ * Handles patterns like: const X = Decorator()(class { ... })
872
+ * @param sourceFile Source file to analyze
873
+ */
874
+ detectAnonymousClasses(sourceFile) {
875
+ try {
876
+ sourceFile.forEachDescendant((node) => {
877
+ if (node.getKind() === ts_morph.SyntaxKind.ClassExpression) {
878
+ const parent = node.getParent();
879
+ if (parent && parent.getKind() === ts_morph.SyntaxKind.CallExpression) {
880
+ const grandParent = parent.getParent();
881
+ if (grandParent && grandParent.getKind() === ts_morph.SyntaxKind.CallExpression) {
882
+ console.warn("Warning: Skipping anonymous class - classes must be named for dependency injection analysis");
883
+ if (this._options.verbose) console.log(` Anonymous class found in ${sourceFile.getFilePath()}`);
884
+ }
885
+ }
886
+ }
887
+ });
888
+ } catch (error) {
889
+ if (this._options.verbose) console.log(` Could not detect anonymous classes in ${sourceFile.getFilePath()}: ${error}`);
890
+ }
891
+ }
892
+ /**
893
+ * Extract constructor dependencies from a class declaration
894
+ * Implements FR-03: Constructor parameter analysis
895
+ * Implements TDD Cycle 2.1: inject() function detection
896
+ * @param classDeclaration ts-morph ClassDeclaration
897
+ * @returns Array of parsed dependencies
898
+ */
899
+ extractConstructorDependencies(classDeclaration) {
900
+ const dependencies = [];
901
+ const verboseStats = {
902
+ decoratorCounts: {
903
+ optional: 0,
904
+ self: 0,
905
+ skipSelf: 0,
906
+ host: 0
907
+ },
908
+ skippedDecorators: [],
909
+ parametersWithDecorators: 0,
910
+ parametersWithoutDecorators: 0,
911
+ legacyDecoratorsUsed: 0,
912
+ injectPatternsUsed: 0,
913
+ totalProcessingTime: 0,
914
+ totalParameters: 0
915
+ };
916
+ const startTime = performance.now();
917
+ if (this._options.verbose && this._options.includeDecorators) {
918
+ console.log("=== Decorator Analysis ===");
919
+ const className = classDeclaration.getName() || "unknown";
920
+ console.log(`Analyzing decorators for class: ${className}`);
921
+ }
922
+ if (this._options.verbose && !this._options.includeDecorators) console.log("Decorator analysis disabled - --include-decorators flag not set");
923
+ const constructors = classDeclaration.getConstructors();
924
+ if (constructors.length > 0) {
925
+ const parameters = constructors[0].getParameters();
926
+ verboseStats.totalParameters = parameters.length;
927
+ for (const param of parameters) {
928
+ const paramStartTime = performance.now();
929
+ const dependency = this.parseConstructorParameter(param);
930
+ const paramEndTime = performance.now();
931
+ if (dependency) {
932
+ dependencies.push(dependency);
933
+ if (this._options.verbose && this._options.includeDecorators) this.collectVerboseStats(param, dependency, verboseStats);
934
+ }
935
+ verboseStats.totalProcessingTime += paramEndTime - paramStartTime;
936
+ }
937
+ }
938
+ const injectDependencies = this.extractInjectFunctionDependencies(classDeclaration);
939
+ dependencies.push(...injectDependencies);
940
+ verboseStats.totalProcessingTime = performance.now() - startTime;
941
+ if (this._options.verbose) this.outputVerboseAnalysis(dependencies, verboseStats, classDeclaration);
942
+ return dependencies;
943
+ }
944
+ /**
945
+ * Check if type reference is circular (Task 3.3)
946
+ * @param typeText Type text to check
947
+ * @param typeNode TypeNode for context
948
+ * @returns True if circular reference detected
949
+ */
950
+ isCircularTypeReference(typeText, typeNode) {
951
+ const currentClass = typeNode.getFirstAncestorByKind(ts_morph.SyntaxKind.ClassDeclaration);
952
+ if (currentClass) {
953
+ if (currentClass.getName() === typeText) return true;
954
+ }
955
+ if (this._circularTypeRefs.has(typeText)) return true;
956
+ this._circularTypeRefs.add(typeText);
957
+ return false;
958
+ }
959
+ /**
960
+ * Check if type is generic (Task 3.3)
961
+ * @param typeText Type text to check
962
+ * @returns True if generic type
963
+ */
964
+ isGenericType(typeText) {
965
+ return typeText.includes("<") && typeText.includes(">");
966
+ }
967
+ /**
968
+ * Handle generic types (Task 3.3)
969
+ * @param typeText Generic type text
970
+ * @param filePath File path for context
971
+ * @param lineNumber Line number
972
+ * @param columnNumber Column number
973
+ * @returns Token string or null
974
+ */
975
+ handleGenericType(typeText, _filePath, _lineNumber, _columnNumber) {
976
+ if (this._options.verbose) console.log(`Processing generic type: ${typeText}`);
977
+ return typeText;
978
+ }
979
+ /**
980
+ * Check if type is union type (Task 3.3)
981
+ * @param typeText Type text to check
982
+ * @returns True if union type
983
+ */
984
+ isUnionType(typeText) {
985
+ return typeText.includes(" | ") && !typeText.includes("<");
986
+ }
987
+ /**
988
+ * Handle union types (Task 3.3)
989
+ * @param typeText Union type text
990
+ * @param filePath File path for context
991
+ * @param lineNumber Line number
992
+ * @param columnNumber Column number
993
+ * @returns Null (union types are skipped)
994
+ */
995
+ handleUnionType(typeText, filePath, lineNumber, columnNumber) {
996
+ this.addStructuredWarning("skippedTypes", {
997
+ type: "complex_union_type",
998
+ message: `Skipping complex union type: ${typeText}`,
999
+ file: filePath,
1000
+ line: lineNumber,
1001
+ column: columnNumber,
1002
+ suggestion: "Consider using a single service type or interface",
1003
+ severity: "info"
1004
+ });
1005
+ return null;
1006
+ }
1007
+ /**
1008
+ * Check if type can be resolved through imports (Task 3.3)
1009
+ * Handles module-scoped types (e.g., MyModule.ScopedService)
1010
+ * @param typeNode TypeNode to check
1011
+ * @returns True if type can be resolved
1012
+ */
1013
+ canResolveType(typeNode) {
1014
+ try {
1015
+ const sourceFile = typeNode.getSourceFile();
1016
+ const typeText = typeNode.getText();
1017
+ const type = typeNode.getType();
1018
+ const typeTextFromSystem = type.getText();
1019
+ const symbol = type.getSymbol();
1020
+ if (typeTextFromSystem === "error" || typeTextFromSystem === "any" || type.isUnknown()) return false;
1021
+ if (!symbol) return false;
1022
+ if ([
1023
+ "Array",
1024
+ "Promise",
1025
+ "Observable",
1026
+ "Date",
1027
+ "Error"
1028
+ ].includes(typeText)) return true;
1029
+ let simpleTypeText = typeText;
1030
+ if (typeText.includes(".")) simpleTypeText = typeText.split(".")[0];
1031
+ const imports = sourceFile.getImportDeclarations();
1032
+ for (const importDecl of imports) {
1033
+ const namedImports = importDecl.getNamedImports();
1034
+ for (const namedImport of namedImports) if (namedImport.getName() === typeText || namedImport.getName() === simpleTypeText) return true;
1035
+ }
1036
+ const typeAliases = sourceFile.getTypeAliases();
1037
+ const interfaces = sourceFile.getInterfaces();
1038
+ const classes = sourceFile.getClasses();
1039
+ if ([
1040
+ ...typeAliases,
1041
+ ...interfaces,
1042
+ ...classes
1043
+ ].some((decl) => decl.getName() === typeText || decl.getName() === simpleTypeText)) return true;
1044
+ if (typeText.includes(".")) {
1045
+ const namespaces = sourceFile.getModules();
1046
+ for (const namespace of namespaces) if (namespace.getName() === simpleTypeText) return true;
1047
+ }
1048
+ return false;
1049
+ } catch {
1050
+ return false;
1051
+ }
1052
+ }
1053
+ /**
1054
+ * Enhanced type token extraction with advanced analysis (Task 3.3)
1055
+ * @param typeNode TypeNode to extract token from
1056
+ * @param filePath File path for context
1057
+ * @param lineNumber Line number
1058
+ * @param columnNumber Column number
1059
+ * @returns Token string or null
1060
+ */
1061
+ extractTypeTokenEnhanced(typeNode, filePath, lineNumber, columnNumber) {
1062
+ const typeText = typeNode.getText();
1063
+ if (this._options.verbose) console.log(`Type resolution steps: Processing '${typeText}' at ${filePath}:${lineNumber}:${columnNumber}`);
1064
+ if (this.isCircularTypeReference(typeText, typeNode)) {
1065
+ this.addStructuredWarning("circularReferences", {
1066
+ type: "circular_type_reference",
1067
+ message: `Circular type reference detected: ${typeText}`,
1068
+ file: filePath,
1069
+ line: lineNumber,
1070
+ column: columnNumber,
1071
+ suggestion: "Consider using interfaces or breaking the circular dependency",
1072
+ severity: "warning"
1073
+ });
1074
+ return null;
1075
+ }
1076
+ if (this.isGenericType(typeText)) return this.handleGenericType(typeText, filePath, lineNumber, columnNumber);
1077
+ if (this.isUnionType(typeText)) return this.handleUnionType(typeText, filePath, lineNumber, columnNumber);
1078
+ if (this.shouldSkipType(typeText)) {
1079
+ this.addStructuredWarning("skippedTypes", {
1080
+ type: "any_unknown_type",
1081
+ message: `Skipping parameter with any/unknown type: ${typeText}`,
1082
+ file: filePath,
1083
+ line: lineNumber,
1084
+ column: columnNumber,
1085
+ suggestion: "Consider adding explicit type annotation",
1086
+ severity: "warning"
1087
+ });
1088
+ return null;
1089
+ }
1090
+ if (this.isPrimitiveType(typeText)) {
1091
+ this.addStructuredWarning("skippedTypes", {
1092
+ type: "primitive_type",
1093
+ message: `Skipping primitive type: ${typeText}`,
1094
+ file: filePath,
1095
+ line: lineNumber,
1096
+ column: columnNumber,
1097
+ suggestion: "Use dependency injection for services, not primitive types",
1098
+ severity: "info"
1099
+ });
1100
+ return null;
1101
+ }
1102
+ if (!this.canResolveType(typeNode)) {
1103
+ this.addStructuredWarning("unresolvedImports", {
1104
+ type: "unresolved_type",
1105
+ message: `Unresolved type '${typeText}' - check imports`,
1106
+ file: filePath,
1107
+ line: lineNumber,
1108
+ column: columnNumber,
1109
+ suggestion: `Ensure ${typeText} is properly imported`,
1110
+ severity: "warning"
1111
+ });
1112
+ return null;
1113
+ }
1114
+ return typeText;
1115
+ }
1116
+ /**
1117
+ * Resolve inferred types with enhanced validation (Task 3.3)
1118
+ * @param type Type object from ts-morph
1119
+ * @param typeText Type text representation
1120
+ * @param param Parameter declaration for context
1121
+ * @param filePath File path for warnings
1122
+ * @param lineNumber Line number for warnings
1123
+ * @param columnNumber Column number for warnings
1124
+ * @returns Resolved token or null
1125
+ */
1126
+ resolveInferredTypeEnhanced(type, typeText, param, filePath, lineNumber, columnNumber) {
1127
+ if (this._options.verbose) console.log(`Attempting to resolve inferred type: ${typeText}`);
1128
+ const symbol = type.getSymbol?.();
1129
+ if (symbol) {
1130
+ const symbolName = symbol.getName();
1131
+ if (symbolName && symbolName !== "__type") return symbolName;
1132
+ }
1133
+ const aliasSymbol = type.getAliasSymbol?.();
1134
+ if (aliasSymbol) {
1135
+ const declarations = aliasSymbol.getDeclarations();
1136
+ if (declarations && declarations.length > 0) return aliasSymbol.getName();
1137
+ }
1138
+ if (this.shouldSkipType(typeText)) {
1139
+ this.addStructuredWarning("skippedTypes", {
1140
+ type: "inferred_any_unknown",
1141
+ message: `Skipping parameter '${param.getName()}' with inferred any/unknown type`,
1142
+ file: filePath,
1143
+ line: lineNumber,
1144
+ column: columnNumber,
1145
+ suggestion: "Add explicit type annotation to improve type safety",
1146
+ severity: "warning"
1147
+ });
1148
+ return null;
1149
+ }
1150
+ if (this.isPrimitiveType(typeText)) {
1151
+ this.addStructuredWarning("skippedTypes", {
1152
+ type: "inferred_primitive",
1153
+ message: `Skipping inferred primitive type parameter '${param.getName()}': ${typeText}`,
1154
+ file: filePath,
1155
+ line: lineNumber,
1156
+ column: columnNumber,
1157
+ suggestion: "Use dependency injection for services, not primitive types",
1158
+ severity: "info"
1159
+ });
1160
+ return null;
1161
+ }
1162
+ const aliasSymbolCheck = type.getAliasSymbol?.();
1163
+ const hasValidAliasSymbol = aliasSymbolCheck && aliasSymbolCheck.getDeclarations().length > 0;
1164
+ if (!symbol && !hasValidAliasSymbol) {
1165
+ this.addStructuredWarning("unresolvedImports", {
1166
+ type: "unresolved_type",
1167
+ message: `Unresolved type '${typeText}' - check imports`,
1168
+ file: filePath,
1169
+ line: lineNumber,
1170
+ column: columnNumber,
1171
+ suggestion: `Ensure ${typeText} is properly imported`,
1172
+ severity: "warning"
1173
+ });
1174
+ return null;
1175
+ }
1176
+ return typeText;
1177
+ }
1178
+ /**
1179
+ * Parse a single constructor parameter to extract dependency token
1180
+ * Implements FR-03 token resolution priority: @Inject > type annotation > inferred type
1181
+ * Implements FR-04 parameter decorator handling
1182
+ * @param param ts-morph ParameterDeclaration
1183
+ * @returns ParsedDependency if valid dependency, null if should be skipped
1184
+ */
1185
+ parseConstructorParameter(param) {
1186
+ const parameterName = param.getName();
1187
+ const filePath = param.getSourceFile().getFilePath();
1188
+ const lineNumber = param.getStartLineNumber();
1189
+ const columnNumber = param.getStart() - param.getStartLinePos();
1190
+ const startTime = performance.now();
1191
+ let cacheKey = null;
1192
+ try {
1193
+ const flags = this.extractParameterDecorators(param);
1194
+ const injectDecorator = param.getDecorator("Inject");
1195
+ if (injectDecorator) {
1196
+ const token = this.extractInjectToken(injectDecorator);
1197
+ if (token) return {
1198
+ token,
1199
+ flags,
1200
+ parameterName
1201
+ };
1202
+ }
1203
+ const initializer = param.getInitializer();
1204
+ if (initializer) {
1205
+ const injectResult = this.analyzeInjectCall(initializer);
1206
+ if (injectResult) {
1207
+ const finalFlags = Object.keys(flags).length > 0 ? flags : injectResult.flags;
1208
+ return {
1209
+ token: injectResult.token,
1210
+ flags: finalFlags,
1211
+ parameterName
1212
+ };
1213
+ }
1214
+ }
1215
+ const typeNode_check = param.getTypeNode();
1216
+ if (typeNode_check) {
1217
+ const token = this.extractTypeTokenEnhanced(typeNode_check, filePath, lineNumber, columnNumber);
1218
+ if (token) return {
1219
+ token,
1220
+ flags,
1221
+ parameterName
1222
+ };
1223
+ }
1224
+ const type = param.getType();
1225
+ const typeText = type.getText(param);
1226
+ cacheKey = `${filePath}:${parameterName}:${typeText}`;
1227
+ if (this._typeResolutionCache.has(cacheKey)) {
1228
+ const cachedResult = this._typeResolutionCache.get(cacheKey);
1229
+ if (this._options.verbose) console.log(`Cache hit for parameter '${parameterName}': ${typeText}`);
1230
+ return cachedResult ? {
1231
+ token: cachedResult,
1232
+ flags,
1233
+ parameterName
1234
+ } : null;
1235
+ }
1236
+ if (this._options.verbose) console.log(`Cache miss for parameter '${parameterName}': ${typeText}`);
1237
+ const resolvedToken = this.resolveInferredTypeEnhanced(type, typeText, param, filePath, lineNumber, columnNumber);
1238
+ this._typeResolutionCache.set(cacheKey, resolvedToken);
1239
+ if (resolvedToken) return {
1240
+ token: resolvedToken,
1241
+ flags,
1242
+ parameterName
1243
+ };
1244
+ return null;
1245
+ } finally {
1246
+ const duration = performance.now() - startTime;
1247
+ if (cacheKey && duration > 10) {
1248
+ if (this._typeResolutionCache.get(cacheKey)) this.addStructuredWarning("performance", {
1249
+ type: "slow_type_resolution",
1250
+ message: `Slow type resolution for parameter '${parameterName}' (${duration.toFixed(2)}ms)`,
1251
+ file: filePath,
1252
+ line: lineNumber,
1253
+ column: columnNumber,
1254
+ suggestion: "Consider adding explicit type annotation",
1255
+ severity: "info"
1256
+ });
1257
+ }
1258
+ }
1259
+ }
1260
+ /**
1261
+ * Extract token from @Inject decorator
1262
+ * @param decorator @Inject decorator
1263
+ * @returns Token string or null
1264
+ */
1265
+ extractInjectToken(decorator) {
1266
+ const callExpr = decorator.getCallExpression();
1267
+ if (!callExpr) return null;
1268
+ const args = callExpr.getArguments();
1269
+ if (args.length === 0) return null;
1270
+ return args[0].getText().replace(/['"]/g, "");
1271
+ }
1272
+ /**
1273
+ * Check if type should be skipped (any/unknown types)
1274
+ * Implements FR-09: Skip dependencies whose type resolves to any/unknown
1275
+ * @param typeText Type text to check
1276
+ * @returns True if should be skipped
1277
+ */
1278
+ shouldSkipType(typeText) {
1279
+ return [
1280
+ "any",
1281
+ "unknown",
1282
+ "object",
1283
+ "Object"
1284
+ ].includes(typeText);
1285
+ }
1286
+ /**
1287
+ * Check if type is primitive and should be skipped
1288
+ * @param typeText Type text to check
1289
+ * @returns True if primitive type
1290
+ */
1291
+ isPrimitiveType(typeText) {
1292
+ return [
1293
+ "string",
1294
+ "number",
1295
+ "boolean",
1296
+ "symbol",
1297
+ "bigint",
1298
+ "undefined",
1299
+ "null"
1300
+ ].includes(typeText);
1301
+ }
1302
+ /**
1303
+ * Extract parameter decorators from constructor parameter
1304
+ * Implements FR-04: Parameter decorator handling (@Optional, @Self, @SkipSelf, @Host)
1305
+ * Optimized for performance with early returns and minimal object allocation
1306
+ * @param param ts-morph ParameterDeclaration
1307
+ * @returns EdgeFlags object with detected decorators
1308
+ */
1309
+ extractParameterDecorators(param) {
1310
+ return this.analyzeParameterDecorators(param, this._options.includeDecorators);
1311
+ }
1312
+ /**
1313
+ * Extract dependencies from inject() function calls in class properties
1314
+ * Implements TDD Cycle 2.1: Modern Angular inject() pattern detection
1315
+ * @param classDeclaration ts-morph ClassDeclaration
1316
+ * @returns Array of parsed dependencies from inject() calls
1317
+ */
1318
+ extractInjectFunctionDependencies(classDeclaration) {
1319
+ const dependencies = [];
1320
+ try {
1321
+ const properties = classDeclaration.getProperties();
1322
+ for (const property of properties) {
1323
+ const dependency = this.parseInjectProperty(property);
1324
+ if (dependency) dependencies.push(dependency);
1325
+ }
1326
+ } catch (error) {
1327
+ if (this._options.verbose) {
1328
+ const className = classDeclaration.getName() || "unknown";
1329
+ const filePath = classDeclaration.getSourceFile().getFilePath();
1330
+ console.warn(`Warning: Failed to extract inject() dependencies for class '${className}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
1331
+ }
1332
+ }
1333
+ return dependencies;
1334
+ }
1335
+ /**
1336
+ * Parse a property declaration for inject() function calls
1337
+ * @param property ts-morph PropertyDeclaration
1338
+ * @returns ParsedDependency if inject() call found, null otherwise
1339
+ */
1340
+ parseInjectProperty(property) {
1341
+ try {
1342
+ const initializer = property.getInitializer();
1343
+ if (!initializer) return null;
1344
+ if (initializer.getKind() !== ts_morph.SyntaxKind.CallExpression) return null;
1345
+ const callExpression = initializer;
1346
+ const expression = callExpression.getExpression();
1347
+ if (expression.getKind() !== ts_morph.SyntaxKind.Identifier) return null;
1348
+ if (expression.getText() !== "inject") return null;
1349
+ if (!this.isAngularInjectImported(property.getSourceFile())) return null;
1350
+ const args = callExpression.getArguments();
1351
+ if (args.length === 0) return null;
1352
+ const token = args[0].getText().replace(/['"]/g, "");
1353
+ if (this.shouldSkipType(token) || this.isPrimitiveType(token)) return null;
1354
+ let flags = {};
1355
+ if (args.length > 1 && this._options.includeDecorators) flags = this.parseInjectOptions(args[1]);
1356
+ const propertyName = property.getName() || "unknown";
1357
+ return {
1358
+ token,
1359
+ flags,
1360
+ parameterName: propertyName
1361
+ };
1362
+ } catch (error) {
1363
+ if (this._options.verbose) {
1364
+ const propertyName = property.getName() || "unknown";
1365
+ const filePath = property.getSourceFile().getFilePath();
1366
+ console.warn(`Warning: Failed to parse inject() property '${propertyName}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
1367
+ }
1368
+ return null;
1369
+ }
1370
+ }
1371
+ /**
1372
+ * Parse options object from inject() function call
1373
+ * @param optionsArg Options argument from inject() call
1374
+ * @returns EdgeFlags object with parsed options
1375
+ */
1376
+ parseInjectOptions(optionsArg) {
1377
+ const flags = {};
1378
+ try {
1379
+ if (optionsArg.getKind() !== ts_morph.SyntaxKind.ObjectLiteralExpression) return flags;
1380
+ const properties = optionsArg.getProperties();
1381
+ const supportedOptions = new Set([
1382
+ "optional",
1383
+ "self",
1384
+ "skipSelf",
1385
+ "host"
1386
+ ]);
1387
+ for (const prop of properties) {
1388
+ if (prop.getKind() !== ts_morph.SyntaxKind.PropertyAssignment) continue;
1389
+ const propertyAssignment = prop;
1390
+ const name = propertyAssignment.getName();
1391
+ if (!supportedOptions.has(name)) {
1392
+ if (this._options.verbose) console.warn(`Unknown inject() option: '${name}' - ignoring`);
1393
+ continue;
1394
+ }
1395
+ const initializer = propertyAssignment.getInitializer();
1396
+ if (initializer && initializer.getText() === "true") switch (name) {
1397
+ case "optional":
1398
+ flags.optional = true;
1399
+ break;
1400
+ case "self":
1401
+ flags.self = true;
1402
+ break;
1403
+ case "skipSelf":
1404
+ flags.skipSelf = true;
1405
+ break;
1406
+ case "host":
1407
+ flags.host = true;
1408
+ break;
1409
+ }
1410
+ }
1411
+ } catch (error) {
1412
+ if (this._options.verbose) console.warn(`Warning: Failed to parse inject() options: ${error instanceof Error ? error.message : String(error)}`);
1413
+ }
1414
+ return flags;
1415
+ }
1416
+ /**
1417
+ * Analyze parameter decorators for TDD Cycle 1.1
1418
+ * Legacy parameter decorator detection method for @Optional, @Self, @SkipSelf, @Host
1419
+ * @param parameter ParameterDeclaration to analyze
1420
+ * @param includeDecorators Whether to include decorators in analysis
1421
+ * @returns EdgeFlags object with detected decorators
1422
+ */
1423
+ analyzeParameterDecorators(parameter, includeDecorators, verboseStats) {
1424
+ if (!includeDecorators) return {};
1425
+ const decorators = parameter.getDecorators();
1426
+ if (decorators.length === 0) return {};
1427
+ const flags = {};
1428
+ try {
1429
+ const supportedDecorators = new Set([
1430
+ "Optional",
1431
+ "Self",
1432
+ "SkipSelf",
1433
+ "Host"
1434
+ ]);
1435
+ for (const decorator of decorators) {
1436
+ const decoratorName = this.getDecoratorName(decorator);
1437
+ if (decoratorName === "Inject") continue;
1438
+ if (supportedDecorators.has(decoratorName)) switch (decoratorName) {
1439
+ case "Optional":
1440
+ flags.optional = true;
1441
+ break;
1442
+ case "Self":
1443
+ flags.self = true;
1444
+ break;
1445
+ case "SkipSelf":
1446
+ flags.skipSelf = true;
1447
+ break;
1448
+ case "Host":
1449
+ flags.host = true;
1450
+ break;
1451
+ }
1452
+ else if (decoratorName) {
1453
+ if (verboseStats) verboseStats.skippedDecorators.push({
1454
+ name: decoratorName,
1455
+ reason: "Unknown or unsupported decorator"
1456
+ });
1457
+ console.warn(`Unknown or unsupported decorator: @${decoratorName}() - This decorator is not recognized as an Angular DI decorator and will be ignored.`);
1458
+ }
1459
+ }
1460
+ } catch (error) {
1461
+ if (this._options.verbose) {
1462
+ const paramName = parameter.getName();
1463
+ const filePath = parameter.getSourceFile().getFilePath();
1464
+ console.warn(`Warning: Failed to extract decorators for parameter '${paramName}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
1465
+ }
1466
+ return {};
1467
+ }
1468
+ return flags;
1469
+ }
1470
+ /**
1471
+ * Check if inject() function is imported from @angular/core
1472
+ * Prevents false positives from custom inject() functions
1473
+ * @param sourceFile Source file to check imports
1474
+ * @returns True if Angular inject is imported
1475
+ */
1476
+ isAngularInjectImported(sourceFile) {
1477
+ try {
1478
+ const importDeclarations = sourceFile.getImportDeclarations();
1479
+ for (const importDecl of importDeclarations) if (importDecl.getModuleSpecifierValue() === "@angular/core") {
1480
+ const namedImports = importDecl.getNamedImports();
1481
+ for (const namedImport of namedImports) {
1482
+ const importName = namedImport.getName();
1483
+ const alias = namedImport.getAliasNode();
1484
+ if (importName === "inject" || alias && alias.getText() === "inject") return true;
1485
+ }
1486
+ }
1487
+ return false;
1488
+ } catch (error) {
1489
+ if (this._options.verbose) console.warn(`Warning: Could not verify inject() import in ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`);
1490
+ return true;
1491
+ }
1492
+ }
1493
+ /**
1494
+ * Analyze inject() function call expression to extract token and options
1495
+ * Implements TDD Cycle 2.1 - Modern Angular inject() pattern support
1496
+ * @param expression Expression to analyze (should be a CallExpression)
1497
+ * @returns ParameterAnalysisResult or null if not a valid inject() call
1498
+ */
1499
+ analyzeInjectCall(expression) {
1500
+ if (!expression) return null;
1501
+ if (expression.getKind() !== ts_morph.SyntaxKind.CallExpression) return null;
1502
+ const callExpression = expression;
1503
+ try {
1504
+ const callIdentifier = callExpression.getExpression();
1505
+ if (callIdentifier.getKind() !== ts_morph.SyntaxKind.Identifier) return null;
1506
+ if (callIdentifier.getText() !== "inject") return null;
1507
+ const sourceFile = expression.getSourceFile();
1508
+ if (!this.isAngularInjectImported(sourceFile)) return null;
1509
+ const args = callExpression.getArguments();
1510
+ if (args.length === 0) {
1511
+ if (this._options.verbose) console.warn("inject() called without token parameter - skipping");
1512
+ return null;
1513
+ }
1514
+ const tokenArg = args[0];
1515
+ let token;
1516
+ if (tokenArg.getKind() === ts_morph.SyntaxKind.StringLiteral) {
1517
+ token = tokenArg.getText().slice(1, -1);
1518
+ if (!token) {
1519
+ if (this._options.verbose) console.warn("inject() called with empty string token - skipping");
1520
+ return null;
1521
+ }
1522
+ } else if (tokenArg.getKind() === ts_morph.SyntaxKind.Identifier) {
1523
+ token = tokenArg.getText();
1524
+ if (token === "undefined" || token === "null") {
1525
+ if (this._options.verbose) console.warn(`inject() called with ${token} token - skipping`);
1526
+ return null;
1527
+ }
1528
+ } else if (tokenArg.getKind() === ts_morph.SyntaxKind.NullKeyword) {
1529
+ if (this._options.verbose) console.warn("inject() called with null token - skipping");
1530
+ return null;
1531
+ } else {
1532
+ token = tokenArg.getText();
1533
+ if (!token) {
1534
+ if (this._options.verbose) console.warn("inject() called with invalid token expression - skipping");
1535
+ return null;
1536
+ }
1537
+ }
1538
+ let flags = {};
1539
+ if (args.length > 1 && this._options.includeDecorators) {
1540
+ const optionsArg = args[1];
1541
+ if (optionsArg.getKind() === ts_morph.SyntaxKind.ObjectLiteralExpression) flags = this.parseInjectOptions(optionsArg);
1542
+ else if (optionsArg.getKind() !== ts_morph.SyntaxKind.NullKeyword && optionsArg.getKind() !== ts_morph.SyntaxKind.UndefinedKeyword) {
1543
+ if (this._options.verbose) console.warn(`inject() called with invalid options type: ${optionsArg.getKindName()} - expected object literal`);
1544
+ }
1545
+ }
1546
+ return {
1547
+ token,
1548
+ flags,
1549
+ source: "inject"
1550
+ };
1551
+ } catch (error) {
1552
+ if (this._options.verbose) {
1553
+ const filePath = expression.getSourceFile().getFilePath();
1554
+ console.warn(`Warning: Failed to analyze inject() call in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
1555
+ }
1556
+ return null;
1557
+ }
1558
+ }
1559
+ /**
1560
+ * Collect verbose statistics for decorator analysis
1561
+ * @param param Parameter declaration being analyzed
1562
+ * @param dependency Parsed dependency result
1563
+ * @param verboseStats Statistics object to update
1564
+ */
1565
+ collectVerboseStats(param, dependency, verboseStats) {
1566
+ const paramName = param.getName();
1567
+ console.log(`Parameter: ${paramName}`);
1568
+ console.log(` Token: ${dependency.token}`);
1569
+ if (Object.keys(dependency.flags || {}).length > 0) {
1570
+ verboseStats.parametersWithDecorators++;
1571
+ if (dependency.flags?.optional) verboseStats.decoratorCounts.optional++;
1572
+ if (dependency.flags?.self) verboseStats.decoratorCounts.self++;
1573
+ if (dependency.flags?.skipSelf) verboseStats.decoratorCounts.skipSelf++;
1574
+ if (dependency.flags?.host) verboseStats.decoratorCounts.host++;
1575
+ const decorators = param.getDecorators();
1576
+ let hasLegacyDecorators = false;
1577
+ let hasInjectPattern = false;
1578
+ this.analyzeParameterDecorators(param, true, verboseStats);
1579
+ for (const decorator of decorators) {
1580
+ const decoratorName = this.getDecoratorName(decorator);
1581
+ if ([
1582
+ "Optional",
1583
+ "Self",
1584
+ "SkipSelf",
1585
+ "Host"
1586
+ ].includes(decoratorName)) {
1587
+ hasLegacyDecorators = true;
1588
+ console.log(` Legacy decorator: @${decoratorName}`);
1589
+ }
1590
+ }
1591
+ const initializer = param.getInitializer();
1592
+ if (initializer && initializer.getKind() === ts_morph.SyntaxKind.CallExpression) {
1593
+ const expression = initializer.getExpression();
1594
+ if (expression.getKind() === ts_morph.SyntaxKind.Identifier) {
1595
+ if (expression.getText() === "inject") {
1596
+ hasInjectPattern = true;
1597
+ verboseStats.injectPatternsUsed++;
1598
+ const flagsStr = JSON.stringify(dependency.flags);
1599
+ console.log(` inject() options: ${flagsStr}`);
1600
+ }
1601
+ }
1602
+ }
1603
+ if (hasLegacyDecorators) {
1604
+ verboseStats.legacyDecoratorsUsed++;
1605
+ if (hasInjectPattern) {
1606
+ console.log(" Decorator Precedence Analysis");
1607
+ console.log(" Legacy decorators take precedence over inject() options");
1608
+ const appliedFlags = Object.keys(dependency.flags || {}).filter((key) => dependency.flags?.[key] === true).map((key) => `@${key.charAt(0).toUpperCase() + key.slice(1)}`).join(", ");
1609
+ console.log(` Applied: ${appliedFlags}`);
1610
+ const injectResult = this.analyzeInjectCall(initializer);
1611
+ if (injectResult && Object.keys(injectResult.flags).length > 0) {
1612
+ const overriddenFlags = JSON.stringify(injectResult.flags);
1613
+ console.log(` Overridden inject() options: ${overriddenFlags}`);
1614
+ }
1615
+ const finalFlags = JSON.stringify(dependency.flags);
1616
+ console.log(` Final flags: ${finalFlags}`);
1617
+ }
1618
+ }
1619
+ } else {
1620
+ verboseStats.parametersWithoutDecorators++;
1621
+ console.log(" No decorators detected");
1622
+ }
1623
+ }
1624
+ /**
1625
+ * Output comprehensive verbose analysis summary
1626
+ * @param dependencies All parsed dependencies
1627
+ * @param verboseStats Collected statistics
1628
+ * @param classDeclaration Class being analyzed
1629
+ */
1630
+ outputVerboseAnalysis(dependencies, verboseStats, classDeclaration) {
1631
+ if (!this._options.verbose) return;
1632
+ if (this._options.includeDecorators) {
1633
+ console.log("=== Decorator Statistics ===");
1634
+ console.log(`Total decorators detected: ${verboseStats.decoratorCounts.optional + verboseStats.decoratorCounts.self + verboseStats.decoratorCounts.skipSelf + verboseStats.decoratorCounts.host}`);
1635
+ if (verboseStats.decoratorCounts.optional > 0) console.log(`@Optional: ${verboseStats.decoratorCounts.optional}`);
1636
+ if (verboseStats.decoratorCounts.self > 0) console.log(`@Self: ${verboseStats.decoratorCounts.self}`);
1637
+ if (verboseStats.decoratorCounts.skipSelf > 0) console.log(`@SkipSelf: ${verboseStats.decoratorCounts.skipSelf}`);
1638
+ if (verboseStats.decoratorCounts.host > 0) console.log(`@Host: ${verboseStats.decoratorCounts.host}`);
1639
+ console.log(`Parameters with decorators: ${verboseStats.parametersWithDecorators}`);
1640
+ console.log(`Parameters without decorators: ${verboseStats.parametersWithoutDecorators}`);
1641
+ if (verboseStats.injectPatternsUsed > 0) {
1642
+ console.log("inject() Pattern Analysis");
1643
+ for (const dep of dependencies) if (dep.parameterName) {
1644
+ const constructors = classDeclaration.getConstructors();
1645
+ if (constructors.length > 0) {
1646
+ const param = constructors[0].getParameters().find((p) => p.getName() === dep.parameterName);
1647
+ if (param) {
1648
+ const initializer = param.getInitializer();
1649
+ if (initializer) {
1650
+ const injectResult = this.analyzeInjectCall(initializer);
1651
+ if (injectResult) {
1652
+ if (injectResult.token.startsWith("\"") && injectResult.token.endsWith("\"")) console.log(`String token: ${injectResult.token}`);
1653
+ else console.log(`Service token: ${injectResult.token}`);
1654
+ if (Object.keys(injectResult.flags).length > 0) {
1655
+ const flagsStr = JSON.stringify(injectResult.flags);
1656
+ console.log(`inject() options detected: ${flagsStr}`);
1657
+ } else console.log("inject() with no options");
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+ }
1664
+ if (verboseStats.skippedDecorators.length > 0) {
1665
+ console.log("Skipped Decorators");
1666
+ for (const skipped of verboseStats.skippedDecorators) {
1667
+ console.log(`${skipped.name}`);
1668
+ console.log(`Reason: ${skipped.reason}`);
1669
+ }
1670
+ console.log(`Total skipped: ${verboseStats.skippedDecorators.length}`);
1671
+ }
1672
+ console.log("Performance Metrics");
1673
+ console.log(`Decorator processing time: ${verboseStats.totalProcessingTime.toFixed(2)}ms`);
1674
+ console.log(`Total parameters analyzed: ${verboseStats.totalParameters}`);
1675
+ if (verboseStats.totalParameters > 0) {
1676
+ const avgTime = verboseStats.totalProcessingTime / verboseStats.totalParameters;
1677
+ console.log(`Average time per parameter: ${avgTime.toFixed(3)}ms`);
1678
+ }
1679
+ console.log("=== Analysis Summary ===");
1680
+ console.log(`Total dependencies: ${dependencies.length}`);
1681
+ console.log(`With decorator flags: ${verboseStats.parametersWithDecorators}`);
1682
+ console.log(`Without decorator flags: ${verboseStats.parametersWithoutDecorators}`);
1683
+ console.log(`Legacy decorators used: ${verboseStats.legacyDecoratorsUsed}`);
1684
+ console.log(`inject() patterns used: ${verboseStats.injectPatternsUsed}`);
1685
+ if (verboseStats.skippedDecorators.length > 0) console.log(`Unknown decorators skipped: ${verboseStats.skippedDecorators.length}`);
1686
+ if (verboseStats.parametersWithDecorators > 0) {
1687
+ console.log("Flags distribution:");
1688
+ if (verboseStats.decoratorCounts.optional > 0) console.log(`optional: ${verboseStats.decoratorCounts.optional}`);
1689
+ if (verboseStats.decoratorCounts.self > 0) console.log(`self: ${verboseStats.decoratorCounts.self}`);
1690
+ if (verboseStats.decoratorCounts.skipSelf > 0) console.log(`skipSelf: ${verboseStats.decoratorCounts.skipSelf}`);
1691
+ if (verboseStats.decoratorCounts.host > 0) console.log(`host: ${verboseStats.decoratorCounts.host}`);
1692
+ }
1693
+ }
1694
+ }
1695
+ };
1696
+
1697
+ //#endregion
1698
+ //#region src/formatters/json-formatter.ts
1699
+ /**
1700
+ * JSON formatter for dependency graph output
1701
+ * Produces pretty-printed JSON with 2-space indentation
1702
+ */
1703
+ var JsonFormatter = class {
1704
+ /**
1705
+ * Create a new JSON formatter
1706
+ * @param logger Optional Logger instance for verbose mode
1707
+ */
1708
+ constructor(logger) {
1709
+ this._logger = logger;
1710
+ }
1711
+ /**
1712
+ * Format a dependency graph as JSON
1713
+ * @param graph The dependency graph to format
1714
+ * @returns Pretty-printed JSON string
1715
+ */
1716
+ format(graph) {
1717
+ this._logger?.time("json-format");
1718
+ this._logger?.info(LogCategory.PERFORMANCE, "Generating JSON output", {
1719
+ nodeCount: graph.nodes.length,
1720
+ edgeCount: graph.edges.length
1721
+ });
1722
+ const result = JSON.stringify(graph, null, 2);
1723
+ const elapsed = this._logger?.timeEnd("json-format") ?? 0;
1724
+ this._logger?.info(LogCategory.PERFORMANCE, "JSON output complete", {
1725
+ outputSize: result.length,
1726
+ elapsed
1727
+ });
1728
+ return result;
1729
+ }
1730
+ };
1731
+
1732
+ //#endregion
1733
+ //#region src/formatters/mermaid-formatter.ts
1734
+ /**
1735
+ * Mermaid formatter for dependency graph output
1736
+ * Produces flowchart LR syntax compatible with Mermaid Live Editor
1737
+ */
1738
+ var MermaidFormatter = class {
1739
+ /**
1740
+ * Create a new Mermaid formatter
1741
+ * @param logger Optional Logger instance for verbose mode
1742
+ */
1743
+ constructor(logger) {
1744
+ this._logger = logger;
1745
+ }
1746
+ /**
1747
+ * Format a dependency graph as Mermaid flowchart
1748
+ * @param graph The dependency graph to format
1749
+ * @returns Mermaid flowchart string
1750
+ */
1751
+ format(graph) {
1752
+ this._logger?.time("mermaid-format");
1753
+ this._logger?.info(LogCategory.PERFORMANCE, "Generating Mermaid output", {
1754
+ nodeCount: graph.nodes.length,
1755
+ edgeCount: graph.edges.length
1756
+ });
1757
+ if (graph.nodes.length === 0) {
1758
+ const result$1 = "flowchart LR\n %% Empty graph - no nodes to display";
1759
+ const elapsed$1 = this._logger?.timeEnd("mermaid-format") ?? 0;
1760
+ this._logger?.info(LogCategory.PERFORMANCE, "Mermaid output complete", {
1761
+ outputSize: 51,
1762
+ elapsed: elapsed$1
1763
+ });
1764
+ return result$1;
1765
+ }
1766
+ const lines = ["flowchart LR"];
1767
+ for (const edge of graph.edges) {
1768
+ const fromNode = this.sanitizeNodeName(edge.from);
1769
+ const toNode = this.sanitizeNodeName(edge.to);
1770
+ if (edge.isCircular) lines.push(` ${fromNode} -.->|circular| ${toNode}`);
1771
+ else lines.push(` ${fromNode} --> ${toNode}`);
1772
+ }
1773
+ if (graph.circularDependencies.length > 0) {
1774
+ lines.push("");
1775
+ lines.push(" %% Circular Dependencies Detected:");
1776
+ for (const cycle of graph.circularDependencies) lines.push(` %% ${cycle.join(" -> ")} -> ${cycle[0]}`);
1777
+ }
1778
+ const result = lines.join("\n");
1779
+ const elapsed = this._logger?.timeEnd("mermaid-format") ?? 0;
1780
+ this._logger?.info(LogCategory.PERFORMANCE, "Mermaid output complete", {
1781
+ outputSize: result.length,
1782
+ elapsed
1783
+ });
1784
+ return result;
1785
+ }
1786
+ /**
1787
+ * Sanitize node names for Mermaid compatibility
1788
+ * Replaces special characters that break Mermaid syntax
1789
+ * @param nodeName The original node name
1790
+ * @returns Sanitized node name
1791
+ */
1792
+ sanitizeNodeName(nodeName) {
1793
+ return nodeName.replace(/[.-]/g, "_").replace(/[^a-zA-Z0-9_]/g, "");
1794
+ }
1795
+ };
1796
+
1797
+ //#endregion
1798
+ //#region src/cli/index.ts
1799
+ /**
1800
+ * ng-di-graph CLI entry point
1801
+ * Supports Node.js (via tsx) execution
1802
+ */
1803
+ const MIN_NODE_MAJOR_VERSION = 20;
1804
+ function enforceMinimumNodeVersion() {
1805
+ const nodeVersion = process.versions.node;
1806
+ if (!nodeVersion) return;
1807
+ const [majorSegment] = nodeVersion.split(".");
1808
+ const major = Number(majorSegment);
1809
+ if (Number.isNaN(major) || major >= MIN_NODE_MAJOR_VERSION) return;
1810
+ const runtimeLabel = process.versions.bun ? `Bun (Node compatibility ${nodeVersion})` : `Node.js ${nodeVersion}`;
1811
+ console.error([
1812
+ `ng-di-graph requires Node.js >= ${MIN_NODE_MAJOR_VERSION}.0.0.`,
1813
+ `Detected runtime: ${runtimeLabel}.`,
1814
+ "Upgrade to Node.js 20.x LTS (match `.node-version` / mise config) before running ng-di-graph."
1815
+ ].join("\n"));
1816
+ process.exit(1);
1817
+ }
1818
+ enforceMinimumNodeVersion();
1819
+ const program = new commander.Command();
1820
+ program.name("ng-di-graph").description("Angular DI dependency graph CLI tool").version("0.1.0");
1821
+ program.option("-p, --project <path>", "tsconfig.json path", "./tsconfig.json").option("-f, --format <format>", "output format: json | mermaid", "json").option("-e, --entry <symbol...>", "starting nodes for sub-graph").option("-d, --direction <dir>", "filtering direction: upstream|downstream|both", "downstream").option("--include-decorators", "include Optional/Self/SkipSelf/Host flags", false).option("--out <file>", "output file (stdout if omitted)").option("-v, --verbose", "show detailed parsing information", false);
1822
+ program.action(async (options) => {
1823
+ try {
1824
+ if (options.direction && ![
1825
+ "upstream",
1826
+ "downstream",
1827
+ "both"
1828
+ ].includes(options.direction)) throw ErrorHandler.createError(`Invalid direction: ${options.direction}. Must be 'upstream', 'downstream', or 'both'`, "INVALID_ARGUMENTS");
1829
+ if (options.format && !["json", "mermaid"].includes(options.format)) throw ErrorHandler.createError(`Invalid format: ${options.format}. Must be 'json' or 'mermaid'`, "INVALID_ARGUMENTS");
1830
+ const cliOptions = {
1831
+ project: options.project,
1832
+ format: options.format,
1833
+ entry: options.entry,
1834
+ direction: options.direction,
1835
+ includeDecorators: options.includeDecorators,
1836
+ out: options.out,
1837
+ verbose: options.verbose
1838
+ };
1839
+ const logger = createLogger(cliOptions.verbose);
1840
+ if (logger) {
1841
+ logger.time("total-execution");
1842
+ logger.info(LogCategory.FILE_PROCESSING, "CLI execution started", {
1843
+ runtime: process.versions.bun ? "Bun" : "Node.js",
1844
+ version: process.versions.bun || process.versions.node,
1845
+ options: cliOptions
1846
+ });
1847
+ }
1848
+ if (cliOptions.verbose) {
1849
+ console.log("🔧 CLI Options:", JSON.stringify(cliOptions, null, 2));
1850
+ console.log(`🚀 Running with ${process.versions.bun ? "Bun" : "Node.js"} ${process.versions.bun || process.versions.node}`);
1851
+ }
1852
+ const parser = new AngularParser(cliOptions, logger);
1853
+ if (cliOptions.verbose) console.log("📂 Loading TypeScript project...");
1854
+ parser.loadProject();
1855
+ if (cliOptions.verbose) console.log("✅ Project loaded successfully");
1856
+ if (cliOptions.verbose) console.log("🔍 Parsing Angular classes...");
1857
+ const parsedClasses = await parser.parseClasses();
1858
+ if (cliOptions.verbose) console.log(`✅ Found ${parsedClasses.length} decorated classes`);
1859
+ if (cliOptions.verbose) console.log("🔗 Building dependency graph...");
1860
+ let graph = buildGraph(parsedClasses, logger);
1861
+ if (cliOptions.verbose) {
1862
+ console.log(`✅ Graph built: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
1863
+ if (graph.circularDependencies.length > 0) console.log(`⚠️ Detected ${graph.circularDependencies.length} circular dependencies`);
1864
+ }
1865
+ if (cliOptions.entry && cliOptions.entry.length > 0) {
1866
+ if (cliOptions.verbose) console.log(`🔍 Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
1867
+ graph = filterGraph(graph, cliOptions);
1868
+ if (cliOptions.verbose) console.log(`✅ Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
1869
+ }
1870
+ let formatter;
1871
+ if (cliOptions.format === "mermaid") formatter = new MermaidFormatter(logger);
1872
+ else formatter = new JsonFormatter(logger);
1873
+ const formattedOutput = formatter.format(graph);
1874
+ await new OutputHandler().writeOutput(formattedOutput, cliOptions.out);
1875
+ if (cliOptions.verbose && cliOptions.out) console.log(`✅ Output written to: ${cliOptions.out}`);
1876
+ if (logger) {
1877
+ const totalTime = logger.timeEnd("total-execution");
1878
+ const stats = logger.getStats();
1879
+ console.error("\n📊 Performance Summary:");
1880
+ console.error(` Total time: ${totalTime.toFixed(2)}ms`);
1881
+ console.error(` Peak memory: ${(stats.memoryUsage.peakUsage / 1024 / 1024).toFixed(2)}MB`);
1882
+ console.error(` Total logs: ${stats.totalLogs}`);
1883
+ }
1884
+ } catch (error) {
1885
+ if (error instanceof CliError) ErrorHandler.handleError(error, options.verbose);
1886
+ else if (error instanceof Error) {
1887
+ const cliError = ErrorHandler.createError(error.message, "INTERNAL_ERROR", void 0, { originalError: error.name });
1888
+ ErrorHandler.handleError(cliError, options.verbose);
1889
+ } else {
1890
+ const cliError = ErrorHandler.createError("An unexpected error occurred", "INTERNAL_ERROR", void 0, { error: String(error) });
1891
+ ErrorHandler.handleError(cliError, options.verbose);
1892
+ }
1893
+ }
1894
+ });
1895
+ process.on("unhandledRejection", (reason, promise) => {
1896
+ const error = ErrorHandler.createError(`Unhandled promise rejection: ${reason}`, "INTERNAL_ERROR", void 0, { promise: String(promise) });
1897
+ ErrorHandler.handleError(error, false);
1898
+ });
1899
+ process.on("uncaughtException", (error) => {
1900
+ const cliError = ErrorHandler.createError(`Uncaught exception: ${error.message}`, "INTERNAL_ERROR", void 0, { stack: error.stack });
1901
+ ErrorHandler.handleError(cliError, true);
1902
+ });
1903
+ const argv = process.argv.length <= 2 ? [...process.argv, "--help"] : process.argv;
1904
+ program.parse(argv);
1905
+
1906
+ //#endregion
1907
+ //# sourceMappingURL=index.cjs.map