ng-di-graph 0.3.0 → 0.4.1

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.
@@ -510,32 +510,37 @@ function buildGraph(parsedClasses, logger) {
510
510
  * @param adjacencyList The adjacency list for traversal
511
511
  * @param resultSet The set to collect traversal results
512
512
  * @param options CLI options for verbose output
513
+ * @param logger Optional logger for structured output
513
514
  */
514
- function validateAndTraverseEntryPoints(entryPoints, graph, adjacencyList, resultSet, options) {
515
+ function validateAndTraverseEntryPoints(entryPoints, graph, adjacencyList, resultSet, options, logger) {
515
516
  for (const entryPoint of entryPoints) if (graph.nodes.some((n) => n.id === entryPoint)) traverseFromEntry(entryPoint, adjacencyList, resultSet);
516
- else if (options.verbose) console.warn(`Entry point '${entryPoint}' not found in graph`);
517
+ else if (options.verbose && logger) {
518
+ const message = `Entry point '${entryPoint}' not found in graph`;
519
+ logger.warn(LogCategory.FILTERING, message, { entryPoint });
520
+ }
517
521
  }
518
522
  /**
519
523
  * Filters a graph based on entry points and traversal direction
520
524
  * @param graph The graph to filter
521
525
  * @param options CLI options containing entry points and direction
526
+ * @param logger Optional logger for structured output
522
527
  * @returns Filtered graph containing only nodes reachable from entry points
523
528
  */
524
- function filterGraph(graph, options) {
529
+ function filterGraph(graph, options, logger) {
525
530
  if (!options.entry || options.entry.length === 0) return graph;
526
531
  const includedNodeIds = /* @__PURE__ */ new Set();
527
532
  if (options.direction === "both") {
528
533
  const upstreamAdjacencyList = buildAdjacencyList(graph, "upstream");
529
534
  const upstreamNodes = /* @__PURE__ */ new Set();
530
- validateAndTraverseEntryPoints(options.entry, graph, upstreamAdjacencyList, upstreamNodes, options);
535
+ validateAndTraverseEntryPoints(options.entry, graph, upstreamAdjacencyList, upstreamNodes, options, logger);
531
536
  const downstreamAdjacencyList = buildAdjacencyList(graph, "downstream");
532
537
  const downstreamNodes = /* @__PURE__ */ new Set();
533
- validateAndTraverseEntryPoints(options.entry, graph, downstreamAdjacencyList, downstreamNodes, options);
538
+ validateAndTraverseEntryPoints(options.entry, graph, downstreamAdjacencyList, downstreamNodes, options, logger);
534
539
  const combinedNodes = new Set([...upstreamNodes, ...downstreamNodes]);
535
540
  for (const nodeId of combinedNodes) includedNodeIds.add(nodeId);
536
541
  } else {
537
542
  const adjacencyList = buildAdjacencyList(graph, options.direction);
538
- validateAndTraverseEntryPoints(options.entry, graph, adjacencyList, includedNodeIds, options);
543
+ validateAndTraverseEntryPoints(options.entry, graph, adjacencyList, includedNodeIds, options, logger);
539
544
  }
540
545
  const filteredNodes = graph.nodes.filter((node) => includedNodeIds.has(node.id));
541
546
  const filteredEdges = graph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to));
@@ -554,9 +559,12 @@ function filterGraph(graph, options) {
554
559
  }
555
560
  return true;
556
561
  });
557
- if (options.verbose) {
558
- console.log(`Filtered graph: ${filteredNodes.length} nodes, ${filteredEdges.length} edges`);
559
- console.log(`Entry points: ${options.entry.join(", ")}`);
562
+ if (options.verbose && logger) {
563
+ logger.info(LogCategory.FILTERING, "Filtered graph summary", {
564
+ nodeCount: filteredNodes.length,
565
+ edgeCount: filteredEdges.length
566
+ });
567
+ logger.debug(LogCategory.FILTERING, "Entry points applied", { entryPoints: options.entry });
560
568
  }
561
569
  return {
562
570
  nodes: filteredNodes,
@@ -663,6 +671,21 @@ var AngularParser = class {
663
671
  totalCount: 0
664
672
  };
665
673
  }
674
+ verboseInfo(category, message, context) {
675
+ if (!this._options.verbose || !this._logger) return;
676
+ this._logger.info(category, message, context);
677
+ }
678
+ verboseDebug(category, message, context) {
679
+ if (!this._options.verbose || !this._logger) return;
680
+ this._logger.debug(category, message, context);
681
+ }
682
+ warn(category, message, context) {
683
+ if (this._logger) {
684
+ this._logger.warn(category, message, context);
685
+ return;
686
+ }
687
+ ErrorHandler.warn(message, context?.filePath);
688
+ }
666
689
  /**
667
690
  * Reset global warning deduplication state (useful for testing)
668
691
  */
@@ -698,8 +721,23 @@ var AngularParser = class {
698
721
  this._structuredWarnings.categories[category].push(warning);
699
722
  this._structuredWarnings.totalCount++;
700
723
  const location = warning.line ? `${warning.file}:${warning.line}:${warning.column}` : warning.file;
701
- console.warn(`[${warning.severity.toUpperCase()}] ${warning.message} (${location})`);
702
- if (warning.suggestion && this._options.verbose) console.warn(` Suggestion: ${warning.suggestion}`);
724
+ const formattedWarning = `[${warning.severity.toUpperCase()}] ${warning.message} (${location})`;
725
+ if (this._logger) {
726
+ this._logger.warn(LogCategory.ERROR_RECOVERY, formattedWarning, {
727
+ category,
728
+ filePath: warning.file,
729
+ lineNumber: warning.line,
730
+ suggestion: warning.suggestion
731
+ });
732
+ if (warning.suggestion && this._options.verbose) this._logger.info(LogCategory.ERROR_RECOVERY, warning.suggestion, {
733
+ category,
734
+ filePath: warning.file,
735
+ lineNumber: warning.line
736
+ });
737
+ } else {
738
+ ErrorHandler.warn(formattedWarning, warning.file);
739
+ if (warning.suggestion && this._options.verbose) ErrorHandler.warn(warning.suggestion, warning.file);
740
+ }
703
741
  GLOBAL_WARNING_KEYS.add(warnKey);
704
742
  }
705
743
  }
@@ -723,12 +761,6 @@ var AngularParser = class {
723
761
  }
724
762
  this._project = new ts_morph.Project({ tsConfigFilePath: tsConfigPath });
725
763
  if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
726
- this._project.getSourceFiles();
727
- const diagnostics = this._project.getProgram().getConfigFileParsingDiagnostics();
728
- if (diagnostics.length > 0) {
729
- const message = diagnostics[0].getMessageText();
730
- throw ErrorHandler.createError(`TypeScript configuration error: ${message}`, "PROJECT_LOAD_FAILED", this._options.project, { diagnosticCount: diagnostics.length });
731
- }
732
764
  } catch (error) {
733
765
  if (error instanceof CliError) throw error;
734
766
  if (error instanceof Error) {
@@ -758,6 +790,103 @@ var AngularParser = class {
758
790
  return this.findDecoratedClasses();
759
791
  }
760
792
  /**
793
+ * Retrieve source files, optionally filtering by user-provided file paths
794
+ */
795
+ getTargetSourceFiles() {
796
+ if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
797
+ if (!this._options.files || this._options.files.length === 0) return this._project.getSourceFiles();
798
+ const projectDir = node_path.default.dirname(node_path.default.resolve(this._options.project));
799
+ const targetPaths = this._options.files.map((filePath) => {
800
+ if (node_path.default.isAbsolute(filePath)) {
801
+ const normalized = node_path.default.normalize(filePath);
802
+ return {
803
+ raw: filePath,
804
+ normalized,
805
+ isDirectory: (0, node_fs.existsSync)(normalized) && (0, node_fs.statSync)(normalized).isDirectory()
806
+ };
807
+ }
808
+ const projectResolved = node_path.default.normalize(node_path.default.resolve(projectDir, filePath));
809
+ const cwdResolved = node_path.default.normalize(node_path.default.resolve(filePath));
810
+ const normalizedPath = (0, node_fs.existsSync)(projectResolved) ? projectResolved : cwdResolved;
811
+ return {
812
+ raw: filePath,
813
+ normalized: normalizedPath,
814
+ isDirectory: (0, node_fs.existsSync)(normalizedPath) && (0, node_fs.statSync)(normalizedPath).isDirectory()
815
+ };
816
+ });
817
+ const isFileMatch = (filePath, target) => {
818
+ if (target.isDirectory) {
819
+ const relative = node_path.default.relative(target.normalized, filePath);
820
+ return relative !== "" && !relative.startsWith("..") && !node_path.default.isAbsolute(relative);
821
+ }
822
+ return filePath === target.normalized;
823
+ };
824
+ const directoryTargets = targetPaths.filter((target) => target.isDirectory);
825
+ const fileTargets = targetPaths.filter((target) => !target.isDirectory);
826
+ const directoryAdds = [];
827
+ for (const target of directoryTargets) {
828
+ if (!(0, node_fs.existsSync)(target.normalized) || !(0, node_fs.statSync)(target.normalized).isDirectory()) continue;
829
+ const directoryGlob = `${target.normalized.replace(/\\/g, "/")}/**/*.{ts,tsx}`;
830
+ const added = this._project.addSourceFilesAtPaths(directoryGlob);
831
+ if (added.length > 0) {
832
+ directoryAdds.push({
833
+ target,
834
+ addedCount: added.length
835
+ });
836
+ this._logger?.warn(LogCategory.FILE_PROCESSING, "Added directory outside tsconfig scope", {
837
+ directory: target.raw,
838
+ addedCount: added.length
839
+ });
840
+ }
841
+ }
842
+ let sourceFiles = this._project.getSourceFiles();
843
+ let matchedFiles = sourceFiles.filter((sourceFile) => {
844
+ const filePath = node_path.default.normalize(sourceFile.getFilePath());
845
+ return targetPaths.some((target) => isFileMatch(filePath, target));
846
+ });
847
+ const missingFileTargets = fileTargets.filter((target) => matchedFiles.every((file) => !isFileMatch(node_path.default.normalize(file.getFilePath()), target)));
848
+ if (missingFileTargets.length > 0) {
849
+ const addedFiles = [];
850
+ for (const target of missingFileTargets) {
851
+ if (!(0, node_fs.existsSync)(target.normalized)) continue;
852
+ const added = this._project.addSourceFileAtPathIfExists(target.normalized);
853
+ if (added) {
854
+ addedFiles.push(added);
855
+ this._logger?.warn(LogCategory.FILE_PROCESSING, "Added file outside tsconfig scope", { filePath: target.raw });
856
+ }
857
+ }
858
+ if (addedFiles.length > 0 || directoryAdds.length > 0) {
859
+ sourceFiles = this._project.getSourceFiles();
860
+ matchedFiles = sourceFiles.filter((sourceFile) => {
861
+ const filePath = node_path.default.normalize(sourceFile.getFilePath());
862
+ return targetPaths.some((target) => isFileMatch(filePath, target));
863
+ });
864
+ }
865
+ }
866
+ const missingTargets = targetPaths.filter((target) => matchedFiles.every((file) => !isFileMatch(node_path.default.normalize(file.getFilePath()), target)));
867
+ if (missingTargets.length > 0) {
868
+ const emptyDirectories = missingTargets.filter((target) => target.isDirectory && (0, node_fs.existsSync)(target.normalized) && (0, node_fs.statSync)(target.normalized).isDirectory());
869
+ if (emptyDirectories.length > 0) {
870
+ const firstEmpty = emptyDirectories[0];
871
+ throw ErrorHandler.createError(`Directory "${firstEmpty.raw}" contained no TypeScript files`, "FILE_NOT_FOUND", firstEmpty.raw);
872
+ }
873
+ const missingList = missingTargets.map((target) => target.raw).join(", ");
874
+ throw ErrorHandler.createError(`Target file(s) not found in project: ${missingList}`, "FILE_NOT_FOUND", missingTargets[0]?.raw);
875
+ }
876
+ this._logger?.info(LogCategory.FILE_PROCESSING, "Applied file filter", {
877
+ targetCount: targetPaths.length,
878
+ matchedCount: matchedFiles.length
879
+ });
880
+ if (this._options.verbose) {
881
+ this._logger?.info(LogCategory.FILE_PROCESSING, `Filtering to specific file(s): ${matchedFiles.length}`, {
882
+ matchedCount: matchedFiles.length,
883
+ targetCount: targetPaths.length
884
+ });
885
+ for (const file of matchedFiles) this._logger?.debug(LogCategory.FILE_PROCESSING, "Including file", { filePath: file.getFilePath() });
886
+ }
887
+ return matchedFiles;
888
+ }
889
+ /**
761
890
  * Find all classes decorated with @Injectable, @Component, or @Directive
762
891
  * Implements FR-02: Decorated Class Collection
763
892
  * @returns Promise<ParsedClass[]> List of decorated classes
@@ -766,20 +895,18 @@ var AngularParser = class {
766
895
  if (!this._project) this.loadProject();
767
896
  if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
768
897
  const decoratedClasses = [];
769
- const sourceFiles = this._project.getSourceFiles();
898
+ const sourceFiles = this.getTargetSourceFiles();
770
899
  let processedFiles = 0;
771
900
  let skippedFiles = 0;
772
901
  this._circularTypeRefs.clear();
773
902
  this._logger?.time("findDecoratedClasses");
774
903
  this._logger?.info(LogCategory.FILE_PROCESSING, "Starting file processing", { fileCount: sourceFiles.length });
775
- if (this._options.verbose) console.log(`Processing ${sourceFiles.length} source files`);
904
+ this._logger?.info(LogCategory.FILE_PROCESSING, "Processing source files", { fileCount: sourceFiles.length });
776
905
  for (const sourceFile of sourceFiles) {
777
906
  const filePath = sourceFile.getFilePath();
778
907
  try {
779
- if (this._options.verbose) console.log(`🔍 Parsing file: ${filePath}`);
780
- this._logger?.debug(LogCategory.FILE_PROCESSING, "Processing file", { filePath });
908
+ this._logger?.debug(LogCategory.FILE_PROCESSING, "Parsing file", { filePath });
781
909
  const classes = sourceFile.getClasses();
782
- if (this._options.verbose) console.log(`File: ${filePath}, Classes: ${classes.length}`);
783
910
  this._logger?.debug(LogCategory.AST_ANALYSIS, "Analyzing classes in file", {
784
911
  filePath,
785
912
  classCount: classes.length
@@ -788,7 +915,6 @@ var AngularParser = class {
788
915
  const parsedClass = this.parseClassDeclaration(classDeclaration);
789
916
  if (parsedClass) {
790
917
  decoratedClasses.push(parsedClass);
791
- if (this._options.verbose) console.log(`Found decorated class: ${parsedClass.name} (${parsedClass.kind})`);
792
918
  this._logger?.info(LogCategory.AST_ANALYSIS, "Found decorated class", {
793
919
  className: parsedClass.name,
794
920
  kind: parsedClass.kind,
@@ -810,7 +936,10 @@ var AngularParser = class {
810
936
  ErrorHandler.warn(`Failed to parse file (skipping): ${error instanceof Error ? error.message : "Unknown error"}`, filePath);
811
937
  }
812
938
  }
813
- if (this._options.verbose) console.log(`✅ Processed ${processedFiles} files, skipped ${skippedFiles} files`);
939
+ this._logger?.info(LogCategory.FILE_PROCESSING, "File processing summary", {
940
+ processedFiles,
941
+ skippedFiles
942
+ });
814
943
  const elapsed = this._logger?.timeEnd("findDecoratedClasses") || 0;
815
944
  this._logger?.info(LogCategory.PERFORMANCE, "File processing complete", {
816
945
  totalClasses: decoratedClasses.length,
@@ -829,17 +958,26 @@ var AngularParser = class {
829
958
  parseClassDeclaration(classDeclaration) {
830
959
  const className = classDeclaration.getName();
831
960
  if (!className) {
832
- console.warn("Warning: Skipping anonymous class - classes must be named for dependency injection analysis");
961
+ const message = "Skipping anonymous class - classes must be named for dependency injection analysis";
962
+ if (this._logger) this._logger.warn(LogCategory.AST_ANALYSIS, message);
963
+ else ErrorHandler.warn(message);
833
964
  return null;
834
965
  }
835
966
  const decorators = classDeclaration.getDecorators();
836
967
  if (this._options.verbose) {
837
968
  const decoratorNames = decorators.map((d) => this.getDecoratorName(d)).join(", ");
838
- console.log(`Class: ${className}, Decorators: ${decorators.length} [${decoratorNames}]`);
969
+ this._logger?.debug(LogCategory.AST_ANALYSIS, "Decorator metadata", {
970
+ className,
971
+ decoratorCount: decorators.length,
972
+ decoratorNames
973
+ });
839
974
  }
840
975
  const angularDecorator = this.findAngularDecorator(decorators);
841
976
  if (!angularDecorator) {
842
- if (this._options.verbose && decorators.length > 0) console.log(` No Angular decorator found for ${className}`);
977
+ if (this._options.verbose && decorators.length > 0) this._logger?.debug(LogCategory.AST_ANALYSIS, "No Angular decorator found", {
978
+ className,
979
+ decoratorCount: decorators.length
980
+ });
843
981
  return null;
844
982
  }
845
983
  return {
@@ -921,14 +1059,14 @@ var AngularParser = class {
921
1059
  if (parent && parent.getKind() === ts_morph.SyntaxKind.CallExpression) {
922
1060
  const grandParent = parent.getParent();
923
1061
  if (grandParent && grandParent.getKind() === ts_morph.SyntaxKind.CallExpression) {
924
- console.warn("Warning: Skipping anonymous class - classes must be named for dependency injection analysis");
925
- if (this._options.verbose) console.log(` Anonymous class found in ${sourceFile.getFilePath()}`);
1062
+ this.warn(LogCategory.AST_ANALYSIS, "Skipping anonymous class - classes must be named for dependency injection analysis", { filePath: sourceFile.getFilePath() });
1063
+ this.verboseDebug(LogCategory.AST_ANALYSIS, `Anonymous class found in ${sourceFile.getFilePath()}`);
926
1064
  }
927
1065
  }
928
1066
  }
929
1067
  });
930
1068
  } catch (error) {
931
- if (this._options.verbose) console.log(` Could not detect anonymous classes in ${sourceFile.getFilePath()}: ${error}`);
1069
+ this.verboseDebug(LogCategory.AST_ANALYSIS, `Could not detect anonymous classes in ${sourceFile.getFilePath()}: ${error}`);
932
1070
  }
933
1071
  }
934
1072
  /**
@@ -957,11 +1095,11 @@ var AngularParser = class {
957
1095
  };
958
1096
  const startTime = performance.now();
959
1097
  if (this._options.verbose && this._options.includeDecorators) {
960
- console.log("=== Decorator Analysis ===");
1098
+ this.verboseInfo(LogCategory.AST_ANALYSIS, "=== Decorator Analysis ===");
961
1099
  const className = classDeclaration.getName() || "unknown";
962
- console.log(`Analyzing decorators for class: ${className}`);
1100
+ this.verboseInfo(LogCategory.AST_ANALYSIS, `Analyzing decorators for class: ${className}`, { className });
963
1101
  }
964
- if (this._options.verbose && !this._options.includeDecorators) console.log("Decorator analysis disabled - --include-decorators flag not set");
1102
+ if (this._options.verbose && !this._options.includeDecorators) this.verboseInfo(LogCategory.AST_ANALYSIS, "Decorator analysis disabled - --include-decorators flag not set");
965
1103
  const constructors = classDeclaration.getConstructors();
966
1104
  if (constructors.length > 0) {
967
1105
  const parameters = constructors[0].getParameters();
@@ -1015,7 +1153,7 @@ var AngularParser = class {
1015
1153
  * @returns Token string or null
1016
1154
  */
1017
1155
  handleGenericType(typeText, _filePath, _lineNumber, _columnNumber) {
1018
- if (this._options.verbose) console.log(`Processing generic type: ${typeText}`);
1156
+ this.verboseDebug(LogCategory.TYPE_RESOLUTION, `Processing generic type: ${typeText}`);
1019
1157
  return typeText;
1020
1158
  }
1021
1159
  /**
@@ -1102,7 +1240,10 @@ var AngularParser = class {
1102
1240
  */
1103
1241
  extractTypeTokenEnhanced(typeNode, filePath, lineNumber, columnNumber) {
1104
1242
  const typeText = typeNode.getText();
1105
- if (this._options.verbose) console.log(`Type resolution steps: Processing '${typeText}' at ${filePath}:${lineNumber}:${columnNumber}`);
1243
+ this.verboseInfo(LogCategory.TYPE_RESOLUTION, `Type resolution steps: Processing '${typeText}' at ${filePath}:${lineNumber}:${columnNumber}`, {
1244
+ filePath,
1245
+ lineNumber
1246
+ });
1106
1247
  if (this.isCircularTypeReference(typeText, typeNode)) {
1107
1248
  this.addStructuredWarning("circularReferences", {
1108
1249
  type: "circular_type_reference",
@@ -1166,7 +1307,10 @@ var AngularParser = class {
1166
1307
  * @returns Resolved token or null
1167
1308
  */
1168
1309
  resolveInferredTypeEnhanced(type, typeText, param, filePath, lineNumber, columnNumber) {
1169
- if (this._options.verbose) console.log(`Attempting to resolve inferred type: ${typeText}`);
1310
+ this.verboseInfo(LogCategory.TYPE_RESOLUTION, `Attempting to resolve inferred type: ${typeText}`, {
1311
+ filePath,
1312
+ lineNumber
1313
+ });
1170
1314
  const symbol = type.getSymbol?.();
1171
1315
  if (symbol) {
1172
1316
  const symbolName = symbol.getName();
@@ -1268,14 +1412,20 @@ var AngularParser = class {
1268
1412
  cacheKey = `${filePath}:${parameterName}:${typeText}`;
1269
1413
  if (this._typeResolutionCache.has(cacheKey)) {
1270
1414
  const cachedResult = this._typeResolutionCache.get(cacheKey);
1271
- if (this._options.verbose) console.log(`Cache hit for parameter '${parameterName}': ${typeText}`);
1415
+ this.verboseDebug(LogCategory.TYPE_RESOLUTION, `Cache hit for parameter '${parameterName}': ${typeText}`, {
1416
+ parameterName,
1417
+ cacheKey
1418
+ });
1272
1419
  return cachedResult ? {
1273
1420
  token: cachedResult,
1274
1421
  flags,
1275
1422
  parameterName
1276
1423
  } : null;
1277
1424
  }
1278
- if (this._options.verbose) console.log(`Cache miss for parameter '${parameterName}': ${typeText}`);
1425
+ this.verboseDebug(LogCategory.TYPE_RESOLUTION, `Cache miss for parameter '${parameterName}': ${typeText}`, {
1426
+ parameterName,
1427
+ cacheKey
1428
+ });
1279
1429
  const resolvedToken = this.resolveInferredTypeEnhanced(type, typeText, param, filePath, lineNumber, columnNumber);
1280
1430
  this._typeResolutionCache.set(cacheKey, resolvedToken);
1281
1431
  if (resolvedToken) return {
@@ -1369,7 +1519,10 @@ var AngularParser = class {
1369
1519
  if (this._options.verbose) {
1370
1520
  const className = classDeclaration.getName() || "unknown";
1371
1521
  const filePath = classDeclaration.getSourceFile().getFilePath();
1372
- console.warn(`Warning: Failed to extract inject() dependencies for class '${className}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
1522
+ this.warn(LogCategory.ERROR_RECOVERY, `Failed to extract inject() dependencies for class '${className}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
1523
+ className,
1524
+ filePath
1525
+ });
1373
1526
  }
1374
1527
  }
1375
1528
  return dependencies;
@@ -1405,7 +1558,10 @@ var AngularParser = class {
1405
1558
  if (this._options.verbose) {
1406
1559
  const propertyName = property.getName() || "unknown";
1407
1560
  const filePath = property.getSourceFile().getFilePath();
1408
- console.warn(`Warning: Failed to parse inject() property '${propertyName}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
1561
+ this.warn(LogCategory.ERROR_RECOVERY, `Failed to parse inject() property '${propertyName}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
1562
+ propertyName,
1563
+ filePath
1564
+ });
1409
1565
  }
1410
1566
  return null;
1411
1567
  }
@@ -1431,7 +1587,7 @@ var AngularParser = class {
1431
1587
  const propertyAssignment = prop;
1432
1588
  const name = propertyAssignment.getName();
1433
1589
  if (!supportedOptions.has(name)) {
1434
- if (this._options.verbose) console.warn(`Unknown inject() option: '${name}' - ignoring`);
1590
+ if (this._options.verbose) this.warn(LogCategory.AST_ANALYSIS, `Unknown inject() option: '${name}' - ignoring`);
1435
1591
  continue;
1436
1592
  }
1437
1593
  const initializer = propertyAssignment.getInitializer();
@@ -1451,7 +1607,7 @@ var AngularParser = class {
1451
1607
  }
1452
1608
  }
1453
1609
  } catch (error) {
1454
- if (this._options.verbose) console.warn(`Warning: Failed to parse inject() options: ${error instanceof Error ? error.message : String(error)}`);
1610
+ if (this._options.verbose) this.warn(LogCategory.ERROR_RECOVERY, `Failed to parse inject() options: ${error instanceof Error ? error.message : String(error)}`);
1455
1611
  }
1456
1612
  return flags;
1457
1613
  }
@@ -1496,14 +1652,21 @@ var AngularParser = class {
1496
1652
  name: decoratorName,
1497
1653
  reason: "Unknown or unsupported decorator"
1498
1654
  });
1499
- console.warn(`Unknown or unsupported decorator: @${decoratorName}() - This decorator is not recognized as an Angular DI decorator and will be ignored.`);
1655
+ const className = parameter.getFirstAncestorByKind(ts_morph.SyntaxKind.ClassDeclaration)?.getName() || void 0;
1656
+ this.warn(LogCategory.AST_ANALYSIS, `Unknown or unsupported decorator: @${decoratorName}() - This decorator is not recognized as an Angular DI decorator and will be ignored.`, {
1657
+ filePath: parameter.getSourceFile().getFilePath(),
1658
+ className
1659
+ });
1500
1660
  }
1501
1661
  }
1502
1662
  } catch (error) {
1503
1663
  if (this._options.verbose) {
1504
1664
  const paramName = parameter.getName();
1505
1665
  const filePath = parameter.getSourceFile().getFilePath();
1506
- console.warn(`Warning: Failed to extract decorators for parameter '${paramName}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
1666
+ this.warn(LogCategory.ERROR_RECOVERY, `Failed to extract decorators for parameter '${paramName}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
1667
+ paramName,
1668
+ filePath
1669
+ });
1507
1670
  }
1508
1671
  return {};
1509
1672
  }
@@ -1528,7 +1691,7 @@ var AngularParser = class {
1528
1691
  }
1529
1692
  return false;
1530
1693
  } catch (error) {
1531
- if (this._options.verbose) console.warn(`Warning: Could not verify inject() import in ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`);
1694
+ if (this._options.verbose) this.warn(LogCategory.ERROR_RECOVERY, `Could not verify inject() import in ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`, { filePath: sourceFile.getFilePath() });
1532
1695
  return true;
1533
1696
  }
1534
1697
  }
@@ -1542,15 +1705,19 @@ var AngularParser = class {
1542
1705
  if (!expression) return null;
1543
1706
  if (expression.getKind() !== ts_morph.SyntaxKind.CallExpression) return null;
1544
1707
  const callExpression = expression;
1708
+ const sourceFile = expression.getSourceFile();
1709
+ const warnVerbose = (message) => {
1710
+ if (this._options.verbose) this.warn(LogCategory.AST_ANALYSIS, message, { filePath: sourceFile.getFilePath() });
1711
+ };
1545
1712
  try {
1546
1713
  const callIdentifier = callExpression.getExpression();
1547
1714
  if (callIdentifier.getKind() !== ts_morph.SyntaxKind.Identifier) return null;
1548
1715
  if (callIdentifier.getText() !== "inject") return null;
1549
- const sourceFile = expression.getSourceFile();
1550
- if (!this.isAngularInjectImported(sourceFile)) return null;
1716
+ const sourceFile$1 = expression.getSourceFile();
1717
+ if (!this.isAngularInjectImported(sourceFile$1)) return null;
1551
1718
  const args = callExpression.getArguments();
1552
1719
  if (args.length === 0) {
1553
- if (this._options.verbose) console.warn("inject() called without token parameter - skipping");
1720
+ warnVerbose("inject() called without token parameter - skipping");
1554
1721
  return null;
1555
1722
  }
1556
1723
  const tokenArg = args[0];
@@ -1558,22 +1725,22 @@ var AngularParser = class {
1558
1725
  if (tokenArg.getKind() === ts_morph.SyntaxKind.StringLiteral) {
1559
1726
  token = tokenArg.getText().slice(1, -1);
1560
1727
  if (!token) {
1561
- if (this._options.verbose) console.warn("inject() called with empty string token - skipping");
1728
+ warnVerbose("inject() called with empty string token - skipping");
1562
1729
  return null;
1563
1730
  }
1564
1731
  } else if (tokenArg.getKind() === ts_morph.SyntaxKind.Identifier) {
1565
1732
  token = tokenArg.getText();
1566
1733
  if (token === "undefined" || token === "null") {
1567
- if (this._options.verbose) console.warn(`inject() called with ${token} token - skipping`);
1734
+ warnVerbose(`inject() called with ${token} token - skipping`);
1568
1735
  return null;
1569
1736
  }
1570
1737
  } else if (tokenArg.getKind() === ts_morph.SyntaxKind.NullKeyword) {
1571
- if (this._options.verbose) console.warn("inject() called with null token - skipping");
1738
+ warnVerbose("inject() called with null token - skipping");
1572
1739
  return null;
1573
1740
  } else {
1574
1741
  token = tokenArg.getText();
1575
1742
  if (!token) {
1576
- if (this._options.verbose) console.warn("inject() called with invalid token expression - skipping");
1743
+ warnVerbose("inject() called with invalid token expression - skipping");
1577
1744
  return null;
1578
1745
  }
1579
1746
  }
@@ -1581,9 +1748,7 @@ var AngularParser = class {
1581
1748
  if (args.length > 1 && this._options.includeDecorators) {
1582
1749
  const optionsArg = args[1];
1583
1750
  if (optionsArg.getKind() === ts_morph.SyntaxKind.ObjectLiteralExpression) flags = this.parseInjectOptions(optionsArg);
1584
- else if (optionsArg.getKind() !== ts_morph.SyntaxKind.NullKeyword && optionsArg.getKind() !== ts_morph.SyntaxKind.UndefinedKeyword) {
1585
- if (this._options.verbose) console.warn(`inject() called with invalid options type: ${optionsArg.getKindName()} - expected object literal`);
1586
- }
1751
+ else if (optionsArg.getKind() !== ts_morph.SyntaxKind.NullKeyword && optionsArg.getKind() !== ts_morph.SyntaxKind.UndefinedKeyword) warnVerbose(`inject() called with invalid options type: ${optionsArg.getKindName()} - expected object literal`);
1587
1752
  }
1588
1753
  return {
1589
1754
  token,
@@ -1591,10 +1756,7 @@ var AngularParser = class {
1591
1756
  source: "inject"
1592
1757
  };
1593
1758
  } catch (error) {
1594
- if (this._options.verbose) {
1595
- const filePath = expression.getSourceFile().getFilePath();
1596
- console.warn(`Warning: Failed to analyze inject() call in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
1597
- }
1759
+ if (this._options.verbose) warnVerbose(`Failed to analyze inject() call in ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`);
1598
1760
  return null;
1599
1761
  }
1600
1762
  }
@@ -1606,8 +1768,11 @@ var AngularParser = class {
1606
1768
  */
1607
1769
  collectVerboseStats(param, dependency, verboseStats) {
1608
1770
  const paramName = param.getName();
1609
- console.log(`Parameter: ${paramName}`);
1610
- console.log(` Token: ${dependency.token}`);
1771
+ this.verboseDebug(LogCategory.AST_ANALYSIS, "Parameter analysis", {
1772
+ paramName,
1773
+ token: dependency.token,
1774
+ flags: dependency.flags
1775
+ });
1611
1776
  if (Object.keys(dependency.flags || {}).length > 0) {
1612
1777
  verboseStats.parametersWithDecorators++;
1613
1778
  if (dependency.flags?.optional) verboseStats.decoratorCounts.optional++;
@@ -1627,7 +1792,10 @@ var AngularParser = class {
1627
1792
  "Host"
1628
1793
  ].includes(decoratorName)) {
1629
1794
  hasLegacyDecorators = true;
1630
- console.log(` Legacy decorator: @${decoratorName}`);
1795
+ this.verboseDebug(LogCategory.AST_ANALYSIS, `Legacy decorator detected`, {
1796
+ decoratorName,
1797
+ paramName
1798
+ });
1631
1799
  }
1632
1800
  }
1633
1801
  const initializer = param.getInitializer();
@@ -1638,29 +1806,44 @@ var AngularParser = class {
1638
1806
  hasInjectPattern = true;
1639
1807
  verboseStats.injectPatternsUsed++;
1640
1808
  const flagsStr = JSON.stringify(dependency.flags);
1641
- console.log(` inject() options: ${flagsStr}`);
1809
+ this.verboseDebug(LogCategory.AST_ANALYSIS, "inject() options detected", {
1810
+ paramName,
1811
+ flags: flagsStr
1812
+ });
1642
1813
  }
1643
1814
  }
1644
1815
  }
1645
1816
  if (hasLegacyDecorators) {
1646
1817
  verboseStats.legacyDecoratorsUsed++;
1647
1818
  if (hasInjectPattern) {
1648
- console.log(" Decorator Precedence Analysis");
1649
- console.log(" Legacy decorators take precedence over inject() options");
1819
+ this.verboseInfo(LogCategory.AST_ANALYSIS, "Decorator precedence analysis", {
1820
+ paramName,
1821
+ legacyDecorators: true,
1822
+ injectPattern: true
1823
+ });
1650
1824
  const appliedFlags = Object.keys(dependency.flags || {}).filter((key) => dependency.flags?.[key] === true).map((key) => `@${key.charAt(0).toUpperCase() + key.slice(1)}`).join(", ");
1651
- console.log(` Applied: ${appliedFlags}`);
1825
+ this.verboseDebug(LogCategory.AST_ANALYSIS, "Applied decorator flags", {
1826
+ paramName,
1827
+ appliedFlags
1828
+ });
1652
1829
  const injectResult = this.analyzeInjectCall(initializer);
1653
1830
  if (injectResult && Object.keys(injectResult.flags).length > 0) {
1654
1831
  const overriddenFlags = JSON.stringify(injectResult.flags);
1655
- console.log(` Overridden inject() options: ${overriddenFlags}`);
1832
+ this.verboseDebug(LogCategory.AST_ANALYSIS, "Overridden inject() options", {
1833
+ paramName,
1834
+ overriddenFlags
1835
+ });
1656
1836
  }
1657
1837
  const finalFlags = JSON.stringify(dependency.flags);
1658
- console.log(` Final flags: ${finalFlags}`);
1838
+ this.verboseDebug(LogCategory.AST_ANALYSIS, "Final decorator flags", {
1839
+ paramName,
1840
+ finalFlags
1841
+ });
1659
1842
  }
1660
1843
  }
1661
1844
  } else {
1662
1845
  verboseStats.parametersWithoutDecorators++;
1663
- console.log(" No decorators detected");
1846
+ this.verboseDebug(LogCategory.AST_ANALYSIS, "No decorators detected", { paramName });
1664
1847
  }
1665
1848
  }
1666
1849
  /**
@@ -1670,69 +1853,24 @@ var AngularParser = class {
1670
1853
  * @param classDeclaration Class being analyzed
1671
1854
  */
1672
1855
  outputVerboseAnalysis(dependencies, verboseStats, classDeclaration) {
1673
- if (!this._options.verbose) return;
1674
- if (this._options.includeDecorators) {
1675
- console.log("=== Decorator Statistics ===");
1676
- console.log(`Total decorators detected: ${verboseStats.decoratorCounts.optional + verboseStats.decoratorCounts.self + verboseStats.decoratorCounts.skipSelf + verboseStats.decoratorCounts.host}`);
1677
- if (verboseStats.decoratorCounts.optional > 0) console.log(`@Optional: ${verboseStats.decoratorCounts.optional}`);
1678
- if (verboseStats.decoratorCounts.self > 0) console.log(`@Self: ${verboseStats.decoratorCounts.self}`);
1679
- if (verboseStats.decoratorCounts.skipSelf > 0) console.log(`@SkipSelf: ${verboseStats.decoratorCounts.skipSelf}`);
1680
- if (verboseStats.decoratorCounts.host > 0) console.log(`@Host: ${verboseStats.decoratorCounts.host}`);
1681
- console.log(`Parameters with decorators: ${verboseStats.parametersWithDecorators}`);
1682
- console.log(`Parameters without decorators: ${verboseStats.parametersWithoutDecorators}`);
1683
- if (verboseStats.injectPatternsUsed > 0) {
1684
- console.log("inject() Pattern Analysis");
1685
- for (const dep of dependencies) if (dep.parameterName) {
1686
- const constructors = classDeclaration.getConstructors();
1687
- if (constructors.length > 0) {
1688
- const param = constructors[0].getParameters().find((p) => p.getName() === dep.parameterName);
1689
- if (param) {
1690
- const initializer = param.getInitializer();
1691
- if (initializer) {
1692
- const injectResult = this.analyzeInjectCall(initializer);
1693
- if (injectResult) {
1694
- if (injectResult.token.startsWith("\"") && injectResult.token.endsWith("\"")) console.log(`String token: ${injectResult.token}`);
1695
- else console.log(`Service token: ${injectResult.token}`);
1696
- if (Object.keys(injectResult.flags).length > 0) {
1697
- const flagsStr = JSON.stringify(injectResult.flags);
1698
- console.log(`inject() options detected: ${flagsStr}`);
1699
- } else console.log("inject() with no options");
1700
- }
1701
- }
1702
- }
1703
- }
1704
- }
1705
- }
1706
- if (verboseStats.skippedDecorators.length > 0) {
1707
- console.log("Skipped Decorators");
1708
- for (const skipped of verboseStats.skippedDecorators) {
1709
- console.log(`${skipped.name}`);
1710
- console.log(`Reason: ${skipped.reason}`);
1711
- }
1712
- console.log(`Total skipped: ${verboseStats.skippedDecorators.length}`);
1713
- }
1714
- console.log("Performance Metrics");
1715
- console.log(`Decorator processing time: ${verboseStats.totalProcessingTime.toFixed(2)}ms`);
1716
- console.log(`Total parameters analyzed: ${verboseStats.totalParameters}`);
1717
- if (verboseStats.totalParameters > 0) {
1718
- const avgTime = verboseStats.totalProcessingTime / verboseStats.totalParameters;
1719
- console.log(`Average time per parameter: ${avgTime.toFixed(3)}ms`);
1720
- }
1721
- console.log("=== Analysis Summary ===");
1722
- console.log(`Total dependencies: ${dependencies.length}`);
1723
- console.log(`With decorator flags: ${verboseStats.parametersWithDecorators}`);
1724
- console.log(`Without decorator flags: ${verboseStats.parametersWithoutDecorators}`);
1725
- console.log(`Legacy decorators used: ${verboseStats.legacyDecoratorsUsed}`);
1726
- console.log(`inject() patterns used: ${verboseStats.injectPatternsUsed}`);
1727
- if (verboseStats.skippedDecorators.length > 0) console.log(`Unknown decorators skipped: ${verboseStats.skippedDecorators.length}`);
1728
- if (verboseStats.parametersWithDecorators > 0) {
1729
- console.log("Flags distribution:");
1730
- if (verboseStats.decoratorCounts.optional > 0) console.log(`optional: ${verboseStats.decoratorCounts.optional}`);
1731
- if (verboseStats.decoratorCounts.self > 0) console.log(`self: ${verboseStats.decoratorCounts.self}`);
1732
- if (verboseStats.decoratorCounts.skipSelf > 0) console.log(`skipSelf: ${verboseStats.decoratorCounts.skipSelf}`);
1733
- if (verboseStats.decoratorCounts.host > 0) console.log(`host: ${verboseStats.decoratorCounts.host}`);
1734
- }
1735
- }
1856
+ if (!this._options.verbose || !this._logger) return;
1857
+ const className = classDeclaration.getName() || "unknown";
1858
+ this._logger.info(LogCategory.AST_ANALYSIS, "Decorator analysis summary", {
1859
+ className,
1860
+ decoratorCounts: verboseStats.decoratorCounts,
1861
+ parametersWithDecorators: verboseStats.parametersWithDecorators,
1862
+ parametersWithoutDecorators: verboseStats.parametersWithoutDecorators,
1863
+ injectPatternsUsed: verboseStats.injectPatternsUsed,
1864
+ skippedDecorators: verboseStats.skippedDecorators.length,
1865
+ legacyDecoratorsUsed: verboseStats.legacyDecoratorsUsed,
1866
+ totalProcessingTime: verboseStats.totalProcessingTime,
1867
+ totalParameters: verboseStats.totalParameters,
1868
+ dependencyCount: dependencies.length
1869
+ });
1870
+ if (verboseStats.skippedDecorators.length > 0) this._logger.debug(LogCategory.AST_ANALYSIS, "Skipped decorators", {
1871
+ skippedDecorators: verboseStats.skippedDecorators,
1872
+ className
1873
+ });
1736
1874
  }
1737
1875
  };
1738
1876
 
@@ -1858,8 +1996,8 @@ function enforceMinimumNodeVersion() {
1858
1996
  process.exit(1);
1859
1997
  }
1860
1998
  enforceMinimumNodeVersion();
1861
- function resolveProjectPath(projectPath, projectOption) {
1862
- const candidatePath = projectPath ?? projectOption;
1999
+ function resolveProjectPath(projectOption) {
2000
+ const candidatePath = projectOption;
1863
2001
  try {
1864
2002
  if ((0, node_fs.statSync)(candidatePath).isDirectory()) {
1865
2003
  const tsconfigPath = (0, node_path.join)(candidatePath, "tsconfig.json");
@@ -1870,18 +2008,22 @@ function resolveProjectPath(projectPath, projectOption) {
1870
2008
  }
1871
2009
  const program = new commander.Command();
1872
2010
  program.name("ng-di-graph").description("Angular DI dependency graph CLI tool").version("0.1.0");
1873
- program.argument("[projectPath]", "tsconfig.json path or project directory containing tsconfig.json").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);
1874
- program.action(async (projectPath, options) => {
2011
+ program.argument("[filePaths...]", "TypeScript files to analyze (alias for --files)").option("-p, --project <path>", "tsconfig.json path", "./tsconfig.json").option("--files <paths...>", "specific file paths to analyze (similar to eslint targets)").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);
2012
+ program.action(async (filePaths = [], options) => {
1875
2013
  try {
1876
- const project = resolveProjectPath(projectPath, options.project);
2014
+ const project = resolveProjectPath(options.project);
1877
2015
  if (options.direction && ![
1878
2016
  "upstream",
1879
2017
  "downstream",
1880
2018
  "both"
1881
2019
  ].includes(options.direction)) throw ErrorHandler.createError(`Invalid direction: ${options.direction}. Must be 'upstream', 'downstream', or 'both'`, "INVALID_ARGUMENTS");
1882
2020
  if (options.format && !["json", "mermaid"].includes(options.format)) throw ErrorHandler.createError(`Invalid format: ${options.format}. Must be 'json' or 'mermaid'`, "INVALID_ARGUMENTS");
2021
+ const tsconfigLikePositional = filePaths.find((filePath) => /(?:^|[\\/])tsconfig(?:\.[^/\\]+)?\.json$/.test(filePath));
2022
+ if (tsconfigLikePositional) throw ErrorHandler.createError(`Positional argument "${tsconfigLikePositional}" looks like a tsconfig. Use --project "${tsconfigLikePositional}" instead.`, "INVALID_ARGUMENTS");
2023
+ const mergedFiles = mergeFileTargets(filePaths, options.files);
1883
2024
  const cliOptions = {
1884
2025
  project,
2026
+ files: mergedFiles.length > 0 ? mergedFiles : void 0,
1885
2027
  format: options.format,
1886
2028
  entry: options.entry,
1887
2029
  direction: options.direction,
@@ -1917,7 +2059,7 @@ program.action(async (projectPath, options) => {
1917
2059
  }
1918
2060
  if (cliOptions.entry && cliOptions.entry.length > 0) {
1919
2061
  if (cliOptions.verbose) console.log(`🔍 Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
1920
- graph = filterGraph(graph, cliOptions);
2062
+ graph = filterGraph(graph, cliOptions, logger);
1921
2063
  if (cliOptions.verbose) console.log(`✅ Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
1922
2064
  }
1923
2065
  let formatter;
@@ -1945,6 +2087,11 @@ program.action(async (projectPath, options) => {
1945
2087
  }
1946
2088
  }
1947
2089
  });
2090
+ function mergeFileTargets(positionalFiles, flagFiles) {
2091
+ const merged = [];
2092
+ for (const filePath of [...positionalFiles, ...flagFiles ?? []]) if (!merged.includes(filePath)) merged.push(filePath);
2093
+ return merged;
2094
+ }
1948
2095
  process.on("unhandledRejection", (reason, promise) => {
1949
2096
  const error = ErrorHandler.createError(`Unhandled promise rejection: ${reason}`, "INTERNAL_ERROR", void 0, { promise: String(promise) });
1950
2097
  ErrorHandler.handleError(error, false);