@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.
@@ -9,44 +9,91 @@ class C033SymbolBasedAnalyzer {
9
9
  this.semanticEngine = semanticEngine;
10
10
  this.verbose = false;
11
11
 
12
- // Database operation patterns (specific to avoid false positives)
12
+ // ============================================================
13
+ // PATTERN DEFINITIONS - Organized by category
14
+ // ============================================================
15
+
16
+ // Database CRUD operations (TypeORM, Prisma, Sequelize, etc.)
13
17
  this.databaseOperations = [
14
- // ORM-specific methods
18
+ // Query operations
15
19
  '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',
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
- // 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'
28
+ 'query', 'exec', 'execute', 'run', 'rawQuery', 'raw',
29
+ // ORM-specific
30
+ 'flush', 'clear', 'refresh', 'reload', 'count', 'aggregate'
30
31
  ];
31
32
 
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'
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
- // ORM framework indicators
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', 'EntityManager', 'EntityRepository',
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 if Service uses dependency injection for Repository
284
+ // Check class-level patterns
209
285
  const hasRepositoryInjection = this.checkRepositoryInjection(cls);
210
- const hasDirectDbAccess = this.checkDirectDatabaseAccess(cls);
286
+ const hasDatabaseInjection = this.checkDatabaseInjection(cls);
211
287
 
212
- if (hasDirectDbAccess && !hasRepositoryInjection) {
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
- // 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 = '';
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
- 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
- }
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
- // 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
- }
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 (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
- });
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 violations;
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();