@sun-asterisk/sunlint 1.3.18 → 1.3.19

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 (34) hide show
  1. package/config/rules/enhanced-rules-registry.json +77 -18
  2. package/core/cli-program.js +2 -1
  3. package/core/github-annotate-service.js +89 -0
  4. package/core/output-service.js +25 -0
  5. package/core/summary-report-service.js +30 -30
  6. package/package.json +3 -2
  7. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
  8. package/rules/common/C017_constructor_logic/analyzer.js +137 -503
  9. package/rules/common/C017_constructor_logic/config.json +50 -0
  10. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
  11. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
  12. package/rules/security/S011_secure_guid_generation/README.md +255 -0
  13. package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
  14. package/rules/security/S011_secure_guid_generation/config.json +56 -0
  15. package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
  16. package/rules/security/S028_file_upload_size_limits/README.md +537 -0
  17. package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
  18. package/rules/security/S028_file_upload_size_limits/config.json +186 -0
  19. package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
  20. package/rules/security/S041_session_token_invalidation/README.md +303 -0
  21. package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
  22. package/rules/security/S041_session_token_invalidation/config.json +175 -0
  23. package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
  24. package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
  25. package/rules/security/S044_re_authentication_required/README.md +136 -0
  26. package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
  27. package/rules/security/S044_re_authentication_required/config.json +161 -0
  28. package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
  29. package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
  30. package/rules/security/S045_brute_force_protection/README.md +345 -0
  31. package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
  32. package/rules/security/S045_brute_force_protection/config.json +139 -0
  33. package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
  34. package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
@@ -5,7 +5,7 @@ class C014SymbolBasedAnalyzer {
5
5
  constructor(semanticEngine = null) {
6
6
  this.semanticEngine = semanticEngine;
7
7
  this.verbose = false;
8
-
8
+
9
9
  // Configuration
10
10
  this.config = {
11
11
  // Built-in classes that are allowed
@@ -17,27 +17,34 @@ class C014SymbolBasedAnalyzer {
17
17
  'TextEncoder', 'TextDecoder', 'MessageChannel', 'MessagePort',
18
18
  'Worker', 'SharedWorker', 'EventSource', 'WebSocket'
19
19
  ],
20
-
20
+
21
21
  // Value objects/DTOs that are typically safe to instantiate
22
22
  allowedValueObjects: [
23
23
  'Money', 'Price', 'Currency', 'Quantity', 'Amount',
24
24
  'Email', 'Phone', 'Address', 'Name', 'Id', 'UserId',
25
25
  'UUID', 'Timestamp', 'Duration', 'Range'
26
26
  ],
27
-
27
+
28
+ // Query builder and utility classes that are safe to instantiate
29
+ allowedQueryBuilders: [
30
+ 'Brackets', 'SelectQueryBuilder', 'QueryBuilder',
31
+ 'WhereExpression', 'OrderByCondition', 'JoinAttribute',
32
+ 'Subquery', 'CTE', 'Raw'
33
+ ],
34
+
28
35
  // Infrastructure patterns that suggest external dependencies
29
36
  infraPatterns: [
30
37
  'Client', 'Repository', 'Service', 'Gateway', 'Adapter',
31
38
  'Provider', 'Factory', 'Builder', 'Manager', 'Handler',
32
39
  'Controller', 'Processor', 'Validator', 'Logger'
33
40
  ],
34
-
41
+
35
42
  // DI decorators that indicate proper injection
36
43
  diDecorators: [
37
44
  'Injectable', 'Inject', 'Autowired', 'Component',
38
45
  'Service', 'Repository', 'Controller', 'autoInjectable'
39
46
  ],
40
-
47
+
41
48
  // Patterns to exclude from analysis
42
49
  excludePatterns: [
43
50
  '**/*.test.ts', '**/*.spec.ts', '**/*.test.js', '**/*.spec.js',
@@ -55,29 +62,37 @@ class C014SymbolBasedAnalyzer {
55
62
 
56
63
  async analyzeFileBasic(filePath, options = {}) {
57
64
  const violations = [];
58
-
65
+
59
66
  try {
67
+ // Check if semantic engine is initialized
68
+ if (!this.semanticEngine || !this.semanticEngine.project) {
69
+ throw new Error('Semantic engine not initialized');
70
+ }
71
+
60
72
  // Try different approaches to get the source file
61
73
  let sourceFile = this.semanticEngine.project.getSourceFile(filePath);
62
-
74
+
63
75
  // If not found by full path, try by filename
64
76
  if (!sourceFile) {
65
77
  const fileName = filePath.split('/').pop();
66
78
  const allFiles = this.semanticEngine.project.getSourceFiles();
67
79
  sourceFile = allFiles.find(f => f.getBaseName() === fileName);
68
80
  }
69
-
81
+
70
82
  // If still not found, try to add the file
71
83
  if (!sourceFile) {
72
84
  try {
73
- if (require('fs').existsSync(filePath)) {
85
+ const fs = require('fs');
86
+ if (fs.existsSync(filePath)) {
74
87
  sourceFile = this.semanticEngine.project.addSourceFileAtPath(filePath);
75
88
  }
76
89
  } catch (addError) {
77
- // Fall through to error below
90
+ if (this.verbose) {
91
+ console.error(`[DEBUG] ❌ C014: Failed to add source file: ${addError.message}`);
92
+ }
78
93
  }
79
94
  }
80
-
95
+
81
96
  if (!sourceFile) {
82
97
  throw new Error(`Source file not found: ${filePath}`);
83
98
  }
@@ -96,7 +111,7 @@ class C014SymbolBasedAnalyzer {
96
111
 
97
112
  // Find all new expressions that might violate DI principles
98
113
  const newExpressions = this.findProblematicNewExpressions(sourceFile);
99
-
114
+
100
115
  for (const expr of newExpressions) {
101
116
  if (this.isDependencyInjectionViolation(expr, sourceFile)) {
102
117
  violations.push({
@@ -128,21 +143,21 @@ class C014SymbolBasedAnalyzer {
128
143
 
129
144
  findProblematicNewExpressions(sourceFile) {
130
145
  const expressions = [];
131
-
132
- function traverse(node) {
146
+
147
+ const traverse = (node) => {
133
148
  if (node.getKind() === SyntaxKind.NewExpression) {
134
149
  const newExpr = node;
135
150
  const expression = newExpr.getExpression();
136
-
151
+
137
152
  // Get class name and context information
138
153
  const className = this.getClassName(expression);
139
154
  const position = sourceFile.getLineAndColumnAtPos(newExpr.getStart());
140
155
  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
-
156
+
157
+ if (this.verbose) {
158
+ console.log(`[DEBUG] 🔍 C014: Found new expression: ${className} at line ${position.line}`);
159
+ }
160
+
146
161
  if (className) {
147
162
  expressions.push({
148
163
  node: newExpr,
@@ -156,28 +171,34 @@ class C014SymbolBasedAnalyzer {
156
171
  }
157
172
 
158
173
  // Traverse children
159
- node.forEachChild(child => traverse.call(this, child));
160
- }
174
+ node.forEachChild(child => traverse(child));
175
+ };
176
+
177
+ traverse(sourceFile);
161
178
 
162
- traverse.call(this, sourceFile);
163
-
164
179
  if (this.verbose) {
165
180
  console.log(`[DEBUG] 🔍 C014: Found ${expressions.length} new expressions total`);
166
181
  }
167
-
182
+
168
183
  return expressions;
169
184
  }
170
185
 
171
186
  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();
187
+ try {
188
+ if (expression.getKind() === SyntaxKind.Identifier) {
189
+ return expression.getText();
190
+ }
191
+
192
+ // Handle qualified names like MyNamespace.MyClass
193
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
194
+ return expression.getName();
195
+ }
196
+ } catch (error) {
197
+ if (this.verbose) {
198
+ console.error(`[DEBUG] ❌ C014: Error getting class name: ${error.message}`);
199
+ }
179
200
  }
180
-
201
+
181
202
  return null;
182
203
  }
183
204
 
@@ -189,70 +210,86 @@ class C014SymbolBasedAnalyzer {
189
210
  isLocalVariable: false,
190
211
  isReturnValue: false,
191
212
  isImmediateUse: false,
213
+ isCallArgument: false,
192
214
  parentFunction: null,
193
215
  hasDecorators: false
194
216
  };
195
217
 
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;
218
+ try {
219
+ let current = newExpressionNode.getParent();
220
+
221
+ while (current) {
222
+ switch (current.getKind()) {
223
+ case SyntaxKind.Constructor:
224
+ context.isInConstructor = true;
225
+ context.parentFunction = current;
226
+ break;
227
+
228
+ case SyntaxKind.MethodDeclaration:
229
+ context.isInMethod = true;
230
+ context.parentFunction = current;
231
+ break;
232
+
233
+ case SyntaxKind.BinaryExpression:
234
+ // Check if it's assignment to this.property
235
+ const binaryExpr = current;
236
+ if (binaryExpr.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
237
+ const left = binaryExpr.getLeft();
238
+ if (left.getKind() === SyntaxKind.PropertyAccessExpression) {
239
+ const propAccess = left;
240
+ if (propAccess.getExpression().getKind() === SyntaxKind.ThisKeyword) {
241
+ context.isAssignedToThis = true;
242
+ }
219
243
  }
220
244
  }
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;
245
+ break;
246
+
247
+ case SyntaxKind.PropertyDeclaration:
248
+ // Check if this new expression is in a class property initializer
249
+ const propDecl = current;
250
+ const initializer = propDecl.getInitializer();
251
+ if (initializer && this.containsNewExpression(initializer, newExpressionNode)) {
252
+ context.isAssignedToThis = true; // Class property is effectively "this.property"
253
+ }
254
+ break;
255
+
256
+ case SyntaxKind.VariableDeclaration:
257
+ context.isLocalVariable = true;
258
+ break;
259
+
260
+ case SyntaxKind.ReturnStatement:
261
+ context.isReturnValue = true;
262
+ break;
263
+
264
+ case SyntaxKind.CallExpression:
265
+ const callExpr = current;
266
+ // Check for immediate method call like new Date().getTime()
267
+ if (callExpr.getExpression() === newExpressionNode) {
268
+ context.isImmediateUse = true;
269
+ } else {
270
+ // Check if new expression is passed as argument to a function call
271
+ const args = callExpr.getArguments();
272
+ for (const arg of args) {
273
+ if (this.containsNewExpression(arg, newExpressionNode)) {
274
+ context.isCallArgument = true;
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ break;
280
+
281
+ case SyntaxKind.ClassDeclaration:
282
+ // Check for DI decorators on the class
283
+ context.hasDecorators = this.hasDecorators(current, this.config.diDecorators);
284
+ break;
285
+ }
286
+
287
+ current = current.getParent();
288
+ }
289
+ } catch (error) {
290
+ if (this.verbose) {
291
+ console.error(`[DEBUG] ❌ C014: Error analyzing context: ${error.message}`);
253
292
  }
254
-
255
- current = current.getParent();
256
293
  }
257
294
 
258
295
  return context;
@@ -262,15 +299,19 @@ class C014SymbolBasedAnalyzer {
262
299
  if (node === targetNewExpr) {
263
300
  return true;
264
301
  }
265
-
302
+
266
303
  let found = false;
267
- node.forEachChild(child => {
268
- if (found) return;
269
- if (this.containsNewExpression(child, targetNewExpr)) {
270
- found = true;
271
- }
272
- });
273
-
304
+ try {
305
+ node.forEachChild(child => {
306
+ if (found) return;
307
+ if (this.containsNewExpression(child, targetNewExpr)) {
308
+ found = true;
309
+ }
310
+ });
311
+ } catch (error) {
312
+ // Silently handle errors in tree traversal
313
+ }
314
+
274
315
  return found;
275
316
  }
276
317
 
@@ -285,6 +326,7 @@ class C014SymbolBasedAnalyzer {
285
326
  isLocalVariable: context.isLocalVariable,
286
327
  isImmediateUse: context.isImmediateUse,
287
328
  isReturnValue: context.isReturnValue,
329
+ isCallArgument: context.isCallArgument,
288
330
  hasDecorators: context.hasDecorators
289
331
  });
290
332
  }
@@ -297,7 +339,23 @@ class C014SymbolBasedAnalyzer {
297
339
  return false;
298
340
  }
299
341
 
300
- // 2. Skip exception/error classes
342
+ // 2. Skip query builder classes (TypeORM Brackets, etc.)
343
+ if (this.config.allowedQueryBuilders.includes(className)) {
344
+ if (this.verbose) {
345
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - query builder class`);
346
+ }
347
+ return false;
348
+ }
349
+
350
+ // 3. Skip if it's passed as an argument to a function (like query builder methods)
351
+ if (context.isCallArgument) {
352
+ if (this.verbose) {
353
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - passed as function argument`);
354
+ }
355
+ return false;
356
+ }
357
+
358
+ // 4. Skip exception/error classes
301
359
  if (this.isExceptionClass(className, sourceFile)) {
302
360
  if (this.verbose) {
303
361
  console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Exception/Error class`);
@@ -305,7 +363,23 @@ class C014SymbolBasedAnalyzer {
305
363
  return false;
306
364
  }
307
365
 
308
- // 4. Skip entity/model classes (data structures)
366
+ // 5. Skip if it's immediate usage (not stored as dependency)
367
+ if (context.isImmediateUse || context.isReturnValue) {
368
+ if (this.verbose) {
369
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - immediate use or return value`);
370
+ }
371
+ return false;
372
+ }
373
+
374
+ // 6. Skip Singleton pattern (self-instantiation in getInstance-like methods)
375
+ if (this.isSingletonPattern(className, context, sourceFile)) {
376
+ if (this.verbose) {
377
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Singleton pattern`);
378
+ }
379
+ return false;
380
+ }
381
+
382
+ // 7. Skip entity/model classes (data structures)
309
383
  if (this.isEntityClass(className)) {
310
384
  if (this.verbose) {
311
385
  console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Entity/Model class`);
@@ -313,7 +387,7 @@ class C014SymbolBasedAnalyzer {
313
387
  return false;
314
388
  }
315
389
 
316
- // 5. Skip command pattern classes (value objects for operations)
390
+ // 8. Skip command pattern classes (value objects for operations)
317
391
  if (this.isCommandPattern(className)) {
318
392
  if (this.verbose) {
319
393
  console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Command pattern class`);
@@ -321,7 +395,7 @@ class C014SymbolBasedAnalyzer {
321
395
  return false;
322
396
  }
323
397
 
324
- // 6. Skip value objects/DTOs (configurable)
398
+ // 9. Skip value objects/DTOs (configurable)
325
399
  if (this.config.allowedValueObjects.includes(className)) {
326
400
  if (this.verbose) {
327
401
  console.log(`[DEBUG] 🔍 C014: Skipping ${className} - allowed value object`);
@@ -329,23 +403,15 @@ class C014SymbolBasedAnalyzer {
329
403
  return false;
330
404
  }
331
405
 
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)) {
406
+ // 10. Skip service locator pattern (centralized API client configuration)
407
+ if (this.isServiceLocatorPattern(context, sourceFile)) {
342
408
  if (this.verbose) {
343
- console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Singleton pattern`);
409
+ console.log(`[DEBUG] 🔍 C014: Skipping ${className} - Service locator pattern`);
344
410
  }
345
411
  return false;
346
412
  }
347
413
 
348
- // 5. Skip if it's a local variable in method (not dependency field) UNLESS it's infrastructure
414
+ // 11. Skip if it's a local variable in method (not dependency field) UNLESS it's infrastructure
349
415
  if (context.isInMethod && context.isLocalVariable && !context.isAssignedToThis) {
350
416
  // Exception: Still flag if it's infrastructure dependency even as local variable
351
417
  if (this.isLikelyExternalDependency(className, sourceFile)) {
@@ -354,14 +420,14 @@ class C014SymbolBasedAnalyzer {
354
420
  }
355
421
  return true;
356
422
  }
357
-
423
+
358
424
  if (this.verbose) {
359
425
  console.log(`[DEBUG] 🔍 C014: Skipping ${className} - local variable in method`);
360
426
  }
361
427
  return false;
362
428
  }
363
429
 
364
- // 6. Main heuristic: Flag if assigned to this.* (field or in constructor/method)
430
+ // 12. Main heuristic: Flag if assigned to this.* (field or in constructor/method)
365
431
  if (context.isAssignedToThis) {
366
432
  // Check if target class suggests external dependency
367
433
  if (this.isLikelyExternalDependency(className, sourceFile)) {
@@ -372,16 +438,8 @@ class C014SymbolBasedAnalyzer {
372
438
  }
373
439
  }
374
440
 
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) {
441
+ // 13. Flag if class has infrastructure patterns and no DI decorators
442
+ if (this.hasInfraPattern(className) && !context.hasDecorators && context.isAssignedToThis) {
385
443
  if (this.verbose) {
386
444
  console.log(`[DEBUG] ✅ C014: ${className} is violation - has infra pattern`);
387
445
  }
@@ -399,9 +457,9 @@ class C014SymbolBasedAnalyzer {
399
457
  if (this.verbose) {
400
458
  console.log(`[DEBUG] 🔍 C014: Checking if ${className} is external dependency`);
401
459
  }
402
-
460
+
403
461
  // Check if class name suggests infrastructure/external service
404
- const hasInfraPattern = this.config.infraPatterns.some(pattern =>
462
+ const hasInfraPattern = this.config.infraPatterns.some(pattern =>
405
463
  className.includes(pattern)
406
464
  );
407
465
 
@@ -413,24 +471,30 @@ class C014SymbolBasedAnalyzer {
413
471
  }
414
472
 
415
473
  // 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)) {
474
+ try {
475
+ const imports = sourceFile.getImportDeclarations();
476
+ for (const importDecl of imports) {
477
+ const namedImports = importDecl.getNamedImports();
478
+ for (const namedImport of namedImports) {
479
+ if (namedImport.getName() === className) {
480
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
427
481
  if (this.verbose) {
428
- console.log(`[DEBUG] C014: ${className} from infrastructure path`);
482
+ console.log(`[DEBUG] 🔍 C014: ${className} imported from: ${moduleSpecifier}`);
483
+ }
484
+ // Check if imported from infrastructure/adapter paths
485
+ if (this.isInfrastructurePath(moduleSpecifier)) {
486
+ if (this.verbose) {
487
+ console.log(`[DEBUG] ✅ C014: ${className} from infrastructure path`);
488
+ }
489
+ return true;
429
490
  }
430
- return true;
431
491
  }
432
492
  }
433
493
  }
494
+ } catch (error) {
495
+ if (this.verbose) {
496
+ console.error(`[DEBUG] ❌ C014: Error checking imports: ${error.message}`);
497
+ }
434
498
  }
435
499
 
436
500
  if (this.verbose) {
@@ -441,15 +505,15 @@ class C014SymbolBasedAnalyzer {
441
505
 
442
506
  isInfrastructurePath(modulePath) {
443
507
  const infraPaths = [
444
- 'infra', 'infrastructure', 'adapters', 'clients',
508
+ 'infra', 'infrastructure', 'adapters', 'clients',
445
509
  'repositories', 'services', 'gateways', 'providers'
446
510
  ];
447
-
511
+
448
512
  // Check explicit infra path keywords
449
513
  if (infraPaths.some(path => modulePath.includes(path))) {
450
514
  return true;
451
515
  }
452
-
516
+
453
517
  // Check common external infrastructure packages
454
518
  const infraPackages = [
455
519
  '@aws-sdk/', 'aws-sdk', 'redis', 'mysql', 'postgresql', 'prisma',
@@ -458,12 +522,12 @@ class C014SymbolBasedAnalyzer {
458
522
  'twilio', 'sendgrid', 'nodemailer', 'kafka', 'rabbitmq',
459
523
  'elasticsearch', 'mongodb', 'cassandra'
460
524
  ];
461
-
525
+
462
526
  return infraPackages.some(pkg => modulePath.includes(pkg));
463
527
  }
464
528
 
465
529
  hasInfraPattern(className) {
466
- return this.config.infraPatterns.some(pattern =>
530
+ return this.config.infraPatterns.some(pattern =>
467
531
  className.includes(pattern)
468
532
  );
469
533
  }
@@ -473,11 +537,11 @@ class C014SymbolBasedAnalyzer {
473
537
  const errorPatterns = [
474
538
  'Error', 'Exception', 'Fault', 'Failure'
475
539
  ];
476
-
477
- const hasErrorName = errorPatterns.some(pattern =>
540
+
541
+ const hasErrorName = errorPatterns.some(pattern =>
478
542
  className.endsWith(pattern) || className.includes(pattern)
479
543
  );
480
-
544
+
481
545
  if (hasErrorName) {
482
546
  return true;
483
547
  }
@@ -494,74 +558,87 @@ class C014SymbolBasedAnalyzer {
494
558
  return false;
495
559
  }
496
560
  }
497
-
561
+
498
562
  return false;
499
563
  }
500
564
 
501
565
  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
- }
566
+ try {
567
+ // Find class declaration in current file
568
+ const classDecl = sourceFile.getClasses().find(cls => cls.getName() === className);
509
569
 
510
- // Check direct inheritance
511
- const extendsClauses = classDecl.getExtends();
512
- if (!extendsClauses) {
513
- return false;
514
- }
570
+ if (!classDecl) {
571
+ // Class might be imported, try to resolve it
572
+ return this.isImportedErrorClass(className, sourceFile);
573
+ }
515
574
 
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)) {
575
+ // Check direct inheritance
576
+ const extendsClauses = classDecl.getExtends();
577
+ if (!extendsClauses) {
578
+ return false;
579
+ }
580
+
581
+ const baseClassName = extendsClauses.getExpression().getText();
582
+
583
+ // Check if directly extends Error-like class
584
+ const errorBaseClasses = [
585
+ 'Error', 'TypeError', 'ReferenceError', 'SyntaxError',
586
+ 'RangeError', 'EvalError', 'URIError', 'AggregateError'
587
+ ];
588
+
589
+ if (errorBaseClasses.includes(baseClassName)) {
590
+ if (this.verbose) {
591
+ console.log(`[DEBUG] 🔍 C014: ${className} extends ${baseClassName} (Error class)`);
592
+ }
593
+ return true;
594
+ }
595
+
596
+ // Recursively check inheritance chain
597
+ return this.inheritsFromErrorClass(baseClassName, sourceFile);
598
+ } catch (error) {
525
599
  if (this.verbose) {
526
- console.log(`[DEBUG] 🔍 C014: ${className} extends ${baseClassName} (Error class)`);
600
+ console.error(`[DEBUG] C014: Error checking inheritance: ${error.message}`);
527
601
  }
528
- return true;
602
+ return false;
529
603
  }
530
-
531
- // Recursively check inheritance chain
532
- return this.inheritsFromErrorClass(baseClassName, sourceFile);
533
604
  }
534
605
 
535
606
  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}`);
607
+ try {
608
+ // Check imports to see if className is imported from error/exception modules
609
+ const imports = sourceFile.getImportDeclarations();
610
+
611
+ for (const importDecl of imports) {
612
+ const namedImports = importDecl.getNamedImports();
613
+ for (const namedImport of namedImports) {
614
+ if (namedImport.getName() === className) {
615
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
616
+
617
+ // Check if imported from error/exception related modules
618
+ const errorModulePatterns = [
619
+ 'error', 'exception', 'http-exception', 'custom-error',
620
+ '../exceptions', './exceptions', '/errors/', '/exceptions/'
621
+ ];
622
+
623
+ const isFromErrorModule = errorModulePatterns.some(pattern =>
624
+ moduleSpecifier.toLowerCase().includes(pattern)
625
+ );
626
+
627
+ if (isFromErrorModule) {
628
+ if (this.verbose) {
629
+ console.log(`[DEBUG] 🔍 C014: ${className} imported from error module: ${moduleSpecifier}`);
630
+ }
631
+ return true;
558
632
  }
559
- return true;
560
633
  }
561
634
  }
562
635
  }
636
+ } catch (error) {
637
+ if (this.verbose) {
638
+ console.error(`[DEBUG] ❌ C014: Error checking imported error class: ${error.message}`);
639
+ }
563
640
  }
564
-
641
+
565
642
  return false;
566
643
  }
567
644
 
@@ -570,8 +647,8 @@ class C014SymbolBasedAnalyzer {
570
647
  const entityPatterns = [
571
648
  'Entity', 'Model', 'Schema', 'Document', 'Dto', 'DTO'
572
649
  ];
573
-
574
- return entityPatterns.some(pattern =>
650
+
651
+ return entityPatterns.some(pattern =>
575
652
  className.endsWith(pattern)
576
653
  );
577
654
  }
@@ -579,57 +656,71 @@ class C014SymbolBasedAnalyzer {
579
656
  isCommandPattern(className) {
580
657
  // Command pattern classes (value objects for operations)
581
658
  const commandPatterns = [
582
- 'Command', 'Request', 'Query', 'Operation',
659
+ 'Command', 'Request', 'Query', 'Operation',
583
660
  'Action', 'Task', 'Job'
584
661
  ];
585
-
586
- return commandPatterns.some(pattern =>
662
+
663
+ return commandPatterns.some(pattern =>
587
664
  className.endsWith(pattern)
588
665
  );
589
666
  }
590
667
 
591
668
  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
- }
669
+ try {
670
+ // Check if we're in an object literal assignment that looks like service locator
671
+ if (!context.isLocalVariable) {
672
+ return false;
673
+ }
596
674
 
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`);
675
+ // Additional check: if file contains many similar API instantiations,
676
+ // it's likely a service locator pattern
677
+ const fileText = sourceFile.getFullText();
678
+ const newExpressionCount = (fileText.match(/new \w+Api\(\)/g) || []).length;
679
+ if (newExpressionCount >= 5) {
680
+ // Many API instantiations suggest service locator pattern
681
+ if (this.verbose) {
682
+ console.log(`[DEBUG] 🔍 C014: Found ${newExpressionCount} API instantiations - likely service locator`);
683
+ }
684
+ return true;
605
685
  }
606
- return true;
607
- }
608
686
 
609
- // Check for service locator variable names in file text
610
- const serviceLocatorPatterns = [
611
- 'apiClient', 'serviceContainer', 'container', 'services',
612
- 'clients', 'providers', 'factories', 'registry'
613
- ];
687
+ // Check for service locator variable names in file text
688
+ const serviceLocatorPatterns = [
689
+ 'apiClient', 'serviceContainer', 'container', 'services',
690
+ 'clients', 'providers', 'factories', 'registry'
691
+ ];
614
692
 
615
- const isServiceLocator = serviceLocatorPatterns.some(pattern =>
616
- fileText.includes(`export const ${pattern}`) ||
617
- fileText.includes(`const ${pattern}`)
618
- );
693
+ const isServiceLocator = serviceLocatorPatterns.some(pattern =>
694
+ fileText.includes(`export const ${pattern}`) ||
695
+ fileText.includes(`const ${pattern}`)
696
+ );
619
697
 
620
- if (isServiceLocator && this.verbose) {
621
- console.log(`[DEBUG] 🔍 C014: Detected service locator pattern from variable name`);
622
- }
698
+ if (isServiceLocator && this.verbose) {
699
+ console.log(`[DEBUG] 🔍 C014: Detected service locator pattern from variable name`);
700
+ }
623
701
 
624
- return isServiceLocator;
702
+ return isServiceLocator;
703
+ } catch (error) {
704
+ if (this.verbose) {
705
+ console.error(`[DEBUG] ❌ C014: Error checking service locator pattern: ${error.message}`);
706
+ }
707
+ return false;
708
+ }
625
709
  }
626
710
 
627
711
  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
- });
712
+ try {
713
+ const decorators = node.getDecorators?.() || [];
714
+ return decorators.some(decorator => {
715
+ const decoratorText = decorator.getText();
716
+ return decoratorNames.some(name => decoratorText.includes(name));
717
+ });
718
+ } catch (error) {
719
+ if (this.verbose) {
720
+ console.error(`[DEBUG] ❌ C014: Error checking decorators: ${error.message}`);
721
+ }
722
+ return false;
723
+ }
633
724
  }
634
725
 
635
726
  shouldSkipFile(filePath) {
@@ -643,57 +734,70 @@ class C014SymbolBasedAnalyzer {
643
734
  * Check if this is a Singleton pattern (self-instantiation)
644
735
  */
645
736
  isSingletonPattern(className, context, sourceFile) {
646
- // Must be in a method (not constructor)
647
- if (!context.isInMethod || context.isInConstructor) {
648
- return false;
649
- }
737
+ try {
738
+ // Must be in a method (not constructor)
739
+ if (!context.isInMethod || context.isInConstructor) {
740
+ return false;
741
+ }
650
742
 
651
- // Must be instantiating the same class we're in
652
- const classDeclaration = this.findContainingClass(context, sourceFile);
653
- if (!classDeclaration) {
654
- return false;
655
- }
743
+ // Must be instantiating the same class we're in
744
+ const classDeclaration = this.findContainingClass(context, sourceFile);
745
+ if (!classDeclaration) {
746
+ return false;
747
+ }
656
748
 
657
- const currentClassName = classDeclaration.getName();
658
- if (currentClassName !== className) {
659
- return false;
660
- }
749
+ const currentClassName = classDeclaration.getName();
750
+ if (currentClassName !== className) {
751
+ return false;
752
+ }
661
753
 
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
- );
754
+ // Method name should suggest singleton (getInstance, instance, create, etc.)
755
+ const methodName = context.parentFunction?.getName?.() || '';
756
+ const singletonMethods = [
757
+ 'getInstance', 'instance', 'getinstance', 'create', 'createInstance',
758
+ 'singleton', 'getSingleton', 'getSharedInstance', 'shared'
759
+ ];
672
760
 
673
- // Must be a static method
674
- const isStaticMethod = context.parentFunction?.getModifiers?.()
675
- ?.some(modifier => modifier.getKind() === SyntaxKind.StaticKeyword) || false;
761
+ const isSingletonMethod = singletonMethods.some(pattern =>
762
+ methodName.toLowerCase().includes(pattern.toLowerCase())
763
+ );
676
764
 
677
- if (this.verbose && isSingletonMethod && isStaticMethod) {
678
- console.log(`[DEBUG] 🔍 C014: Detected Singleton pattern: ${currentClassName}.${methodName}()`);
679
- }
765
+ // Must be a static method
766
+ const isStaticMethod = context.parentFunction?.getModifiers?.()
767
+ ?.some(modifier => modifier.getKind() === SyntaxKind.StaticKeyword) || false;
680
768
 
681
- return isSingletonMethod && isStaticMethod;
769
+ if (this.verbose && isSingletonMethod && isStaticMethod) {
770
+ console.log(`[DEBUG] 🔍 C014: Detected Singleton pattern: ${currentClassName}.${methodName}()`);
771
+ }
772
+
773
+ return isSingletonMethod && isStaticMethod;
774
+ } catch (error) {
775
+ if (this.verbose) {
776
+ console.error(`[DEBUG] ❌ C014: Error checking singleton pattern: ${error.message}`);
777
+ }
778
+ return false;
779
+ }
682
780
  }
683
781
 
684
782
  /**
685
783
  * Find the containing class declaration
686
784
  */
687
785
  findContainingClass(context, sourceFile) {
688
- let current = context.parentFunction?.getParent();
689
-
690
- while (current) {
691
- if (current.getKind() === SyntaxKind.ClassDeclaration) {
692
- return current;
786
+ try {
787
+ let current = context.parentFunction?.getParent();
788
+
789
+ while (current) {
790
+ if (current.getKind() === SyntaxKind.ClassDeclaration) {
791
+ return current;
792
+ }
793
+ current = current.getParent();
794
+ }
795
+ } catch (error) {
796
+ if (this.verbose) {
797
+ console.error(`[DEBUG] ❌ C014: Error finding containing class: ${error.message}`);
693
798
  }
694
- current = current.getParent();
695
799
  }
696
-
800
+
697
801
  return null;
698
802
  }
699
803
 
@@ -725,27 +829,35 @@ class C014SymbolBasedAnalyzer {
725
829
 
726
830
  buildViolationMessage(expr) {
727
831
  const { className, context } = expr;
728
-
832
+
729
833
  if (context.isInConstructor && context.isAssignedToThis) {
730
834
  return `Direct instantiation of '${className}' in constructor. Consider injecting this dependency instead of creating it directly.`;
731
835
  }
732
-
836
+
733
837
  if (context.isInMethod && context.isAssignedToThis) {
734
838
  return `Direct instantiation of '${className}' assigned to instance field. Consider injecting this dependency.`;
735
839
  }
736
-
840
+
737
841
  return `Direct instantiation of '${className}'. Consider using dependency injection or factory pattern.`;
738
842
  }
739
843
 
740
844
  buildSuggestion(expr) {
741
845
  const { className, context } = expr;
742
-
846
+
743
847
  if (context.isInConstructor) {
744
- return `Inject ${className} via constructor parameter: constructor(private ${className.toLowerCase()}: ${className})`;
848
+ return `Inject ${className} via constructor parameter: constructor(private ${this.toLowerCamelCase(className)}: ${className})`;
745
849
  }
746
-
850
+
747
851
  return `Consider injecting ${className} as a dependency or using a factory pattern`;
748
852
  }
853
+
854
+ /**
855
+ * Convert class name to lower camel case for parameter names
856
+ */
857
+ toLowerCamelCase(str) {
858
+ if (!str || str.length === 0) return str;
859
+ return str.charAt(0).toLowerCase() + str.slice(1);
860
+ }
749
861
  }
750
862
 
751
863
  module.exports = C014SymbolBasedAnalyzer;