@sun-asterisk/sunlint 1.3.47 → 1.3.49

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.
Files changed (185) hide show
  1. package/config/rules/rules-registry-generated.json +1717 -282
  2. package/core/architecture-integration.js +57 -15
  3. package/core/cli-action-handler.js +51 -36
  4. package/core/config-manager.js +6 -0
  5. package/core/config-merger.js +33 -0
  6. package/core/config-validator.js +37 -2
  7. package/core/file-targeting-service.js +148 -15
  8. package/core/init-command.js +118 -70
  9. package/core/output-service.js +12 -3
  10. package/core/project-detector.js +517 -0
  11. package/core/scoring-service.js +12 -6
  12. package/core/summary-report-service.js +9 -4
  13. package/core/tui-select.js +245 -0
  14. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  15. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  16. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  17. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  18. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  19. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  20. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  21. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  22. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  23. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  24. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  25. package/engines/impact/cli.js +54 -39
  26. package/engines/impact/config/default-config.js +105 -5
  27. package/engines/impact/core/impact-analyzer.js +12 -15
  28. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  29. package/engines/impact/core/utils/method-call-graph.js +272 -87
  30. package/origin-rules/dart-en.md +1 -1
  31. package/origin-rules/go-en.md +231 -0
  32. package/origin-rules/php-en.md +107 -0
  33. package/origin-rules/python-en.md +113 -0
  34. package/origin-rules/ruby-en.md +607 -0
  35. package/package.json +1 -1
  36. package/scripts/copy-arch-detect.js +5 -1
  37. package/scripts/copy-impact-analyzer.js +5 -1
  38. package/scripts/generate-rules-registry.js +30 -14
  39. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  40. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  41. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  42. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  43. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  44. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  45. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  46. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  47. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  48. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  49. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  50. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  51. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  52. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  53. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  54. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  55. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  56. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  57. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  58. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  59. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  60. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  61. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  62. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  63. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  64. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  65. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  66. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  67. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  68. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
  69. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  70. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  71. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  72. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  73. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  74. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  75. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  76. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  77. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  78. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  79. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  80. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  81. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  82. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  83. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  84. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  85. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  86. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  87. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  88. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  89. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  90. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  91. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  92. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  93. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  94. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  95. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  96. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  97. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  98. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  99. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  100. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  101. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  102. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  103. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  104. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  105. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  106. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  107. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  108. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  109. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  110. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  111. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  112. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  113. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  114. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  115. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  116. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  117. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  118. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  119. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  120. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  121. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  122. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  123. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  124. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  125. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  126. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  127. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  128. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  129. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  130. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  131. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  132. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  133. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  134. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  135. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  136. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  137. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  138. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  139. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  153. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  154. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  155. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  156. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  157. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  158. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  159. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  160. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  161. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  162. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  163. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  164. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  165. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  166. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  167. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  168. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  169. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  170. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  171. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  172. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  173. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  174. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  175. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  176. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  177. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  178. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  179. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  180. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  181. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  182. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  183. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  184. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  185. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -9,8 +9,8 @@ import path from 'path';
9
9
  export class MethodCallGraph {
10
10
  constructor() {
11
11
  this.project = null;
12
- this.methodCallMap = new Map(); // method -> [callers] (reverse map)
13
- this.methodCallsMap = new Map(); // method -> [methods it calls] (forward map)
12
+ this.methodCallMap = new Map(); // method -> Set(callers)
13
+ this.methodCallsMap = new Map(); // method -> Set(methods it calls)
14
14
  this.methodToFile = new Map(); // method -> file path
15
15
  this.methodToEndpoint = new Map(); // method -> endpoint info
16
16
  this.interfaceToClass = new Map(); // interface name -> concrete class name
@@ -19,121 +19,311 @@ export class MethodCallGraph {
19
19
  this.queueNameToProcessor = new Map(); // queue name -> processor class name
20
20
  this.methodToQueueProcessor = new Map(); // method -> queue processor info
21
21
  this.endpointToQueueNames = new Map(); // endpoint method -> [queue names]
22
+ this.classSet = new Set(); // All class names for O(1) lookup
23
+ this.typeCache = new Map(); // Cache type resolution results
24
+ this.dispatchPatterns = new Set(['sendcommand', 'send', 'execute', 'dispatch', 'publish', 'emit', 'trigger', 'enqueue']);
22
25
  }
23
26
 
24
27
  /**
25
28
  * Initialize ts-morph project and build call graph
26
29
  */
27
- async initialize(sourceDir, excludePaths = [], verbose = false) {
30
+ async initialize(sourceDir, excludePaths = [], verbose = false, gitignoreParser = null) {
28
31
  this.verbose = verbose;
32
+ this.gitignoreParser = gitignoreParser;
29
33
 
34
+ if (this.verbose) {
35
+ console.log(`\n🔍 Discovering TypeScript files...`);
36
+ }
37
+
38
+ // Step 1: Find files BEFORE creating project (prevents module resolution)
39
+ const { glob } = await import('glob');
40
+ const pattern = `${sourceDir}/**/*.ts`;
41
+
42
+ // Build ignore patterns for glob
43
+ const ignorePatterns = [
44
+ '**/node_modules/**',
45
+ '**/dist/**',
46
+ '**/build/**',
47
+ '**/*.d.ts', // Type definitions have no runtime code
48
+ ...excludePaths.map(p => `**/*${p}*/**`), // Convert simple paths to globs
49
+ ];
50
+
51
+ // Get all matching files
52
+ const allFiles = glob.sync(pattern, {
53
+ ignore: ignorePatterns,
54
+ absolute: true,
55
+ nodir: true,
56
+ });
57
+
58
+ // Apply gitignore filtering if available
59
+ const rootDir = path.join(sourceDir, '..');
60
+ const filteredFiles = gitignoreParser
61
+ ? allFiles.filter(filePath => {
62
+ const relativeToRoot = path.relative(rootDir, filePath);
63
+ return !gitignoreParser.shouldExclude(relativeToRoot);
64
+ })
65
+ : allFiles;
66
+
67
+ if (this.verbose) {
68
+ console.log(` Found ${allFiles.length} TypeScript files`);
69
+ if (gitignoreParser) {
70
+ console.log(` Filtered to ${filteredFiles.length} files after gitignore patterns`);
71
+ }
72
+ const excludedCount = allFiles.length - filteredFiles.length;
73
+ if (excludedCount > 0) {
74
+ console.log(` Excluded ${excludedCount} files (${Math.round(excludedCount / allFiles.length * 100)}%)`);
75
+ }
76
+ }
77
+
78
+ // Step 2: Create ts-morph project WITHOUT tsconfig (prevents loading node_modules)
30
79
  this.project = new Project({
31
- tsConfigFilePath: path.join(sourceDir, '../tsconfig.json'),
32
80
  skipAddingFilesFromTsConfig: true,
81
+ skipLoadingLibFiles: true,
82
+ compilerOptions: {
83
+ target: 99, // ESNext
84
+ module: 1, // CommonJS
85
+ skipLibCheck: true,
86
+ allowJs: false,
87
+ noResolve: true, // Critical: Don't resolve module imports
88
+ }
33
89
  });
34
90
 
35
- // Add source files
36
- this.project.addSourceFilesAtPaths(`${sourceDir}/**/*.ts`);
91
+ // Step 3: Add only the filtered files (no module resolution!)
92
+ if (this.verbose) {
93
+ console.log(`\n📊 Loading ${filteredFiles.length} files into ts-morph...`);
94
+ }
95
+
96
+ this.project.addSourceFilesAtPaths(filteredFiles);
97
+
98
+ const sourceFiles = this.project.getSourceFiles();
37
99
 
38
- // Filter out excluded paths
39
- const sourceFiles = this.project.getSourceFiles().filter(sf => {
40
- const filePath = sf.getFilePath();
41
- return !excludePaths.some(ex => filePath.includes(ex));
42
- });
100
+ if (this.verbose) {
101
+ console.log(` ✅ Loaded ${sourceFiles.length} files`);
102
+ console.log(`\n🔬 Analyzing source code...`);
103
+ }
43
104
 
105
+ // Step 4: Single pass analysis
44
106
  for (const sourceFile of sourceFiles) {
45
- this.extractInterfaceMappings(sourceFile);
46
- this.extractCommandMappings(sourceFile);
47
- this.extractQueueProcessorMappings(sourceFile);
107
+ this.analyzeFileComplete(sourceFile);
48
108
  }
49
109
 
50
- for (const sourceFile of sourceFiles) {
51
- this.analyzeFile(sourceFile);
110
+ if (this.verbose) {
111
+ console.log(`\n✅ Analysis complete!`);
112
+ console.log(` Classes found: ${this.classSet.size}`);
113
+ console.log(` Methods found: ${this.methodToFile.size}`);
114
+ console.log(` Endpoints found: ${this.methodToEndpoint.size}`);
52
115
  }
53
116
 
54
117
  return this;
55
118
  }
56
119
 
57
120
  /**
58
- * Analyze a single file to extract method definitions and calls
121
+ * Analyze a single file - consolidated single pass
59
122
  */
60
- analyzeFile(sourceFile) {
123
+ analyzeFileComplete(sourceFile) {
61
124
  const filePath = sourceFile.getFilePath();
62
-
63
- // Get all classes
64
125
  const classes = sourceFile.getClasses();
65
126
 
66
127
  for (const classDecl of classes) {
67
128
  const className = classDecl.getName();
68
- if (!className) continue; // Skip anonymous classes
129
+ if (!className) continue;
130
+
131
+ this.classSet.add(className);
69
132
 
70
- // Check if this class is a queue processor
133
+ // Extract class-level metadata
71
134
  const classDecorators = classDecl.getDecorators();
135
+
136
+ // Check for @Processor decorator
72
137
  const processorDecorator = classDecorators.find(d => d.getName() === 'Processor');
73
138
  let processorQueueName = null;
74
-
75
139
  if (processorDecorator) {
76
140
  const args = processorDecorator.getArguments();
77
141
  processorQueueName = args[0]?.getText().replace(/['"]/g, '');
142
+ this.queueNameToProcessor.set(processorQueueName, className);
78
143
  }
79
144
 
80
- // Get all methods in this class
81
- const methods = classDecl.getMethods();
145
+ // Check for @Command decorator
146
+ const commandDecorator = classDecorators.find(d => d.getName() === 'Command');
147
+ if (commandDecorator) {
148
+ const args = commandDecorator.getArguments();
149
+ if (args.length > 0) {
150
+ const text = args[0].getText();
151
+ const nameMatch = text.match(/name:\s*['"]([^'"]+)['"]/);
152
+ if (nameMatch) {
153
+ this.commandNameToClass.set(nameMatch[1], className);
154
+ }
155
+ }
156
+ }
82
157
 
158
+ // Extract interface mappings from @Module decorator
159
+ const moduleDecorator = classDecl.getDecorator('Module');
160
+ if (moduleDecorator) {
161
+ const args = moduleDecorator.getArguments();
162
+ if (args.length > 0) {
163
+ const text = args[0].getText();
164
+ const providerPattern = /\{\s*provide:\s*['"](\w+)['"]\s*,\s*useClass:\s*(\w+)/g;
165
+ let match;
166
+ while ((match = providerPattern.exec(text)) !== null) {
167
+ this.interfaceToClass.set(match[1], match[2]);
168
+ }
169
+ }
170
+ }
171
+
172
+ // Analyze methods
173
+ const methods = classDecl.getMethods();
83
174
  for (const method of methods) {
84
- const methodName = method.getName();
85
- const fullMethodName = `${className}.${methodName}`;
86
-
87
- // Store method location
88
- this.methodToFile.set(fullMethodName, filePath);
89
-
90
- // Check if this is an endpoint method (has HTTP decorator)
91
- const decorators = method.getDecorators();
92
- const httpDecorator = decorators.find(d =>
93
- ['Get', 'Post', 'Put', 'Delete', 'Patch', 'Options', 'Head', 'All'].includes(d.getName())
94
- );
95
-
96
- if (httpDecorator) {
97
- const decoratorName = httpDecorator.getName();
98
- const args = httpDecorator.getArguments();
99
- const route = args[0]?.getText().replace(/['"]/g, '') || '/';
100
-
101
- this.methodToEndpoint.set(fullMethodName, {
102
- method: decoratorName.toUpperCase(),
103
- path: route,
104
- controller: className,
105
- file: filePath,
106
- });
175
+ this.analyzeMethodComplete(method, className, filePath, processorQueueName);
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Analyze a single method - consolidated single AST traversal
182
+ */
183
+ analyzeMethodComplete(method, className, filePath, processorQueueName) {
184
+ const methodName = method.getName();
185
+ const fullMethodName = `${className}.${methodName}`;
186
+
187
+ this.methodToFile.set(fullMethodName, filePath);
188
+
189
+ // Check decorators
190
+ const decorators = method.getDecorators();
191
+
192
+ // Check for HTTP endpoint
193
+ const httpDecorator = decorators.find(d =>
194
+ ['Get', 'Post', 'Put', 'Delete', 'Patch', 'Options', 'Head', 'All'].includes(d.getName())
195
+ );
196
+ if (httpDecorator) {
197
+ const decoratorName = httpDecorator.getName();
198
+ const args = httpDecorator.getArguments();
199
+ const route = args[0]?.getText().replace(/['"]/g, '') || '/';
200
+ this.methodToEndpoint.set(fullMethodName, {
201
+ method: decoratorName.toUpperCase(),
202
+ path: route,
203
+ controller: className,
204
+ file: filePath,
205
+ });
206
+ }
207
+
208
+ // Check for @Process decorator
209
+ const processDecorator = decorators.find(d => d.getName() === 'Process');
210
+ if (processDecorator && processorQueueName) {
211
+ const args = processDecorator.getArguments();
212
+ const jobName = args[0]?.getText().replace(/['"]/g, '') || '';
213
+ this.methodToQueueProcessor.set(fullMethodName, {
214
+ queueName: processorQueueName,
215
+ jobName: jobName,
216
+ processor: className,
217
+ method: methodName,
218
+ file: filePath,
219
+ });
220
+ }
221
+
222
+ // Single AST traversal for all call expressions
223
+ const callExpressions = method.getDescendantsOfKind(SyntaxKind.CallExpression);
224
+
225
+ const commandNames = [];
226
+ const queueNames = [];
227
+
228
+ for (const call of callExpressions) {
229
+ const expression = call.getExpression();
230
+
231
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
232
+ const calledMethodName = expression.getName();
233
+ const objExpression = expression.getExpression();
234
+
235
+ // Process method calls
236
+ const calledClassName = this.resolveClassName(objExpression, className);
237
+ if (calledClassName) {
238
+ this.recordMethodCall(fullMethodName, calledClassName, calledMethodName);
107
239
  }
108
240
 
109
- // Check if this is a queue processor method (has @Process decorator)
110
- const processDecorator = decorators.find(d => d.getName() === 'Process');
111
- if (processDecorator && processorQueueName) {
112
- const args = processDecorator.getArguments();
113
- const jobName = args[0]?.getText().replace(/['"]/g, '') || '';
114
-
115
- this.methodToQueueProcessor.set(fullMethodName, {
116
- queueName: processorQueueName,
117
- jobName: jobName,
118
- processor: className,
119
- method: methodName,
120
- file: filePath,
121
- });
241
+ // Process command dispatches
242
+ if (this.isDispatchMethod(calledMethodName)) {
243
+ const args = call.getArguments();
244
+ if (args.length > 0) {
245
+ const firstArg = args[0];
246
+ if (firstArg.getKind() === SyntaxKind.StringLiteral) {
247
+ const commandName = firstArg.getText().replace(/['"]/g, '');
248
+ commandNames.push(commandName);
249
+ } else if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
250
+ const commandName = this.extractCommandFromObject(firstArg, method);
251
+ if (commandName) commandNames.push(commandName);
252
+ }
253
+ }
254
+ }
122
255
 
123
- if (this.verbose) {
124
- console.log(` 🔄 Found queue processor: ${processorQueueName}${jobName ? `/${jobName}` : ''} → ${fullMethodName}`);
256
+ // Process queue dispatches
257
+ if (calledMethodName === 'add' && objExpression.getKind() === SyntaxKind.PropertyAccessExpression) {
258
+ const queuePropertyName = objExpression.getName();
259
+ const classDecl = method.getParent();
260
+ if (classDecl && classDecl.getKind() === SyntaxKind.ClassDeclaration) {
261
+ const constructor = classDecl.getConstructors()[0];
262
+ if (constructor) {
263
+ const queueName = this.findQueueNameFromConstructor(constructor, queuePropertyName);
264
+ if (queueName && !queueNames.includes(queueName)) {
265
+ queueNames.push(queueName);
266
+ }
267
+ }
125
268
  }
126
269
  }
270
+ }
271
+ }
127
272
 
128
- // Find all method calls within this method
129
- this.analyzeMethodCalls(method, className, fullMethodName);
273
+ if (commandNames.length > 0) {
274
+ this.endpointToCommandNames.set(fullMethodName, commandNames);
275
+ }
276
+ if (queueNames.length > 0) {
277
+ this.endpointToQueueNames.set(fullMethodName, queueNames);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Resolve class name with caching and optimizations
283
+ */
284
+ resolveClassName(objExpression, fallbackClassName) {
285
+ if (objExpression.getKind() === SyntaxKind.ThisKeyword) {
286
+ return fallbackClassName;
287
+ }
288
+
289
+ const cacheKey = objExpression.getText();
290
+ if (this.typeCache.has(cacheKey)) {
291
+ return this.typeCache.get(cacheKey);
292
+ }
293
+
294
+ const type = objExpression.getType();
295
+ const symbol = type.getSymbol();
296
+
297
+ if (!symbol) {
298
+ this.typeCache.set(cacheKey, null);
299
+ return null;
300
+ }
130
301
 
131
- // Detect command dispatches and queue jobs in ALL methods (not just endpoints)
132
- // This allows us to track: Controller → Service → Queue → Processor
133
- this.detectCommandDispatches(method, fullMethodName);
134
- this.detectQueueDispatches(method, fullMethodName);
302
+ let calledClassName = symbol.getName();
303
+
304
+ if (this.interfaceToClass.has(calledClassName)) {
305
+ calledClassName = this.interfaceToClass.get(calledClassName);
306
+ } else if (calledClassName.startsWith('I') && calledClassName.length > 1) {
307
+ const possibleClassName = calledClassName.substring(1);
308
+ if (this.classSet.has(possibleClassName)) {
309
+ calledClassName = possibleClassName;
135
310
  }
136
311
  }
312
+
313
+ this.typeCache.set(cacheKey, calledClassName);
314
+ return calledClassName;
315
+ }
316
+
317
+ /**
318
+ * Check if method name matches dispatch patterns
319
+ */
320
+ isDispatchMethod(methodName) {
321
+ const lower = methodName.toLowerCase();
322
+ if (this.dispatchPatterns.has(lower)) return true;
323
+ for (const pattern of this.dispatchPatterns) {
324
+ if (lower.includes(pattern)) return true;
325
+ }
326
+ return false;
137
327
  }
138
328
 
139
329
  /**
@@ -458,25 +648,17 @@ export class MethodCallGraph {
458
648
  recordMethodCall(callerFullName, calleeClassName, calleeMethodName) {
459
649
  const calleeFullName = `${calleeClassName}.${calleeMethodName}`;
460
650
 
461
- // Add to reverse map (who calls this method)
651
+ // Add to reverse map (who calls this method) - use Set for O(1)
462
652
  if (!this.methodCallMap.has(calleeFullName)) {
463
- this.methodCallMap.set(calleeFullName, []);
653
+ this.methodCallMap.set(calleeFullName, new Set());
464
654
  }
655
+ this.methodCallMap.get(calleeFullName).add(callerFullName);
465
656
 
466
- const callers = this.methodCallMap.get(calleeFullName);
467
- if (!callers.includes(callerFullName)) {
468
- callers.push(callerFullName);
469
- }
470
-
471
- // Add to forward map (what this method calls)
657
+ // Add to forward map (what this method calls) - use Set for O(1)
472
658
  if (!this.methodCallsMap.has(callerFullName)) {
473
- this.methodCallsMap.set(callerFullName, []);
474
- }
475
-
476
- const calls = this.methodCallsMap.get(callerFullName);
477
- if (!calls.includes(calleeFullName)) {
478
- calls.push(calleeFullName);
659
+ this.methodCallsMap.set(callerFullName, new Set());
479
660
  }
661
+ this.methodCallsMap.get(callerFullName).add(calleeFullName);
480
662
  }
481
663
 
482
664
  /**
@@ -653,17 +835,20 @@ export class MethodCallGraph {
653
835
  * Get direct callers of a method
654
836
  */
655
837
  getCallers(methodName) {
656
- return this.methodCallMap.get(methodName) || [];
838
+ const callers = this.methodCallMap.get(methodName);
839
+ return callers ? Array.from(callers) : [];
657
840
  }
658
841
 
659
842
  /**
660
843
  * Find all callers of a method (recursive)
661
844
  */
662
- findAllCallers(methodName, visited = new Set(), depth = 0) {
845
+ findAllCallers(methodName, visited = new Set(), depth = 0, maxDepth = 10) {
663
846
  if (visited.has(methodName)) return [];
847
+ if (depth >= maxDepth) return [];
664
848
 
665
849
  visited.add(methodName);
666
- const directCallers = this.methodCallMap.get(methodName) || [];
850
+ const directCallersSet = this.methodCallMap.get(methodName);
851
+ const directCallers = directCallersSet ? Array.from(directCallersSet) : [];
667
852
  let allCallers = [...directCallers];
668
853
 
669
854
  if (this.verbose && depth === 0) {
@@ -676,7 +861,7 @@ export class MethodCallGraph {
676
861
  if (this.verbose && depth === 0) {
677
862
  console.log(` Tracing up from: ${caller}`);
678
863
  }
679
- const indirectCallers = this.findAllCallers(caller, visited, depth + 1);
864
+ const indirectCallers = this.findAllCallers(caller, visited, depth + 1, maxDepth);
680
865
  allCallers = allCallers.concat(indirectCallers);
681
866
  }
682
867
 
@@ -82,7 +82,7 @@
82
82
  ### 📘 Rule D008 – Avoid Long Functions
83
83
 
84
84
  - **Objective**: Improve code readability and maintainability by limiting function length
85
- - **Details**: Functions should not exceed 60 lines of effective code (excluding comments and opening/closing braces). Long functions are harder to understand, test, and maintain. They often indicate that the function is doing too much and should be broken down into smaller, more focused functions. The line count excludes blank lines, comments (both single-line // and multi-line /* */), and the opening { and closing } braces. The maximum line limit is configurable.
85
+ - **Details**: Functions should not exceed 60 lines of effective code (excluding comments and opening/closing braces). Long functions are harder to understand, test, and maintain. They often indicate that the function is doing too much and should be broken down into smaller, more focused functions. The line count excludes blank lines, comments (both single-line // and multi-line /\* \*/), and the opening `{` and closing `}` braces. The maximum line limit is configurable.
86
86
  - **Applies to**: Flutter/Dart
87
87
  - **Tools**: Custom analyzer (D008)
88
88
  - **Principles**: CODE_QUALITY, MAINTAINABILITY, READABILITY