@sun-asterisk/sunlint 1.3.0 → 1.3.2

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 (124) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/CONTRIBUTING.md +249 -605
  3. package/README.md +3 -4
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/large-project.json +143 -0
  7. package/config/presets/all.json +0 -1
  8. package/config/release.json +70 -0
  9. package/config/rule-analysis-strategies.js +38 -3
  10. package/config/rules/enhanced-rules-registry.json +474 -1179
  11. package/config/rules/rules-registry-generated.json +3 -3
  12. package/core/cli-action-handler.js +24 -30
  13. package/core/cli-program.js +11 -3
  14. package/core/config-merger.js +29 -2
  15. package/core/enhanced-rules-registry.js +3 -2
  16. package/core/semantic-engine.js +129 -19
  17. package/core/semantic-rule-base.js +4 -2
  18. package/core/unified-rule-registry.js +1 -1
  19. package/docs/COMMAND-EXAMPLES.md +134 -0
  20. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  21. package/engines/heuristic-engine.js +135 -16
  22. package/integrations/eslint/plugin/index.js +0 -2
  23. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
  24. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
  25. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
  26. package/origin-rules/common-en.md +19 -15
  27. package/package.json +1 -1
  28. package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
  29. package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
  30. package/rules/common/C006_function_naming/analyzer.js +29 -3
  31. package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
  32. package/rules/common/C010_limit_block_nesting/config.json +64 -0
  33. package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
  34. package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
  35. package/rules/common/C013_no_dead_code/analyzer.js +75 -177
  36. package/rules/common/C013_no_dead_code/config.json +61 -0
  37. package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
  38. package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
  39. package/rules/common/C014_dependency_injection/analyzer.js +48 -313
  40. package/rules/common/C014_dependency_injection/config.json +26 -0
  41. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
  42. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  43. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  44. package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
  45. package/rules/common/C018_no_throw_generic_error/config.json +50 -0
  46. package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
  47. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
  48. package/rules/common/C019_log_level_usage/analyzer.js +110 -317
  49. package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
  50. package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
  51. package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
  52. package/rules/common/C023_no_duplicate_variable/config.json +50 -0
  53. package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
  54. package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
  55. package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
  56. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
  57. package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
  58. package/rules/common/C033_separate_service_repository/README.md +78 -0
  59. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  60. package/rules/common/C033_separate_service_repository/config.json +50 -0
  61. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  62. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  63. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  64. package/rules/common/C035_error_logging_context/analyzer.js +232 -0
  65. package/rules/common/C035_error_logging_context/config.json +54 -0
  66. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  67. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  68. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  69. package/rules/common/C040_centralized_validation/config.json +46 -0
  70. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  71. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  72. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  73. package/rules/common/C076_explicit_function_types/README.md +30 -0
  74. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  75. package/rules/common/C076_explicit_function_types/config.json +15 -0
  76. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  77. package/rules/index.js +6 -1
  78. package/rules/parser/rule-parser.js +13 -2
  79. package/rules/security/S005_no_origin_auth/README.md +226 -0
  80. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  81. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  82. package/rules/security/S005_no_origin_auth/config.json +85 -0
  83. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  84. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  85. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  86. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  87. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  88. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  89. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  90. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  91. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  92. package/rules/security/S009_no_insecure_encryption/README.md +158 -0
  93. package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
  94. package/rules/security/S009_no_insecure_encryption/config.json +55 -0
  95. package/rules/security/S010_no_insecure_encryption/README.md +224 -0
  96. package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
  97. package/rules/security/S010_no_insecure_encryption/config.json +48 -0
  98. package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
  99. package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
  100. package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
  101. package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
  102. package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
  103. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  104. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  105. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  106. package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
  107. package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
  108. package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
  109. package/rules/security/S055_content_type_validation/README.md +176 -0
  110. package/rules/security/S055_content_type_validation/analyzer.js +312 -0
  111. package/rules/security/S055_content_type_validation/config.json +48 -0
  112. package/rules/utils/rule-helpers.js +140 -1
  113. package/scripts/consolidate-config.js +116 -0
  114. package/scripts/prepare-release.sh +1 -1
  115. package/config/rules/rules-registry.json +0 -765
  116. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  117. package/docs/FUTURE_PACKAGES.md +0 -83
  118. package/docs/HEURISTIC_VS_AI.md +0 -113
  119. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  120. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  121. package/docs/RELEASE_GUIDE.md +0 -230
  122. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  123. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  124. package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
@@ -0,0 +1,751 @@
1
+ // Enhanced symbol-based analyzer for C014
2
+ const { SyntaxKind } = require('ts-morph');
3
+
4
+ class C014SymbolBasedAnalyzer {
5
+ constructor(semanticEngine = null) {
6
+ this.semanticEngine = semanticEngine;
7
+ this.verbose = false;
8
+
9
+ // Configuration
10
+ this.config = {
11
+ // Built-in classes that are allowed
12
+ allowedBuiltins: [
13
+ 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'RegExp',
14
+ 'Map', 'Set', 'WeakMap', 'WeakSet', 'Promise', 'Error', 'TypeError',
15
+ 'FormData', 'Headers', 'Request', 'Response', 'URLSearchParams',
16
+ 'URL', 'Blob', 'File', 'Buffer', 'AbortController', 'AbortSignal',
17
+ 'TextEncoder', 'TextDecoder', 'MessageChannel', 'MessagePort',
18
+ 'Worker', 'SharedWorker', 'EventSource', 'WebSocket'
19
+ ],
20
+
21
+ // Value objects/DTOs that are typically safe to instantiate
22
+ allowedValueObjects: [
23
+ 'Money', 'Price', 'Currency', 'Quantity', 'Amount',
24
+ 'Email', 'Phone', 'Address', 'Name', 'Id', 'UserId',
25
+ 'UUID', 'Timestamp', 'Duration', 'Range'
26
+ ],
27
+
28
+ // Infrastructure patterns that suggest external dependencies
29
+ infraPatterns: [
30
+ 'Client', 'Repository', 'Service', 'Gateway', 'Adapter',
31
+ 'Provider', 'Factory', 'Builder', 'Manager', 'Handler',
32
+ 'Controller', 'Processor', 'Validator', 'Logger'
33
+ ],
34
+
35
+ // DI decorators that indicate proper injection
36
+ diDecorators: [
37
+ 'Injectable', 'Inject', 'Autowired', 'Component',
38
+ 'Service', 'Repository', 'Controller', 'autoInjectable'
39
+ ],
40
+
41
+ // Patterns to exclude from analysis
42
+ excludePatterns: [
43
+ '**/*.test.ts', '**/*.spec.ts', '**/*.test.js', '**/*.spec.js',
44
+ '**/tests/**', '**/test/**', '**/migration/**', '**/scripts/**'
45
+ ]
46
+ };
47
+ }
48
+
49
+ async initialize(semanticEngine = null) {
50
+ if (semanticEngine) {
51
+ this.semanticEngine = semanticEngine;
52
+ }
53
+ this.verbose = semanticEngine?.verbose || false;
54
+ }
55
+
56
+ async analyzeFileBasic(filePath, options = {}) {
57
+ const violations = [];
58
+
59
+ try {
60
+ // Try different approaches to get the source file
61
+ let sourceFile = this.semanticEngine.project.getSourceFile(filePath);
62
+
63
+ // If not found by full path, try by filename
64
+ if (!sourceFile) {
65
+ const fileName = filePath.split('/').pop();
66
+ const allFiles = this.semanticEngine.project.getSourceFiles();
67
+ sourceFile = allFiles.find(f => f.getBaseName() === fileName);
68
+ }
69
+
70
+ // If still not found, try to add the file
71
+ if (!sourceFile) {
72
+ try {
73
+ if (require('fs').existsSync(filePath)) {
74
+ sourceFile = this.semanticEngine.project.addSourceFileAtPath(filePath);
75
+ }
76
+ } catch (addError) {
77
+ // Fall through to error below
78
+ }
79
+ }
80
+
81
+ if (!sourceFile) {
82
+ throw new Error(`Source file not found: ${filePath}`);
83
+ }
84
+
85
+ if (this.verbose) {
86
+ console.log(`[DEBUG] 🔍 C014: Analyzing DI violations in ${filePath.split('/').pop()}`);
87
+ }
88
+
89
+ // Skip test files and excluded patterns
90
+ if (this.shouldSkipFile(filePath)) {
91
+ if (this.verbose) {
92
+ console.log(`[DEBUG] 🔍 C014: Skipping excluded file ${filePath.split('/').pop()}`);
93
+ }
94
+ return violations;
95
+ }
96
+
97
+ // Find all new expressions that might violate DI principles
98
+ const newExpressions = this.findProblematicNewExpressions(sourceFile);
99
+
100
+ for (const expr of newExpressions) {
101
+ if (this.isDependencyInjectionViolation(expr, sourceFile)) {
102
+ violations.push({
103
+ ruleId: 'C014',
104
+ message: this.buildViolationMessage(expr),
105
+ filePath: filePath,
106
+ line: expr.line,
107
+ column: expr.column,
108
+ severity: 'warning',
109
+ category: 'design',
110
+ confidence: expr.confidence,
111
+ suggestion: this.buildSuggestion(expr)
112
+ });
113
+ }
114
+ }
115
+
116
+ if (this.verbose) {
117
+ console.log(`[DEBUG] 🔍 C014: Found ${violations.length} DI violations`);
118
+ }
119
+
120
+ return violations;
121
+ } catch (error) {
122
+ if (this.verbose) {
123
+ console.error(`[DEBUG] ❌ C014: Symbol analysis error: ${error.message}`);
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ findProblematicNewExpressions(sourceFile) {
130
+ const expressions = [];
131
+
132
+ function traverse(node) {
133
+ if (node.getKind() === SyntaxKind.NewExpression) {
134
+ const newExpr = node;
135
+ const expression = newExpr.getExpression();
136
+
137
+ // Get class name and context information
138
+ const className = this.getClassName(expression);
139
+ const position = sourceFile.getLineAndColumnAtPos(newExpr.getStart());
140
+ const context = this.analyzeContext(newExpr);
141
+
142
+ if (this.verbose) {
143
+ console.log(`[DEBUG] 🔍 C014: Found new expression: ${className} at line ${position.line}`);
144
+ }
145
+
146
+ if (className) {
147
+ expressions.push({
148
+ node: newExpr,
149
+ className: className,
150
+ line: position.line,
151
+ column: position.column,
152
+ context: context,
153
+ confidence: this.calculateConfidence(className, context)
154
+ });
155
+ }
156
+ }
157
+
158
+ // Traverse children
159
+ node.forEachChild(child => traverse.call(this, child));
160
+ }
161
+
162
+ traverse.call(this, sourceFile);
163
+
164
+ if (this.verbose) {
165
+ console.log(`[DEBUG] 🔍 C014: Found ${expressions.length} new expressions total`);
166
+ }
167
+
168
+ return expressions;
169
+ }
170
+
171
+ getClassName(expression) {
172
+ if (expression.getKind() === SyntaxKind.Identifier) {
173
+ return expression.getText();
174
+ }
175
+
176
+ // Handle qualified names like MyNamespace.MyClass
177
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
178
+ return expression.getName();
179
+ }
180
+
181
+ return null;
182
+ }
183
+
184
+ analyzeContext(newExpressionNode) {
185
+ const context = {
186
+ isInConstructor: false,
187
+ isAssignedToThis: false,
188
+ isInMethod: false,
189
+ isLocalVariable: false,
190
+ isReturnValue: false,
191
+ isImmediateUse: false,
192
+ parentFunction: null,
193
+ hasDecorators: false
194
+ };
195
+
196
+ let current = newExpressionNode.getParent();
197
+
198
+ while (current) {
199
+ switch (current.getKind()) {
200
+ case SyntaxKind.Constructor:
201
+ context.isInConstructor = true;
202
+ context.parentFunction = current;
203
+ break;
204
+
205
+ case SyntaxKind.MethodDeclaration:
206
+ context.isInMethod = true;
207
+ context.parentFunction = current;
208
+ break;
209
+
210
+ case SyntaxKind.BinaryExpression:
211
+ // Check if it's assignment to this.property
212
+ const binaryExpr = current;
213
+ if (binaryExpr.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
214
+ const left = binaryExpr.getLeft();
215
+ if (left.getKind() === SyntaxKind.PropertyAccessExpression) {
216
+ const propAccess = left;
217
+ if (propAccess.getExpression().getKind() === SyntaxKind.ThisKeyword) {
218
+ context.isAssignedToThis = true;
219
+ }
220
+ }
221
+ }
222
+ break;
223
+
224
+ case SyntaxKind.PropertyDeclaration:
225
+ // Check if this new expression is in a class property initializer
226
+ const propDecl = current;
227
+ const initializer = propDecl.getInitializer();
228
+ if (initializer && this.containsNewExpression(initializer, newExpressionNode)) {
229
+ context.isAssignedToThis = true; // Class property is effectively "this.property"
230
+ }
231
+ break;
232
+
233
+ case SyntaxKind.VariableDeclaration:
234
+ context.isLocalVariable = true;
235
+ break;
236
+
237
+ case SyntaxKind.ReturnStatement:
238
+ context.isReturnValue = true;
239
+ break;
240
+
241
+ case SyntaxKind.CallExpression:
242
+ // Check for immediate method call like new Date().getTime()
243
+ const callExpr = current;
244
+ if (callExpr.getExpression() === newExpressionNode) {
245
+ context.isImmediateUse = true;
246
+ }
247
+ break;
248
+
249
+ case SyntaxKind.ClassDeclaration:
250
+ // Check for DI decorators on the class
251
+ context.hasDecorators = this.hasDecorators(current, this.config.diDecorators);
252
+ break;
253
+ }
254
+
255
+ current = current.getParent();
256
+ }
257
+
258
+ return context;
259
+ }
260
+
261
+ containsNewExpression(node, targetNewExpr) {
262
+ if (node === targetNewExpr) {
263
+ return true;
264
+ }
265
+
266
+ let found = false;
267
+ node.forEachChild(child => {
268
+ if (found) return;
269
+ if (this.containsNewExpression(child, targetNewExpr)) {
270
+ found = true;
271
+ }
272
+ });
273
+
274
+ return found;
275
+ }
276
+
277
+ isDependencyInjectionViolation(expr, sourceFile) {
278
+ const { className, context } = expr;
279
+
280
+ if (this.verbose) {
281
+ console.log(`[DEBUG] 🔍 C014: Checking violation for ${className}:`, {
282
+ isAssignedToThis: context.isAssignedToThis,
283
+ isInConstructor: context.isInConstructor,
284
+ isInMethod: context.isInMethod,
285
+ isLocalVariable: context.isLocalVariable,
286
+ isImmediateUse: context.isImmediateUse,
287
+ isReturnValue: context.isReturnValue,
288
+ hasDecorators: context.hasDecorators
289
+ });
290
+ }
291
+
292
+ // 1. Skip built-in JavaScript/DOM classes
293
+ if (this.config.allowedBuiltins.includes(className)) {
294
+ if (this.verbose) {
295
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - allowed builtin`);
296
+ }
297
+ return false;
298
+ }
299
+
300
+ // 2. Skip exception/error classes
301
+ if (this.isExceptionClass(className, sourceFile)) {
302
+ if (this.verbose) {
303
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Exception/Error class`);
304
+ }
305
+ return false;
306
+ }
307
+
308
+ // 4. Skip entity/model classes (data structures)
309
+ if (this.isEntityClass(className)) {
310
+ if (this.verbose) {
311
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Entity/Model class`);
312
+ }
313
+ return false;
314
+ }
315
+
316
+ // 5. Skip command pattern classes (value objects for operations)
317
+ if (this.isCommandPattern(className)) {
318
+ if (this.verbose) {
319
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Command pattern class`);
320
+ }
321
+ return false;
322
+ }
323
+
324
+ // 6. Skip value objects/DTOs (configurable)
325
+ if (this.config.allowedValueObjects.includes(className)) {
326
+ if (this.verbose) {
327
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - allowed value object`);
328
+ }
329
+ return false;
330
+ }
331
+
332
+ // 3. Skip if it's immediate usage (not stored as dependency)
333
+ if (context.isImmediateUse || context.isReturnValue) {
334
+ if (this.verbose) {
335
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - immediate use or return value`);
336
+ }
337
+ return false;
338
+ }
339
+
340
+ // 4. Skip Singleton pattern (self-instantiation in getInstance-like methods)
341
+ if (this.isSingletonPattern(className, context, sourceFile)) {
342
+ if (this.verbose) {
343
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Singleton pattern`);
344
+ }
345
+ return false;
346
+ }
347
+
348
+ // 5. Skip if it's a local variable in method (not dependency field) UNLESS it's infrastructure
349
+ if (context.isInMethod && context.isLocalVariable && !context.isAssignedToThis) {
350
+ // Exception: Still flag if it's infrastructure dependency even as local variable
351
+ if (this.isLikelyExternalDependency(className, sourceFile)) {
352
+ if (this.verbose) {
353
+ console.log(`[DEBUG] ✅ C014: ${className} is violation - infrastructure dependency even as local variable`);
354
+ }
355
+ return true;
356
+ }
357
+
358
+ if (this.verbose) {
359
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - local variable in method`);
360
+ }
361
+ return false;
362
+ }
363
+
364
+ // 6. Main heuristic: Flag if assigned to this.* (field or in constructor/method)
365
+ if (context.isAssignedToThis) {
366
+ // Check if target class suggests external dependency
367
+ if (this.isLikelyExternalDependency(className, sourceFile)) {
368
+ if (this.verbose) {
369
+ console.log(`[DEBUG] ✅ C014: ${className} is violation - assigned to this and external dependency`);
370
+ }
371
+ return true;
372
+ }
373
+ }
374
+
375
+ // 7. Skip if it's service locator pattern (centralized API client configuration)
376
+ if (this.isServiceLocatorPattern(context, sourceFile)) {
377
+ if (this.verbose) {
378
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Service locator pattern`);
379
+ }
380
+ return false;
381
+ }
382
+
383
+ // 8. Flag if class has infrastructure patterns and no DI decorators
384
+ if (this.hasInfraPattern(className) && !context.hasDecorators) {
385
+ if (this.verbose) {
386
+ console.log(`[DEBUG] ✅ C014: ${className} is violation - has infra pattern`);
387
+ }
388
+ return true;
389
+ }
390
+
391
+ if (this.verbose) {
392
+ console.log(`[DEBUG] 🔍 C014: ${className} is NOT a violation`);
393
+ }
394
+
395
+ return false;
396
+ }
397
+
398
+ isLikelyExternalDependency(className, sourceFile) {
399
+ if (this.verbose) {
400
+ console.log(`[DEBUG] 🔍 C014: Checking if ${className} is external dependency`);
401
+ }
402
+
403
+ // Check if class name suggests infrastructure/external service
404
+ const hasInfraPattern = this.config.infraPatterns.some(pattern =>
405
+ className.includes(pattern)
406
+ );
407
+
408
+ if (hasInfraPattern) {
409
+ if (this.verbose) {
410
+ console.log(`[DEBUG] ✅ C014: ${className} has infra pattern`);
411
+ }
412
+ return true;
413
+ }
414
+
415
+ // Check import statements to see if it's from external module
416
+ const imports = sourceFile.getImportDeclarations();
417
+ for (const importDecl of imports) {
418
+ const namedImports = importDecl.getNamedImports();
419
+ for (const namedImport of namedImports) {
420
+ if (namedImport.getName() === className) {
421
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
422
+ if (this.verbose) {
423
+ console.log(`[DEBUG] 🔍 C014: ${className} imported from: ${moduleSpecifier}`);
424
+ }
425
+ // Check if imported from infrastructure/adapter paths
426
+ if (this.isInfrastructurePath(moduleSpecifier)) {
427
+ if (this.verbose) {
428
+ console.log(`[DEBUG] ✅ C014: ${className} from infrastructure path`);
429
+ }
430
+ return true;
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ if (this.verbose) {
437
+ console.log(`[DEBUG] 🔍 C014: ${className} is NOT external dependency`);
438
+ }
439
+ return false;
440
+ }
441
+
442
+ isInfrastructurePath(modulePath) {
443
+ const infraPaths = [
444
+ 'infra', 'infrastructure', 'adapters', 'clients',
445
+ 'repositories', 'services', 'gateways', 'providers'
446
+ ];
447
+
448
+ // Check explicit infra path keywords
449
+ if (infraPaths.some(path => modulePath.includes(path))) {
450
+ return true;
451
+ }
452
+
453
+ // Check common external infrastructure packages
454
+ const infraPackages = [
455
+ '@aws-sdk/', 'aws-sdk', 'redis', 'mysql', 'postgresql', 'prisma',
456
+ 'mongoose', 'sequelize', 'typeorm', 'knex', 'pg', 'mysql2',
457
+ 's3-sync-client', 'firebase', 'googleapis', 'stripe',
458
+ 'twilio', 'sendgrid', 'nodemailer', 'kafka', 'rabbitmq',
459
+ 'elasticsearch', 'mongodb', 'cassandra'
460
+ ];
461
+
462
+ return infraPackages.some(pkg => modulePath.includes(pkg));
463
+ }
464
+
465
+ hasInfraPattern(className) {
466
+ return this.config.infraPatterns.some(pattern =>
467
+ className.includes(pattern)
468
+ );
469
+ }
470
+
471
+ isExceptionClass(className, sourceFile) {
472
+ // First check by naming convention (fast path)
473
+ const errorPatterns = [
474
+ 'Error', 'Exception', 'Fault', 'Failure'
475
+ ];
476
+
477
+ const hasErrorName = errorPatterns.some(pattern =>
478
+ className.endsWith(pattern) || className.includes(pattern)
479
+ );
480
+
481
+ if (hasErrorName) {
482
+ return true;
483
+ }
484
+
485
+ // Check inheritance hierarchy using semantic analysis
486
+ if (this.semanticEngine && sourceFile) {
487
+ try {
488
+ return this.inheritsFromErrorClass(className, sourceFile);
489
+ } catch (error) {
490
+ if (this.verbose) {
491
+ console.log(`[DEBUG] 🔍 C014: Could not check inheritance for ${className}: ${error.message}`);
492
+ }
493
+ // Fall back to name-based check only
494
+ return false;
495
+ }
496
+ }
497
+
498
+ return false;
499
+ }
500
+
501
+ inheritsFromErrorClass(className, sourceFile) {
502
+ // Find class declaration in current file
503
+ const classDecl = sourceFile.getClasses().find(cls => cls.getName() === className);
504
+
505
+ if (!classDecl) {
506
+ // Class might be imported, try to resolve it
507
+ return this.isImportedErrorClass(className, sourceFile);
508
+ }
509
+
510
+ // Check direct inheritance
511
+ const extendsClauses = classDecl.getExtends();
512
+ if (!extendsClauses) {
513
+ return false;
514
+ }
515
+
516
+ const baseClassName = extendsClauses.getExpression().getText();
517
+
518
+ // Check if directly extends Error-like class
519
+ const errorBaseClasses = [
520
+ 'Error', 'TypeError', 'ReferenceError', 'SyntaxError',
521
+ 'RangeError', 'EvalError', 'URIError', 'AggregateError'
522
+ ];
523
+
524
+ if (errorBaseClasses.includes(baseClassName)) {
525
+ if (this.verbose) {
526
+ console.log(`[DEBUG] 🔍 C014: ${className} extends ${baseClassName} (Error class)`);
527
+ }
528
+ return true;
529
+ }
530
+
531
+ // Recursively check inheritance chain
532
+ return this.inheritsFromErrorClass(baseClassName, sourceFile);
533
+ }
534
+
535
+ isImportedErrorClass(className, sourceFile) {
536
+ // Check imports to see if className is imported from error/exception modules
537
+ const imports = sourceFile.getImportDeclarations();
538
+
539
+ for (const importDecl of imports) {
540
+ const namedImports = importDecl.getNamedImports();
541
+ for (const namedImport of namedImports) {
542
+ if (namedImport.getName() === className) {
543
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
544
+
545
+ // Check if imported from error/exception related modules
546
+ const errorModulePatterns = [
547
+ 'error', 'exception', 'http-exception', 'custom-error',
548
+ '../exceptions', './exceptions', '/errors/', '/exceptions/'
549
+ ];
550
+
551
+ const isFromErrorModule = errorModulePatterns.some(pattern =>
552
+ moduleSpecifier.toLowerCase().includes(pattern)
553
+ );
554
+
555
+ if (isFromErrorModule) {
556
+ if (this.verbose) {
557
+ console.log(`[DEBUG] 🔍 C014: ${className} imported from error module: ${moduleSpecifier}`);
558
+ }
559
+ return true;
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ return false;
566
+ }
567
+
568
+ isEntityClass(className) {
569
+ // Common entity/model class patterns
570
+ const entityPatterns = [
571
+ 'Entity', 'Model', 'Schema', 'Document', 'Dto', 'DTO'
572
+ ];
573
+
574
+ return entityPatterns.some(pattern =>
575
+ className.endsWith(pattern)
576
+ );
577
+ }
578
+
579
+ isCommandPattern(className) {
580
+ // Command pattern classes (value objects for operations)
581
+ const commandPatterns = [
582
+ 'Command', 'Request', 'Query', 'Operation',
583
+ 'Action', 'Task', 'Job'
584
+ ];
585
+
586
+ return commandPatterns.some(pattern =>
587
+ className.endsWith(pattern)
588
+ );
589
+ }
590
+
591
+ isServiceLocatorPattern(context, sourceFile) {
592
+ // Check if we're in an object literal assignment that looks like service locator
593
+ if (!context.isLocalVariable) {
594
+ return false;
595
+ }
596
+
597
+ // Additional check: if file contains many similar API instantiations,
598
+ // it's likely a service locator pattern
599
+ const fileText = sourceFile.getFullText();
600
+ const newExpressionCount = (fileText.match(/new \w+Api\(\)/g) || []).length;
601
+ if (newExpressionCount >= 5) {
602
+ // Many API instantiations suggest service locator pattern
603
+ if (this.verbose) {
604
+ console.log(`[DEBUG] 🔍 C014: Found ${newExpressionCount} API instantiations - likely service locator`);
605
+ }
606
+ return true;
607
+ }
608
+
609
+ // Check for service locator variable names in file text
610
+ const serviceLocatorPatterns = [
611
+ 'apiClient', 'serviceContainer', 'container', 'services',
612
+ 'clients', 'providers', 'factories', 'registry'
613
+ ];
614
+
615
+ const isServiceLocator = serviceLocatorPatterns.some(pattern =>
616
+ fileText.includes(`export const ${pattern}`) ||
617
+ fileText.includes(`const ${pattern}`)
618
+ );
619
+
620
+ if (isServiceLocator && this.verbose) {
621
+ console.log(`[DEBUG] 🔍 C014: Detected service locator pattern from variable name`);
622
+ }
623
+
624
+ return isServiceLocator;
625
+ }
626
+
627
+ hasDecorators(node, decoratorNames) {
628
+ const decorators = node.getDecorators?.() || [];
629
+ return decorators.some(decorator => {
630
+ const decoratorText = decorator.getText();
631
+ return decoratorNames.some(name => decoratorText.includes(name));
632
+ });
633
+ }
634
+
635
+ shouldSkipFile(filePath) {
636
+ return this.config.excludePatterns.some(pattern => {
637
+ const regex = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
638
+ return new RegExp(regex).test(filePath);
639
+ });
640
+ }
641
+
642
+ /**
643
+ * Check if this is a Singleton pattern (self-instantiation)
644
+ */
645
+ isSingletonPattern(className, context, sourceFile) {
646
+ // Must be in a method (not constructor)
647
+ if (!context.isInMethod || context.isInConstructor) {
648
+ return false;
649
+ }
650
+
651
+ // Must be instantiating the same class we're in
652
+ const classDeclaration = this.findContainingClass(context, sourceFile);
653
+ if (!classDeclaration) {
654
+ return false;
655
+ }
656
+
657
+ const currentClassName = classDeclaration.getName();
658
+ if (currentClassName !== className) {
659
+ return false;
660
+ }
661
+
662
+ // Method name should suggest singleton (getInstance, instance, create, etc.)
663
+ const methodName = context.parentFunction?.getName?.() || '';
664
+ const singletonMethods = [
665
+ 'getInstance', 'instance', 'getinstance', 'create', 'createInstance',
666
+ 'singleton', 'getSingleton', 'getSharedInstance', 'shared'
667
+ ];
668
+
669
+ const isSingletonMethod = singletonMethods.some(pattern =>
670
+ methodName.toLowerCase().includes(pattern.toLowerCase())
671
+ );
672
+
673
+ // Must be a static method
674
+ const isStaticMethod = context.parentFunction?.getModifiers?.()
675
+ ?.some(modifier => modifier.getKind() === SyntaxKind.StaticKeyword) || false;
676
+
677
+ if (this.verbose && isSingletonMethod && isStaticMethod) {
678
+ console.log(`[DEBUG] 🔍 C014: Detected Singleton pattern: ${currentClassName}.${methodName}()`);
679
+ }
680
+
681
+ return isSingletonMethod && isStaticMethod;
682
+ }
683
+
684
+ /**
685
+ * Find the containing class declaration
686
+ */
687
+ findContainingClass(context, sourceFile) {
688
+ let current = context.parentFunction?.getParent();
689
+
690
+ while (current) {
691
+ if (current.getKind() === SyntaxKind.ClassDeclaration) {
692
+ return current;
693
+ }
694
+ current = current.getParent();
695
+ }
696
+
697
+ return null;
698
+ }
699
+
700
+ calculateConfidence(className, context) {
701
+ let confidence = 0.6; // Base confidence
702
+
703
+ // Increase confidence for infrastructure patterns
704
+ if (this.hasInfraPattern(className)) {
705
+ confidence += 0.2;
706
+ }
707
+
708
+ // Increase confidence if assigned to this.* (dependency field)
709
+ if (context.isAssignedToThis) {
710
+ confidence += 0.2;
711
+ }
712
+
713
+ // Decrease confidence for value objects
714
+ if (this.config.allowedValueObjects.includes(className)) {
715
+ confidence -= 0.3;
716
+ }
717
+
718
+ // Decrease confidence if has DI decorators
719
+ if (context.hasDecorators) {
720
+ confidence -= 0.4;
721
+ }
722
+
723
+ return Math.max(0.3, Math.min(1.0, confidence));
724
+ }
725
+
726
+ buildViolationMessage(expr) {
727
+ const { className, context } = expr;
728
+
729
+ if (context.isInConstructor && context.isAssignedToThis) {
730
+ return `Direct instantiation of '${className}' in constructor. Consider injecting this dependency instead of creating it directly.`;
731
+ }
732
+
733
+ if (context.isInMethod && context.isAssignedToThis) {
734
+ return `Direct instantiation of '${className}' assigned to instance field. Consider injecting this dependency.`;
735
+ }
736
+
737
+ return `Direct instantiation of '${className}'. Consider using dependency injection or factory pattern.`;
738
+ }
739
+
740
+ buildSuggestion(expr) {
741
+ const { className, context } = expr;
742
+
743
+ if (context.isInConstructor) {
744
+ return `Inject ${className} via constructor parameter: constructor(private ${className.toLowerCase()}: ${className})`;
745
+ }
746
+
747
+ return `Consider injecting ${className} as a dependency or using a factory pattern`;
748
+ }
749
+ }
750
+
751
+ module.exports = C014SymbolBasedAnalyzer;