@sun-asterisk/sunlint 1.3.16 → 1.3.17

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 (50) hide show
  1. package/config/rule-analysis-strategies.js +3 -3
  2. package/config/rules/enhanced-rules-registry.json +40 -20
  3. package/core/cli-action-handler.js +2 -2
  4. package/core/config-merger.js +28 -6
  5. package/core/constants/defaults.js +1 -1
  6. package/core/file-targeting-service.js +72 -4
  7. package/core/output-service.js +21 -4
  8. package/engines/heuristic-engine.js +5 -0
  9. package/package.json +1 -1
  10. package/rules/common/C002_no_duplicate_code/README.md +115 -0
  11. package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
  12. package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
  13. package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
  14. package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
  15. package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
  16. package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
  17. package/rules/common/C008/analyzer.js +40 -0
  18. package/rules/common/C008/config.json +20 -0
  19. package/rules/common/C008/ts-morph-analyzer.js +1067 -0
  20. package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
  21. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
  22. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
  23. package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
  24. package/rules/common/C033_separate_service_repository/README.md +131 -20
  25. package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
  26. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
  27. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
  28. package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
  29. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
  30. package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
  31. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
  32. package/rules/docs/C002_no_duplicate_code.md +276 -11
  33. package/rules/index.js +5 -1
  34. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
  35. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
  36. package/rules/security/S010_no_insecure_encryption/README.md +78 -0
  37. package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
  38. package/rules/security/S013_tls_enforcement/README.md +51 -0
  39. package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
  40. package/rules/security/S013_tls_enforcement/config.json +41 -0
  41. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
  42. package/rules/security/S014_tls_version_enforcement/README.md +354 -0
  43. package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
  44. package/rules/security/S014_tls_version_enforcement/config.json +56 -0
  45. package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
  46. package/rules/security/S055_content_type_validation/analyzer.js +121 -279
  47. package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
  48. package/rules/tests/C002_no_duplicate_code.test.js +111 -22
  49. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
  50. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
@@ -1,29 +1,52 @@
1
1
  /**
2
- * Symbol-based analyzer for C033 - Advanced semantic analysis
3
- * Purpose: Use AST + Data Flow to detect true database access violations
2
+ * C033 Symbol-Based Analyzer
3
+ * Detect Service-Repository separation violations
4
4
  */
5
5
 
6
6
  class C033SymbolBasedAnalyzer {
7
7
  constructor(semanticEngine = null) {
8
8
  this.ruleId = 'C033';
9
- this.ruleName = 'Separate Service and Repository Logic (Symbol-Based)';
10
9
  this.semanticEngine = semanticEngine;
11
10
  this.verbose = false;
12
11
 
13
- // Known database/ORM symbols and interfaces
14
- this.databaseSymbols = [
15
- 'Repository', 'EntityManager', 'QueryBuilder', 'Connection',
16
- 'PrismaClient', 'Model', 'Collection', 'Table'
12
+ // Database operation patterns (specific to avoid false positives)
13
+ this.databaseOperations = [
14
+ // ORM-specific methods
15
+ 'findOne', 'findById', 'findBy', 'findOneBy', 'findAndCount', 'findByIds',
16
+ 'createQueryBuilder', 'getRepository', 'getManager', 'getConnection',
17
+ // CRUD operations
18
+ 'save', 'insert', 'upsert', 'persist',
19
+ 'update', 'patch', 'merge',
20
+ 'delete', 'remove', 'softDelete', 'destroy',
21
+ // Query execution
22
+ 'query', 'exec', 'execute', 'run', 'rawQuery',
23
+ // Specific ORM methods
24
+ 'flush', 'clear', 'refresh', 'reload',
25
+ // SQL builder methods
26
+ 'select', 'from', 'where', 'innerJoin', 'leftJoin', 'rightJoin',
27
+ 'orderBy', 'groupBy', 'having', 'limit', 'offset',
28
+ // Transaction methods
29
+ 'beginTransaction', 'commit', 'rollback', 'transaction'
17
30
  ];
18
31
 
19
- // ORM framework patterns
20
- this.ormPatterns = {
21
- typeorm: ['Repository', 'EntityManager', 'QueryRunner', 'QueryBuilder'],
22
- prisma: ['PrismaClient', 'PrismaService'],
23
- mongoose: ['Model', 'Document', 'Schema'],
24
- sequelize: ['Model', 'Sequelize', 'QueryInterface'],
25
- knex: ['Knex', 'QueryBuilder']
26
- };
32
+ // Business logic indicators (should not be in Repository)
33
+ this.businessLogicPatterns = [
34
+ 'calculate', 'compute', 'process', 'transform', 'convert',
35
+ 'validate', 'verify', 'check', 'ensure', 'confirm',
36
+ 'format', 'parse', 'serialize', 'deserialize',
37
+ 'notify', 'send', 'publish', 'trigger', 'handle', 'execute',
38
+ 'apply', 'enforce', 'implement'
39
+ ];
40
+
41
+ // ORM framework indicators
42
+ this.ormFrameworks = [
43
+ 'Repository', 'EntityManager', 'EntityRepository',
44
+ 'PrismaClient', 'PrismaService',
45
+ 'Model', 'Document', 'Schema',
46
+ 'Sequelize', 'QueryInterface',
47
+ 'Knex', 'QueryBuilder',
48
+ 'Connection', 'DataSource'
49
+ ];
27
50
  }
28
51
 
29
52
  async initialize(semanticEngine = null) {
@@ -31,31 +54,14 @@ class C033SymbolBasedAnalyzer {
31
54
  this.semanticEngine = semanticEngine;
32
55
  }
33
56
  this.verbose = semanticEngine?.verbose || false;
34
-
35
- if (this.verbose) {
36
- console.log(`[DEBUG] 🔧 C033 Symbol-Based: Analyzer initialized`);
37
- }
38
57
  }
39
58
 
40
59
  async analyze(files, language, options = {}) {
41
60
  const violations = [];
42
61
 
43
- if (!this.semanticEngine?.project) {
44
- if (this.verbose) {
45
- console.warn('[C033 Symbol-Based] No semantic engine available, skipping analysis');
46
- }
47
- return violations;
48
- }
49
-
50
62
  for (const filePath of files) {
51
- try {
52
- const fileViolations = await this.analyzeFileWithSymbols(filePath, options);
53
- violations.push(...fileViolations);
54
- } catch (error) {
55
- if (this.verbose) {
56
- console.warn(`[C033 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
57
- }
58
- }
63
+ const fileViolations = await this.analyzeFileWithSymbols(filePath, options);
64
+ violations.push(...fileViolations);
59
65
  }
60
66
 
61
67
  return violations;
@@ -63,305 +69,442 @@ class C033SymbolBasedAnalyzer {
63
69
 
64
70
  async analyzeFileWithSymbols(filePath, options = {}) {
65
71
  const violations = [];
66
- const sourceFile = this.semanticEngine.project.getSourceFileByFilePath(filePath);
67
72
 
68
- if (!sourceFile) {
69
- return violations;
70
- }
73
+ try {
74
+ if (!this.semanticEngine?.project) {
75
+ return violations;
76
+ }
71
77
 
72
- // 1. Classify file type using semantic analysis
73
- const fileType = this.classifyFileSemanticType(sourceFile, filePath);
74
-
75
- if (fileType !== 'service') {
76
- return violations; // Only analyze Service files
77
- }
78
+ const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
79
+ if (!sourceFile) {
80
+ return violations;
81
+ }
78
82
 
79
- // 2. Analyze call expressions in Service classes
80
- const classes = sourceFile.getClasses();
81
-
82
- for (const cls of classes) {
83
- const className = cls.getName() || 'UnknownClass';
83
+ // Determine file type
84
+ const fileType = this.determineFileType(filePath, sourceFile);
84
85
 
85
- // Skip if not a Service class
86
- if (!this.isServiceClass(cls)) {
87
- continue;
86
+ if (this.verbose) {
87
+ console.log(`[C033] Analyzing ${filePath} as ${fileType}`);
88
88
  }
89
-
90
- const methods = cls.getMethods();
91
-
92
- for (const method of methods) {
93
- const methodViolations = this.analyzeMethodForDatabaseCalls(
94
- method, sourceFile, className, filePath
95
- );
96
- violations.push(...methodViolations);
89
+
90
+ // Analyze based on file type
91
+ if (fileType === 'service') {
92
+ violations.push(...this.analyzeServiceFile(sourceFile, filePath));
93
+ } else if (fileType === 'repository') {
94
+ violations.push(...this.analyzeRepositoryFile(sourceFile, filePath));
95
+ } else if (fileType === 'controller') {
96
+ violations.push(...this.analyzeControllerFile(sourceFile, filePath));
97
+ } else if (fileType === 'mixed') {
98
+ // Add file-level violation for mixing concerns
99
+ violations.push({
100
+ ruleId: this.ruleId,
101
+ severity: 'warning',
102
+ message: 'Service and Repository classes should be in separate files to maintain clear separation of concerns',
103
+ file: filePath,
104
+ line: 1,
105
+ column: 1
106
+ });
107
+
108
+ // Also analyze individual Service and Repository classes for additional violations
109
+ violations.push(...this.analyzeServiceFile(sourceFile, filePath));
110
+ violations.push(...this.analyzeRepositoryFile(sourceFile, filePath));
111
+ }
112
+
113
+ } catch (error) {
114
+ if (this.verbose) {
115
+ console.warn(`[C033] Error analyzing ${filePath}:`, error.message);
97
116
  }
98
117
  }
99
-
118
+
100
119
  return violations;
101
120
  }
102
121
 
103
- /**
104
- * Analyze method for direct database calls using symbol resolution
105
- */
106
- analyzeMethodForDatabaseCalls(method, sourceFile, className, filePath) {
107
- const violations = [];
108
- const methodName = method.getName();
122
+ determineFileType(filePath, sourceFile) {
123
+ // Extract just the filename (not full path) to avoid false positives from directory names
124
+ const path = require('path');
125
+ const fileName = path.basename(filePath).toLowerCase();
126
+
127
+ // Check filename patterns
128
+ if (fileName.includes('service') && fileName.includes('repository')) {
129
+ if (this.verbose) {
130
+ console.log(`[C033-DEBUG] File ${fileName} detected as MIXED (filename has both service and repository)`);
131
+ }
132
+ return 'mixed';
133
+ }
134
+ if (fileName.includes('service')) {
135
+ if (this.verbose) {
136
+ console.log(`[C033-DEBUG] File ${fileName} detected as SERVICE (filename)`);
137
+ }
138
+ return 'service';
139
+ }
140
+ if (fileName.includes('repository')) {
141
+ if (this.verbose) {
142
+ console.log(`[C033-DEBUG] File ${fileName} detected as REPOSITORY (filename)`);
143
+ }
144
+ return 'repository';
145
+ }
146
+ if (fileName.includes('controller')) {
147
+ if (this.verbose) {
148
+ console.log(`[C033-DEBUG] File ${fileName} detected as CONTROLLER (filename)`);
149
+ }
150
+ return 'controller';
151
+ }
152
+
153
+ // Check class names in content
154
+ const classes = sourceFile.getClasses();
155
+ if (classes.length === 0) {
156
+ if (this.verbose) {
157
+ console.log(`[C033-DEBUG] File ${filePath} has no classes - UNKNOWN`);
158
+ }
159
+ return 'unknown';
160
+ }
109
161
 
110
- // Get all call expressions in the method
111
- const callExpressions = method.getDescendantsOfKind(this.getKind('CallExpression'));
162
+ const classNames = classes.map(cls => cls.getName() || 'Unnamed').join(', ');
163
+ const hasService = classes.some(cls => cls.getName()?.toLowerCase().includes('service'));
164
+ const hasRepository = classes.some(cls => cls.getName()?.toLowerCase().includes('repository'));
112
165
 
113
- for (const callExpr of callExpressions) {
114
- const violation = this.analyzeCallExpression(callExpr, sourceFile, className, methodName, filePath);
115
- if (violation) {
116
- violations.push(violation);
166
+ if (this.verbose) {
167
+ console.log(`[C033-DEBUG] File ${filePath} has classes: [${classNames}]`);
168
+ console.log(`[C033-DEBUG] hasService=${hasService}, hasRepository=${hasRepository}`);
169
+ }
170
+
171
+ if (hasService && hasRepository) {
172
+ if (this.verbose) {
173
+ console.log(`[C033-DEBUG] File ${filePath} detected as MIXED (has both Service and Repository classes)`);
174
+ }
175
+ return 'mixed';
176
+ }
177
+ if (hasService) {
178
+ if (this.verbose) {
179
+ console.log(`[C033-DEBUG] File ${filePath} detected as SERVICE (class name)`);
180
+ }
181
+ return 'service';
182
+ }
183
+ if (hasRepository) {
184
+ if (this.verbose) {
185
+ console.log(`[C033-DEBUG] File ${filePath} detected as REPOSITORY (class name)`);
117
186
  }
187
+ return 'repository';
118
188
  }
119
189
 
120
- return violations;
190
+ if (this.verbose) {
191
+ console.log(`[C033-DEBUG] File ${filePath} detected as UNKNOWN`);
192
+ }
193
+ return 'unknown';
121
194
  }
122
195
 
123
- /**
124
- * Analyze individual call expression using symbol resolution
125
- */
126
- analyzeCallExpression(callExpr, sourceFile, className, methodName, filePath) {
127
- const expression = callExpr.getExpression();
196
+ analyzeServiceFile(sourceFile, filePath) {
197
+ const violations = [];
198
+ const classes = sourceFile.getClasses();
128
199
 
129
- // Handle property access (obj.method())
130
- if (expression.getKind() === this.getKind('PropertyAccessExpression')) {
131
- const propertyAccess = expression;
132
- const object = propertyAccess.getExpression();
133
- const property = propertyAccess.getName();
200
+ for (const cls of classes) {
201
+ const className = cls.getName() || 'UnnamedClass';
134
202
 
135
- // Get the symbol/type of the object being called
136
- const objectSymbol = this.getObjectSymbol(object);
203
+ // Skip if this is a Repository class (in mixed files)
204
+ if (className.toLowerCase().includes('repository')) {
205
+ continue;
206
+ }
137
207
 
138
- if (this.isDatabaseOperation(objectSymbol, property)) {
139
- // Check if it's going through repository (acceptable)
140
- if (this.isRepositoryAccess(objectSymbol)) {
141
- return null; // OK: Service -> Repository -> Database
142
- }
143
-
144
- // Direct database access in Service (violation)
145
- const lineNumber = callExpr.getStartLineNumber();
146
- const columnNumber = callExpr.getStart() - sourceFile.getLineStartPos(lineNumber - 1) + 1;
147
-
148
- return {
208
+ // Check if Service uses dependency injection for Repository
209
+ const hasRepositoryInjection = this.checkRepositoryInjection(cls);
210
+ const hasDirectDbAccess = this.checkDirectDatabaseAccess(cls);
211
+
212
+ if (hasDirectDbAccess && !hasRepositoryInjection) {
213
+ violations.push({
149
214
  ruleId: this.ruleId,
150
215
  severity: 'warning',
151
- message: `Service should not contain direct database calls`,
152
- source: this.ruleId,
216
+ message: `Service class '${className}' should use dependency injection to inject Repository instead of direct database access.`,
153
217
  file: filePath,
154
- line: lineNumber,
155
- column: columnNumber,
156
- description: `[SYMBOL-BASED] Direct database call '${property}()' on '${objectSymbol?.name || 'unknown'}' found in Service`,
157
- suggestion: 'Use Repository pattern for data access',
158
- category: 'architecture'
159
- };
218
+ line: cls.getStartLineNumber(),
219
+ column: 1
220
+ });
221
+ }
222
+
223
+ const methods = cls.getMethods();
224
+
225
+ for (const method of methods) {
226
+ // Check for direct database operations in Service (not via repository)
227
+ const methodBody = method.getBodyText() || '';
228
+
229
+ // Check for direct database access patterns
230
+ const directDbPatterns = [
231
+ // Direct access to dataSource/connection
232
+ /this\.(dataSource|connection|entityManager|manager)\s*\.\s*createQueryBuilder/i,
233
+ /this\.(dataSource|connection|entityManager|manager)\s*\.\s*getRepository/i,
234
+ /this\.(dataSource|connection|entityManager|manager)\s*\.\s*query/i,
235
+
236
+ // Global getRepository/getConnection calls
237
+ /(?<!this\.[\w]+Repository\.)getRepository\s*\(/i,
238
+ /getConnection\s*\(/i,
239
+ /getManager\s*\(/i,
240
+
241
+ // createQueryBuilder on dataSource (not repository)
242
+ /this\.(dataSource|connection|db|database)\s*\.\s*createQueryBuilder/i,
243
+ ];
244
+
245
+ let hasDirectDbAccess = false;
246
+ let detectedOperation = '';
247
+
248
+ for (const pattern of directDbPatterns) {
249
+ if (pattern.test(methodBody)) {
250
+ hasDirectDbAccess = true;
251
+
252
+ // Extract operation name for better error message
253
+ const match = methodBody.match(pattern);
254
+ if (match) {
255
+ detectedOperation = match[0].replace(/this\./g, '').replace(/\s/g, '');
256
+ }
257
+ break;
258
+ }
259
+ }
260
+
261
+ // Also check database operations list
262
+ if (!hasDirectDbAccess) {
263
+ for (const operation of this.databaseOperations) {
264
+ const pattern = new RegExp(`\\b${operation}\\s*\\(`, 'i');
265
+ if (pattern.test(methodBody)) {
266
+ // Check if this is truly direct DB call (not via repository)
267
+ const dbObjectPattern = /this\.(dataSource|connection|entityManager|manager|database|db)\./i;
268
+ const globalCallPattern = /\b(getRepository|getConnection|getManager|createQueryBuilder)\s*\(/i;
269
+
270
+ if (dbObjectPattern.test(methodBody) || globalCallPattern.test(methodBody)) {
271
+ hasDirectDbAccess = true;
272
+ detectedOperation = operation;
273
+ break;
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ if (hasDirectDbAccess) {
280
+ violations.push({
281
+ ruleId: this.ruleId,
282
+ severity: 'warning',
283
+ message: `Service method '${method.getName()}' directly calls database operation '${detectedOperation}'. Consider using Repository pattern to separate data access logic.`,
284
+ file: filePath,
285
+ line: method.getStartLineNumber(),
286
+ column: 1
287
+ });
288
+ }
160
289
  }
161
290
  }
162
291
 
163
- return null;
292
+ return violations;
164
293
  }
165
294
 
166
- /**
167
- * Get symbol information for an object expression
168
- */
169
- getObjectSymbol(objectExpr) {
295
+ checkRepositoryInjection(cls) {
170
296
  try {
171
- // Try to get the symbol of the expression
172
- const symbol = objectExpr.getSymbol();
173
- if (symbol) {
174
- return {
175
- name: symbol.getName(),
176
- type: this.getSymbolType(symbol),
177
- isDatabase: this.isSymbolDatabase(symbol)
178
- };
297
+ // Check constructor parameters
298
+ const constructor = cls.getConstructors()[0];
299
+ if (constructor) {
300
+ const params = constructor.getParameters();
301
+ for (const param of params) {
302
+ const paramName = param.getName().toLowerCase();
303
+ const paramType = param.getType().getText().toLowerCase();
304
+
305
+ if (paramName.includes('repository') ||
306
+ paramName.includes('repo') ||
307
+ paramType.includes('repository')) {
308
+ return true;
309
+ }
310
+ }
179
311
  }
180
312
 
181
- // Fallback: analyze by text patterns
182
- const text = objectExpr.getText();
183
- return {
184
- name: text,
185
- type: this.inferTypeFromText(text),
186
- isDatabase: this.isTextDatabase(text)
187
- };
313
+ // Check class properties
314
+ const properties = cls.getProperties();
315
+ for (const prop of properties) {
316
+ const propName = prop.getName().toLowerCase();
317
+ const propType = prop.getType().getText().toLowerCase();
318
+
319
+ if (propName.includes('repository') ||
320
+ propName.includes('repo') ||
321
+ propType.includes('repository')) {
322
+ return true;
323
+ }
324
+ }
188
325
  } catch (error) {
189
- return null;
326
+ // Ignore errors
190
327
  }
328
+
329
+ return false;
191
330
  }
192
331
 
193
- /**
194
- * Check if operation is a database operation
195
- */
196
- isDatabaseOperation(objectSymbol, methodName) {
197
- if (!objectSymbol) return false;
198
-
199
- // Exclude queue/job operations (Bull.js, agenda, etc.)
200
- if (this.isQueueOperation(objectSymbol, methodName)) {
201
- return false;
332
+ checkDirectDatabaseAccess(cls) {
333
+ try {
334
+ const classText = cls.getText();
335
+
336
+ // Check for ORM framework usage
337
+ for (const framework of this.ormFrameworks) {
338
+ if (classText.includes(framework)) {
339
+ return true;
340
+ }
341
+ }
342
+
343
+ // Check for database operations
344
+ for (const operation of this.databaseOperations) {
345
+ const pattern = new RegExp(`\\b${operation}\\s*\\(`, 'i');
346
+ if (pattern.test(classText)) {
347
+ return true;
348
+ }
349
+ }
350
+ } catch (error) {
351
+ // Ignore errors
202
352
  }
203
353
 
204
- // Known database method patterns
205
- const dbMethods = [
206
- 'findOneBy', 'findBy', 'findAndCount', 'findMany', 'findFirst',
207
- 'save', 'insert', 'create', 'upsert',
208
- 'update', 'patch', 'merge', 'set',
209
- 'delete', 'remove', 'destroy',
210
- 'query', 'execute', 'run',
211
- 'createQueryBuilder', 'getRepository'
212
- ];
213
-
214
- return objectSymbol.isDatabase && dbMethods.includes(methodName);
354
+ return false;
215
355
  }
216
356
 
217
- /**
218
- * Check if this is a queue/job operation (should be excluded)
219
- */
220
- isQueueOperation(objectSymbol, methodName) {
221
- if (!objectSymbol) return false;
222
-
223
- const queueMethods = [
224
- 'remove', 'isFailed', 'isCompleted', 'isActive', 'isWaiting', 'isDelayed',
225
- 'getJob', 'getJobs', 'add', 'process', 'on', 'off',
226
- 'retry', 'moveToCompleted', 'moveToFailed'
227
- ];
228
-
229
- const queueTypes = ['queue', 'job', 'bull'];
230
- const objectName = objectSymbol.name.toLowerCase();
231
-
232
- // Enhanced detection for Bull.js Job objects
233
- const isQueueMethod = queueMethods.includes(methodName);
234
- const isQueueObject = queueTypes.some(type => objectName.includes(type)) ||
235
- /job/i.test(objectName) ||
236
- /queue/i.test(objectName);
357
+ analyzeRepositoryFile(sourceFile, filePath) {
358
+ const violations = [];
359
+ const classes = sourceFile.getClasses();
237
360
 
238
- if (this.verbose && (isQueueMethod || isQueueObject)) {
239
- console.log(`[DEBUG] Queue Check: object="${objectName}", method="${methodName}", isQueue=${isQueueMethod && isQueueObject}`);
361
+ for (const cls of classes) {
362
+ const className = cls.getName() || 'UnnamedClass';
363
+
364
+ // Skip if this is a Service class (in mixed files)
365
+ if (className.toLowerCase().includes('service') && !className.toLowerCase().includes('repository')) {
366
+ continue;
367
+ }
368
+
369
+ const methods = cls.getMethods();
370
+
371
+ // Check if Repository follows CRUD pattern
372
+ const hasCrudMethods = this.checkCrudPattern(cls);
373
+ const hasBusinessLogic = this.checkBusinessLogicInRepository(cls);
374
+
375
+ if (hasBusinessLogic) {
376
+ violations.push({
377
+ ruleId: this.ruleId,
378
+ severity: 'warning',
379
+ message: `Repository class '${className}' should contain only CRUD operations. Business logic detected - move to Service layer.`,
380
+ file: filePath,
381
+ line: cls.getStartLineNumber(),
382
+ column: 1
383
+ });
384
+ }
385
+
386
+ for (const method of methods) {
387
+ const methodName = method.getName();
388
+
389
+ // Check method name for business logic indicators
390
+ for (const pattern of this.businessLogicPatterns) {
391
+ if (methodName.toLowerCase().includes(pattern.toLowerCase())) {
392
+ violations.push({
393
+ ruleId: this.ruleId,
394
+ severity: 'warning',
395
+ message: `Repository method '${methodName}' in class '${className}' appears to contain business logic ('${pattern}'). Move business logic to Service layer.`,
396
+ file: filePath,
397
+ line: method.getStartLineNumber(),
398
+ column: 1
399
+ });
400
+ break;
401
+ }
402
+ }
403
+
404
+ // Check method complexity (too many control structures = business logic)
405
+ const methodBody = method.getBodyText() || '';
406
+ const ifCount = (methodBody.match(/\bif\s*\(/g) || []).length;
407
+ const forCount = (methodBody.match(/\bfor\s*\(/g) || []).length;
408
+ const whileCount = (methodBody.match(/\bwhile\s*\(/g) || []).length;
409
+ const complexityCount = ifCount + forCount + whileCount;
410
+
411
+ if (complexityCount > 2) {
412
+ violations.push({
413
+ ruleId: this.ruleId,
414
+ severity: 'warning',
415
+ message: `Repository method '${methodName}' is too complex (${complexityCount} control structures). Repository should contain only simple data access operations.`,
416
+ file: filePath,
417
+ line: method.getStartLineNumber(),
418
+ column: 1
419
+ });
420
+ }
421
+
422
+ // Check for business logic in method body
423
+ const hasCalculation = /\b(calculate|compute|sum|total|average)\b/i.test(methodBody);
424
+ const hasValidation = /\b(validate|verify|check|ensure|confirm)\b/i.test(methodBody);
425
+ const hasTransformation = /\b(transform|convert|format|parse)\b/i.test(methodBody);
426
+
427
+ if ((hasCalculation || hasValidation || hasTransformation) && complexityCount > 0) {
428
+ violations.push({
429
+ ruleId: this.ruleId,
430
+ severity: 'warning',
431
+ message: `Repository method '${methodName}' contains business logic operations. Repository should focus on data access only.`,
432
+ file: filePath,
433
+ line: method.getStartLineNumber(),
434
+ column: 1
435
+ });
436
+ }
437
+ }
240
438
  }
241
439
 
242
- return isQueueMethod && isQueueObject;
243
- }
244
-
245
- /**
246
- * Check if access is through repository (acceptable)
247
- */
248
- isRepositoryAccess(objectSymbol) {
249
- if (!objectSymbol) return false;
250
-
251
- const name = objectSymbol.name.toLowerCase();
252
- return name.includes('repository') || name.includes('repo');
440
+ return violations;
253
441
  }
254
442
 
255
- /**
256
- * Check if symbol represents database object
257
- */
258
- isSymbolDatabase(symbol) {
443
+ checkCrudPattern(cls) {
259
444
  try {
260
- const type = symbol.getType();
261
- const typeName = type.getSymbol()?.getName() || '';
445
+ const methods = cls.getMethods();
446
+ const methodNames = methods.map(m => m.getName().toLowerCase());
447
+
448
+ // Check for basic CRUD operations
449
+ const hasCreate = methodNames.some(name => name.includes('create') || name.includes('save') || name.includes('insert'));
450
+ const hasRead = methodNames.some(name => name.includes('find') || name.includes('get') || name.includes('select'));
451
+ const hasUpdate = methodNames.some(name => name.includes('update') || name.includes('patch'));
452
+ const hasDelete = methodNames.some(name => name.includes('delete') || name.includes('remove'));
262
453
 
263
- return this.databaseSymbols.some(dbSymbol =>
264
- typeName.includes(dbSymbol)
265
- );
454
+ return hasCreate || hasRead || hasUpdate || hasDelete;
266
455
  } catch (error) {
267
456
  return false;
268
457
  }
269
458
  }
270
459
 
271
- /**
272
- * Infer type from text patterns (fallback)
273
- */
274
- inferTypeFromText(text) {
275
- const lowerText = text.toLowerCase();
276
-
277
- // Check for known database object patterns
278
- if (/manager|connection|client|prisma/i.test(lowerText)) {
279
- return 'database';
280
- }
281
-
282
- if (/repository|repo/i.test(lowerText)) {
283
- return 'repository';
460
+ checkBusinessLogicInRepository(cls) {
461
+ try {
462
+ const classText = cls.getText();
463
+
464
+ // Check for business logic patterns
465
+ for (const pattern of this.businessLogicPatterns) {
466
+ const regex = new RegExp(`\\b${pattern}\\w*\\s*\\(`, 'i');
467
+ if (regex.test(classText)) {
468
+ return true;
469
+ }
470
+ }
471
+ } catch (error) {
472
+ // Ignore errors
284
473
  }
285
474
 
286
- return 'unknown';
287
- }
288
-
289
- /**
290
- * Check if text represents database access
291
- */
292
- isTextDatabase(text) {
293
- const lowerText = text.toLowerCase();
294
- return /manager|connection|client|prisma|entitymanager/i.test(lowerText) &&
295
- !/repository|repo/i.test(lowerText);
475
+ return false;
296
476
  }
297
477
 
298
- /**
299
- * Classify file type using semantic analysis
300
- */
301
- classifyFileSemanticType(sourceFile, filePath) {
302
- const fileName = sourceFile.getBaseName().toLowerCase();
303
-
304
- // Check filename patterns
305
- if (/service\.ts$|service\.js$/i.test(fileName)) return 'service';
306
- if (/repository\.ts$|repository\.js$/i.test(fileName)) return 'repository';
307
-
308
- // Check class patterns
478
+ analyzeControllerFile(sourceFile, filePath) {
479
+ const violations = [];
309
480
  const classes = sourceFile.getClasses();
481
+
310
482
  for (const cls of classes) {
311
- if (this.isServiceClass(cls)) return 'service';
312
- if (this.isRepositoryClass(cls)) return 'repository';
483
+ const className = cls.getName() || 'UnnamedClass';
484
+ const methods = cls.getMethods();
485
+
486
+ for (const method of methods) {
487
+ const methodBody = method.getBodyText() || '';
488
+
489
+ // Controllers should not directly access database
490
+ for (const operation of this.databaseOperations) {
491
+ const pattern = new RegExp(`\\b${operation}\\s*\\(`, 'i');
492
+ if (pattern.test(methodBody)) {
493
+ violations.push({
494
+ ruleId: this.ruleId,
495
+ severity: 'warning',
496
+ message: `Controller class '${className}' should not directly access database with '${operation}'. Use Service layer instead.`,
497
+ file: filePath,
498
+ line: method.getStartLineNumber(),
499
+ column: 1
500
+ });
501
+ break;
502
+ }
503
+ }
504
+ }
313
505
  }
314
506
 
315
- return 'unknown';
316
- }
317
-
318
- /**
319
- * Check if class is a Service class
320
- */
321
- isServiceClass(cls) {
322
- const className = cls.getName()?.toLowerCase() || '';
323
-
324
- // Check class name
325
- if (/service$/.test(className)) return true;
326
-
327
- // Check decorators
328
- const decorators = cls.getDecorators();
329
- return decorators.some(decorator => {
330
- const decoratorName = decorator.getName().toLowerCase();
331
- return decoratorName.includes('service') || decoratorName === 'injectable';
332
- });
333
- }
334
-
335
- /**
336
- * Check if class is a Repository class
337
- */
338
- isRepositoryClass(cls) {
339
- const className = cls.getName()?.toLowerCase() || '';
340
- return /repository$|repo$/.test(className);
341
- }
342
-
343
- /**
344
- * Get TypeScript SyntaxKind
345
- */
346
- getKind(kindName) {
347
- try {
348
- const ts = require('typescript');
349
- return ts.SyntaxKind[kindName];
350
- } catch (error) {
351
- // Fallback for ts-morph
352
- return this.semanticEngine?.project?.getTypeChecker()?.compilerObject?.SyntaxKind?.[kindName] || 0;
353
- }
354
- }
355
-
356
- /**
357
- * Get symbol type information
358
- */
359
- getSymbolType(symbol) {
360
- try {
361
- return symbol.getType().getText();
362
- } catch (error) {
363
- return 'unknown';
364
- }
507
+ return violations;
365
508
  }
366
509
  }
367
510