@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.
- package/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -1,29 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
//
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
'
|
|
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
|
-
//
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
try {
|
|
74
|
+
if (!this.semanticEngine?.project) {
|
|
75
|
+
return violations;
|
|
76
|
+
}
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
continue;
|
|
86
|
+
if (this.verbose) {
|
|
87
|
+
console.log(`[C033] Analyzing ${filePath} as ${fileType}`);
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
violations.push(...
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
const
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
190
|
+
if (this.verbose) {
|
|
191
|
+
console.log(`[C033-DEBUG] File ${filePath} detected as UNKNOWN`);
|
|
192
|
+
}
|
|
193
|
+
return 'unknown';
|
|
121
194
|
}
|
|
122
195
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
//
|
|
136
|
-
|
|
203
|
+
// Skip if this is a Repository class (in mixed files)
|
|
204
|
+
if (className.toLowerCase().includes('repository')) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
137
207
|
|
|
138
|
-
if
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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:
|
|
155
|
-
column:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
-
//
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
326
|
+
// Ignore errors
|
|
190
327
|
}
|
|
328
|
+
|
|
329
|
+
return false;
|
|
191
330
|
}
|
|
192
331
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
|
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
|
|
261
|
-
const
|
|
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
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
|
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
|
|