@sun-asterisk/sunlint 1.3.23 → 1.3.25
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.
- package/config/rules/enhanced-rules-registry.json +32 -0
- package/core/github-annotate-service.js +1 -4
- package/package.json +1 -1
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +40 -11
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +104 -28
- package/rules/common/C019_log_level_usage/analyzer.js +30 -27
- package/rules/common/C019_log_level_usage/config.json +4 -2
- package/rules/common/C019_log_level_usage/ts-morph-analyzer.js +274 -0
- package/rules/common/C020_unused_imports/analyzer.js +88 -0
- package/rules/common/C020_unused_imports/config.json +64 -0
- package/rules/common/C020_unused_imports/ts-morph-analyzer.js +358 -0
- package/rules/common/C021_import_organization/analyzer.js +88 -0
- package/rules/common/C021_import_organization/config.json +77 -0
- package/rules/common/C021_import_organization/ts-morph-analyzer.js +373 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +106 -31
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +377 -87
|
@@ -9,44 +9,91 @@ class C033SymbolBasedAnalyzer {
|
|
|
9
9
|
this.semanticEngine = semanticEngine;
|
|
10
10
|
this.verbose = false;
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// ============================================================
|
|
13
|
+
// PATTERN DEFINITIONS - Organized by category
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
// Database CRUD operations (TypeORM, Prisma, Sequelize, etc.)
|
|
13
17
|
this.databaseOperations = [
|
|
14
|
-
//
|
|
18
|
+
// Query operations
|
|
15
19
|
'findOne', 'findById', 'findBy', 'findOneBy', 'findAndCount', 'findByIds',
|
|
16
|
-
'
|
|
17
|
-
//
|
|
18
|
-
'save', 'insert', 'upsert', 'persist',
|
|
19
|
-
|
|
20
|
-
'
|
|
20
|
+
'find', 'findMany', 'findAll', 'findFirst', 'get', 'getMany', 'getOne',
|
|
21
|
+
// Create operations
|
|
22
|
+
'save', 'insert', 'upsert', 'persist', 'create', 'createMany',
|
|
23
|
+
// Update operations
|
|
24
|
+
'update', 'patch', 'merge', 'updateMany', 'set',
|
|
25
|
+
// Delete operations
|
|
26
|
+
'delete', 'remove', 'softDelete', 'destroy', 'deleteMany',
|
|
21
27
|
// Query execution
|
|
22
|
-
'query', 'exec', 'execute', 'run', 'rawQuery',
|
|
23
|
-
//
|
|
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'
|
|
28
|
+
'query', 'exec', 'execute', 'run', 'rawQuery', 'raw',
|
|
29
|
+
// ORM-specific
|
|
30
|
+
'flush', 'clear', 'refresh', 'reload', 'count', 'aggregate'
|
|
30
31
|
];
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
this.
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
33
|
+
// Query builder methods
|
|
34
|
+
this.queryBuilderMethods = [
|
|
35
|
+
'createQueryBuilder', 'queryBuilder',
|
|
36
|
+
'select', 'from', 'where', 'andWhere', 'orWhere',
|
|
37
|
+
'innerJoin', 'leftJoin', 'rightJoin', 'join',
|
|
38
|
+
'orderBy', 'groupBy', 'having', 'limit', 'offset', 'skip', 'take',
|
|
39
|
+
'getMany', 'getOne', 'getRawMany', 'getRawOne', 'getCount'
|
|
39
40
|
];
|
|
40
41
|
|
|
41
|
-
//
|
|
42
|
+
// Transaction and connection methods
|
|
43
|
+
this.transactionMethods = [
|
|
44
|
+
'transaction', 'beginTransaction', 'commit', 'rollback',
|
|
45
|
+
'startTransaction', 'commitTransaction', 'rollbackTransaction',
|
|
46
|
+
'createQueryRunner', 'getQueryRunner'
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Schema and migration operations (should rarely be in Service)
|
|
50
|
+
this.schemaOperations = [
|
|
51
|
+
'synchronize', 'sync', 'dropDatabase', 'dropSchema',
|
|
52
|
+
'runMigrations', 'undoLastMigration', 'showMigrations',
|
|
53
|
+
'createTable', 'dropTable', 'alterTable', 'addColumn', 'dropColumn'
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Database connection properties
|
|
57
|
+
// NOTE: Removed generic 'client' to avoid false positives with httpClient, apiClient, etc.
|
|
58
|
+
// Only specific database clients are included
|
|
59
|
+
this.databaseProperties = [
|
|
60
|
+
'dataSource', 'connection', 'entityManager', 'manager',
|
|
61
|
+
'database', 'db',
|
|
62
|
+
// Specific database clients only
|
|
63
|
+
'prismaClient', 'prismaService',
|
|
64
|
+
'sequelizeClient', 'sequelizeConnection',
|
|
65
|
+
'mongoClient', 'mongoConnection',
|
|
66
|
+
'redisClient', 'redisConnection',
|
|
67
|
+
'dbClient', 'databaseClient',
|
|
68
|
+
// ORM instances
|
|
69
|
+
'prisma', 'sequelize', 'knex', 'mongoose', 'typeorm'
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// ORM framework type indicators
|
|
42
73
|
this.ormFrameworks = [
|
|
43
|
-
'Repository', '
|
|
74
|
+
'Repository', 'EntityRepository', 'EntityManager',
|
|
44
75
|
'PrismaClient', 'PrismaService',
|
|
45
76
|
'Model', 'Document', 'Schema',
|
|
46
77
|
'Sequelize', 'QueryInterface',
|
|
47
78
|
'Knex', 'QueryBuilder',
|
|
48
79
|
'Connection', 'DataSource'
|
|
49
80
|
];
|
|
81
|
+
|
|
82
|
+
// Business logic indicators (should NOT be in Repository)
|
|
83
|
+
this.businessLogicPatterns = [
|
|
84
|
+
// Calculation and computation
|
|
85
|
+
'calculate', 'compute', 'sum', 'total', 'average', 'aggregate',
|
|
86
|
+
// Validation and verification
|
|
87
|
+
'validate', 'verify', 'check', 'ensure', 'confirm', 'assert',
|
|
88
|
+
// Transformation and conversion
|
|
89
|
+
'format', 'parse', 'serialize', 'deserialize', 'transform', 'convert',
|
|
90
|
+
// Business operations
|
|
91
|
+
'process', 'handle', 'execute', 'perform', 'apply',
|
|
92
|
+
// Notifications and communications
|
|
93
|
+
'notify', 'send', 'publish', 'trigger', 'broadcast',
|
|
94
|
+
// Business rules
|
|
95
|
+
'enforce', 'implement', 'authorize', 'approve', 'reject'
|
|
96
|
+
];
|
|
50
97
|
}
|
|
51
98
|
|
|
52
99
|
async initialize(semanticEngine = null) {
|
|
@@ -56,6 +103,31 @@ class C033SymbolBasedAnalyzer {
|
|
|
56
103
|
this.verbose = semanticEngine?.verbose || false;
|
|
57
104
|
}
|
|
58
105
|
|
|
106
|
+
// ============================================================
|
|
107
|
+
// HELPER METHODS
|
|
108
|
+
// ============================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Calculate exact line number from string index
|
|
112
|
+
* @param {string} text - The method body text
|
|
113
|
+
* @param {number} index - Index of match in text
|
|
114
|
+
* @param {number} startLine - Starting line number of method
|
|
115
|
+
* @returns {number} Exact line number of violation
|
|
116
|
+
*/
|
|
117
|
+
calculateLineNumberFromIndex(text, index, startLine) {
|
|
118
|
+
if (index === undefined || index === null) {
|
|
119
|
+
return startLine; // Fallback to method start line
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const textBeforeMatch = text.substring(0, index);
|
|
123
|
+
const lineOffset = (textBeforeMatch.match(/\n/g) || []).length;
|
|
124
|
+
|
|
125
|
+
// Method body starts after the opening brace, which is typically on the line
|
|
126
|
+
// after method signature. getBodyText() returns content inside braces.
|
|
127
|
+
// So we need to add lineOffset without additional adjustment.
|
|
128
|
+
return startLine + lineOffset;
|
|
129
|
+
}
|
|
130
|
+
|
|
59
131
|
async analyze(files, language, options = {}) {
|
|
60
132
|
const violations = [];
|
|
61
133
|
|
|
@@ -193,6 +265,10 @@ class C033SymbolBasedAnalyzer {
|
|
|
193
265
|
return 'unknown';
|
|
194
266
|
}
|
|
195
267
|
|
|
268
|
+
// ============================================================
|
|
269
|
+
// SERVICE FILE ANALYSIS
|
|
270
|
+
// ============================================================
|
|
271
|
+
|
|
196
272
|
analyzeServiceFile(sourceFile, filePath) {
|
|
197
273
|
const violations = [];
|
|
198
274
|
const classes = sourceFile.getClasses();
|
|
@@ -205,11 +281,24 @@ class C033SymbolBasedAnalyzer {
|
|
|
205
281
|
continue;
|
|
206
282
|
}
|
|
207
283
|
|
|
208
|
-
// Check
|
|
284
|
+
// Check class-level patterns
|
|
209
285
|
const hasRepositoryInjection = this.checkRepositoryInjection(cls);
|
|
210
|
-
const
|
|
286
|
+
const hasDatabaseInjection = this.checkDatabaseInjection(cls);
|
|
211
287
|
|
|
212
|
-
|
|
288
|
+
// VIOLATION: Service has both Repository and direct database access
|
|
289
|
+
if (hasRepositoryInjection && hasDatabaseInjection) {
|
|
290
|
+
violations.push({
|
|
291
|
+
ruleId: this.ruleId,
|
|
292
|
+
severity: 'warning',
|
|
293
|
+
message: `Service class '${className}' has both Repository injection and direct database access (DataSource/Connection/EntityManager). This creates inconsistent patterns - use only Repository.`,
|
|
294
|
+
file: filePath,
|
|
295
|
+
line: cls.getStartLineNumber(),
|
|
296
|
+
column: 1
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// VIOLATION: Service has direct database access without Repository
|
|
301
|
+
if (!hasRepositoryInjection && hasDatabaseInjection) {
|
|
213
302
|
violations.push({
|
|
214
303
|
ruleId: this.ruleId,
|
|
215
304
|
severity: 'warning',
|
|
@@ -220,78 +309,223 @@ class C033SymbolBasedAnalyzer {
|
|
|
220
309
|
});
|
|
221
310
|
}
|
|
222
311
|
|
|
312
|
+
// Analyze each method in the service
|
|
223
313
|
const methods = cls.getMethods();
|
|
224
|
-
|
|
225
314
|
for (const method of methods) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
315
|
+
violations.push(...this.analyzeServiceMethod(method, className, filePath));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return violations;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
analyzeServiceMethod(method, className, filePath) {
|
|
323
|
+
const violations = [];
|
|
324
|
+
const methodName = method.getName();
|
|
325
|
+
const methodBody = method.getBodyText() || '';
|
|
326
|
+
|
|
327
|
+
// Get the actual body block start line (opening brace)
|
|
328
|
+
const methodBodyNode = method.getBody();
|
|
329
|
+
const bodyStartLine = methodBodyNode ? methodBodyNode.getStartLineNumber() + 1 : method.getStartLineNumber();
|
|
330
|
+
|
|
331
|
+
// Check for different types of violations
|
|
332
|
+
const directDbViolation = this.checkDirectDatabaseAccess_InMethod(methodBody);
|
|
333
|
+
if (directDbViolation) {
|
|
334
|
+
const violationLine = this.calculateLineNumberFromIndex(
|
|
335
|
+
methodBody,
|
|
336
|
+
directDbViolation.matchIndex,
|
|
337
|
+
bodyStartLine
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
violations.push({
|
|
341
|
+
ruleId: this.ruleId,
|
|
342
|
+
severity: 'warning',
|
|
343
|
+
message: `Service method '${methodName}' directly calls database operation '${directDbViolation.operation}'. Consider using Repository pattern to separate data access logic.`,
|
|
344
|
+
file: filePath,
|
|
345
|
+
line: violationLine,
|
|
346
|
+
column: 1
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const transactionViolation = this.checkTransactionAccess_InMethod(methodBody);
|
|
351
|
+
if (transactionViolation) {
|
|
352
|
+
const violationLine = this.calculateLineNumberFromIndex(
|
|
353
|
+
methodBody,
|
|
354
|
+
transactionViolation.matchIndex,
|
|
355
|
+
bodyStartLine
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
violations.push({
|
|
359
|
+
ruleId: this.ruleId,
|
|
360
|
+
severity: 'warning',
|
|
361
|
+
message: `Service method '${methodName}' directly handles transactions using '${transactionViolation.operation}'. Consider delegating transaction logic to Repository or a dedicated transaction service.`,
|
|
362
|
+
file: filePath,
|
|
363
|
+
line: violationLine,
|
|
364
|
+
column: 1
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const queryRunnerViolation = this.checkQueryRunnerAccess_InMethod(methodBody);
|
|
369
|
+
if (queryRunnerViolation) {
|
|
370
|
+
const violationLine = this.calculateLineNumberFromIndex(
|
|
371
|
+
methodBody,
|
|
372
|
+
queryRunnerViolation.matchIndex,
|
|
373
|
+
bodyStartLine
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
violations.push({
|
|
377
|
+
ruleId: this.ruleId,
|
|
378
|
+
severity: 'warning',
|
|
379
|
+
message: `Service method '${methodName}' directly uses QueryRunner. QueryRunner operations should be encapsulated in Repository layer.`,
|
|
380
|
+
file: filePath,
|
|
381
|
+
line: violationLine,
|
|
382
|
+
column: 1
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const schemaViolation = this.checkSchemaOperations_InMethod(methodBody);
|
|
387
|
+
if (schemaViolation) {
|
|
388
|
+
const violationLine = this.calculateLineNumberFromIndex(
|
|
389
|
+
methodBody,
|
|
390
|
+
schemaViolation.matchIndex,
|
|
391
|
+
bodyStartLine
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
violations.push({
|
|
395
|
+
ruleId: this.ruleId,
|
|
396
|
+
severity: 'error',
|
|
397
|
+
message: `Service method '${methodName}' performs schema operations ('${schemaViolation.operation}'). Schema operations should not be in Service layer.`,
|
|
398
|
+
file: filePath,
|
|
399
|
+
line: violationLine,
|
|
400
|
+
column: 1
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return violations;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ============================================================
|
|
408
|
+
// SERVICE METHOD VIOLATION CHECKERS
|
|
409
|
+
// ============================================================
|
|
410
|
+
|
|
411
|
+
checkDirectDatabaseAccess_InMethod(methodBody) {
|
|
412
|
+
// Pattern 1: Direct access to dataSource/connection/manager
|
|
413
|
+
const directAccessPatterns = [
|
|
414
|
+
{ regex: /this\.(dataSource|connection|entityManager|manager)\s*\.\s*createQueryBuilder/i, type: 'createQueryBuilder' },
|
|
415
|
+
{ regex: /this\.(dataSource|connection|entityManager|manager)\s*\.\s*getRepository/i, type: 'getRepository' },
|
|
416
|
+
{ regex: /this\.(dataSource|connection|entityManager|manager)\s*\.\s*query/i, type: 'query' },
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
for (const pattern of directAccessPatterns) {
|
|
420
|
+
const match = methodBody.match(pattern.regex);
|
|
421
|
+
if (match) {
|
|
422
|
+
return { operation: pattern.type, pattern: 'direct-access', matchIndex: match.index };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Pattern 2: Global ORM calls
|
|
427
|
+
const globalPatterns = [
|
|
428
|
+
{ regex: /(?<!Repository\.)getRepository\s*\(/i, type: 'getRepository' },
|
|
429
|
+
{ regex: /\bgetConnection\s*\(/i, type: 'getConnection' },
|
|
430
|
+
{ regex: /\bgetManager\s*\(/i, type: 'getManager' },
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
for (const pattern of globalPatterns) {
|
|
434
|
+
const match = methodBody.match(pattern.regex);
|
|
435
|
+
if (match) {
|
|
436
|
+
return { operation: pattern.type, pattern: 'global-call', matchIndex: match.index };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Pattern 3: Database operations with context check
|
|
441
|
+
for (const operation of this.databaseOperations) {
|
|
442
|
+
const operationPattern = new RegExp(`\\b${operation}\\s*\\(`, 'gi');
|
|
443
|
+
const matches = methodBody.matchAll(operationPattern);
|
|
444
|
+
|
|
445
|
+
for (const match of matches) {
|
|
446
|
+
const matchIndex = match.index;
|
|
447
|
+
const contextStart = Math.max(0, matchIndex - 50);
|
|
448
|
+
const context = methodBody.substring(contextStart, matchIndex);
|
|
247
449
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
}
|
|
450
|
+
// Check if this is a call through repository (ALLOWED)
|
|
451
|
+
const repositoryCallPattern = /this\.([\w]+Repository|[\w]+Repo)\s*\.\s*$/i;
|
|
452
|
+
if (repositoryCallPattern.test(context)) {
|
|
453
|
+
continue; // Skip - this is allowed
|
|
259
454
|
}
|
|
260
455
|
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
}
|
|
456
|
+
// Check if this is direct database call (VIOLATION)
|
|
457
|
+
const directDbCallPattern = /this\.(dataSource|connection|entityManager|manager|database|db|client|prisma)\./i;
|
|
458
|
+
const globalDbCallPattern = /(getRepository|getConnection|getManager|createQueryBuilder)\s*\([^)]*\)\s*\.?[\w.]*$/i;
|
|
278
459
|
|
|
279
|
-
if (
|
|
280
|
-
|
|
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
|
-
});
|
|
460
|
+
if (directDbCallPattern.test(context) || globalDbCallPattern.test(context)) {
|
|
461
|
+
return { operation, pattern: 'context-detected', matchIndex };
|
|
288
462
|
}
|
|
289
463
|
}
|
|
290
464
|
}
|
|
291
465
|
|
|
292
|
-
return
|
|
466
|
+
return null;
|
|
293
467
|
}
|
|
294
468
|
|
|
469
|
+
checkTransactionAccess_InMethod(methodBody) {
|
|
470
|
+
// Check for transaction method calls
|
|
471
|
+
for (const txMethod of this.transactionMethods) {
|
|
472
|
+
// Pattern: this.(dataSource|connection|manager).transaction(...)
|
|
473
|
+
const txPattern = new RegExp(`this\\.(dataSource|connection|entityManager|manager|db)\\s*\\.\\s*${txMethod}\\s*\\(`, 'i');
|
|
474
|
+
const match = methodBody.match(txPattern);
|
|
475
|
+
if (match) {
|
|
476
|
+
return { operation: txMethod, type: 'direct-transaction', matchIndex: match.index };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Pattern: global transaction calls
|
|
480
|
+
const globalTxPattern = new RegExp(`\\b${txMethod}\\s*\\(`, 'i');
|
|
481
|
+
// Make sure it's not a repository method call
|
|
482
|
+
const contextPattern = new RegExp(`this\\.(\\w+Repository|\\w+Repo)\\s*\\.\\s*${txMethod}\\s*\\(`, 'i');
|
|
483
|
+
|
|
484
|
+
if (globalTxPattern.test(methodBody) && !contextPattern.test(methodBody)) {
|
|
485
|
+
const globalMatch = methodBody.match(globalTxPattern);
|
|
486
|
+
return { operation: txMethod, type: 'global-transaction', matchIndex: globalMatch.index };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
checkQueryRunnerAccess_InMethod(methodBody) {
|
|
494
|
+
// Check for QueryRunner usage
|
|
495
|
+
const queryRunnerPatterns = [
|
|
496
|
+
/this\.(dataSource|connection)\s*\.\s*createQueryRunner\s*\(/i,
|
|
497
|
+
/this\.(dataSource|connection)\s*\.\s*getQueryRunner\s*\(/i,
|
|
498
|
+
/\bcreateQueryRunner\s*\(\s*\)/i,
|
|
499
|
+
/queryRunner\s*\.\s*(connect|startTransaction|commitTransaction|rollbackTransaction|release)/i
|
|
500
|
+
];
|
|
501
|
+
|
|
502
|
+
for (const pattern of queryRunnerPatterns) {
|
|
503
|
+
const match = methodBody.match(pattern);
|
|
504
|
+
if (match) {
|
|
505
|
+
return { operation: 'QueryRunner', detected: true, matchIndex: match.index };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
checkSchemaOperations_InMethod(methodBody) {
|
|
513
|
+
// Check for schema and migration operations
|
|
514
|
+
for (const schemaOp of this.schemaOperations) {
|
|
515
|
+
const schemaPattern = new RegExp(`this\\.(dataSource|connection|db)\\s*\\.\\s*${schemaOp}\\s*\\(`, 'i');
|
|
516
|
+
const match = methodBody.match(schemaPattern);
|
|
517
|
+
if (match) {
|
|
518
|
+
return { operation: schemaOp, severity: 'error', matchIndex: match.index };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ============================================================
|
|
526
|
+
// SERVICE CLASS-LEVEL CHECKERS
|
|
527
|
+
// ============================================================
|
|
528
|
+
|
|
295
529
|
checkRepositoryInjection(cls) {
|
|
296
530
|
try {
|
|
297
531
|
// Check constructor parameters
|
|
@@ -329,6 +563,54 @@ class C033SymbolBasedAnalyzer {
|
|
|
329
563
|
return false;
|
|
330
564
|
}
|
|
331
565
|
|
|
566
|
+
checkDatabaseInjection(cls) {
|
|
567
|
+
try {
|
|
568
|
+
// Check constructor parameters for database-related injections
|
|
569
|
+
const constructor = cls.getConstructors()[0];
|
|
570
|
+
if (constructor) {
|
|
571
|
+
const params = constructor.getParameters();
|
|
572
|
+
for (const param of params) {
|
|
573
|
+
const paramName = param.getName().toLowerCase();
|
|
574
|
+
const paramType = param.getType().getText().toLowerCase();
|
|
575
|
+
|
|
576
|
+
// Check for database connection properties
|
|
577
|
+
for (const dbProp of this.databaseProperties) {
|
|
578
|
+
if (paramName.includes(dbProp) || paramType.includes(dbProp)) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Check for ORM framework types
|
|
584
|
+
for (const framework of this.ormFrameworks) {
|
|
585
|
+
if (paramType.includes(framework.toLowerCase())) {
|
|
586
|
+
// Exclude Repository types - those are good
|
|
587
|
+
if (framework.includes('Repository')) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Check class properties
|
|
597
|
+
const properties = cls.getProperties();
|
|
598
|
+
for (const prop of properties) {
|
|
599
|
+
const propName = prop.getName().toLowerCase();
|
|
600
|
+
|
|
601
|
+
for (const dbProp of this.databaseProperties) {
|
|
602
|
+
if (propName.includes(dbProp)) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
// Ignore errors
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
332
614
|
checkDirectDatabaseAccess(cls) {
|
|
333
615
|
try {
|
|
334
616
|
const classText = cls.getText();
|
|
@@ -354,6 +636,10 @@ class C033SymbolBasedAnalyzer {
|
|
|
354
636
|
return false;
|
|
355
637
|
}
|
|
356
638
|
|
|
639
|
+
// ============================================================
|
|
640
|
+
// REPOSITORY FILE ANALYSIS
|
|
641
|
+
// ============================================================
|
|
642
|
+
|
|
357
643
|
analyzeRepositoryFile(sourceFile, filePath) {
|
|
358
644
|
const violations = [];
|
|
359
645
|
const classes = sourceFile.getClasses();
|
|
@@ -475,6 +761,10 @@ class C033SymbolBasedAnalyzer {
|
|
|
475
761
|
return false;
|
|
476
762
|
}
|
|
477
763
|
|
|
764
|
+
// ============================================================
|
|
765
|
+
// CONTROLLER FILE ANALYSIS
|
|
766
|
+
// ============================================================
|
|
767
|
+
|
|
478
768
|
analyzeControllerFile(sourceFile, filePath) {
|
|
479
769
|
const violations = [];
|
|
480
770
|
const classes = sourceFile.getClasses();
|