@sun-asterisk/sunlint 1.2.2 → 1.3.1
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/CHANGELOG.md +107 -1
- package/CONTRIBUTING.md +1654 -66
- package/README.md +19 -6
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/engines/engines-enhanced.json +86 -0
- package/config/engines/semantic-config.json +114 -0
- package/config/eslint-rule-mapping.json +50 -38
- package/config/large-project.json +143 -0
- package/config/presets/all.json +0 -1
- package/config/release.json +70 -0
- package/config/rule-analysis-strategies.js +23 -4
- package/config/rules/S027-categories.json +122 -0
- package/config/rules/enhanced-rules-registry.json +2564 -0
- package/config/rules/rules-registry-generated.json +785 -837
- package/config/rules/rules-registry.json +13 -1
- package/core/adapters/sunlint-rule-adapter.js +25 -30
- package/core/analysis-orchestrator.js +42 -2
- package/core/categories.js +52 -0
- package/core/category-constants.js +39 -0
- package/core/cli-action-handler.js +53 -32
- package/core/cli-program.js +11 -3
- package/core/config-manager.js +111 -0
- package/core/config-merger.js +88 -0
- package/core/constants/categories.js +168 -0
- package/core/constants/defaults.js +165 -0
- package/core/constants/engines.js +185 -0
- package/core/constants/index.js +30 -0
- package/core/constants/rules.js +215 -0
- package/core/enhanced-rules-registry.js +3 -3
- package/core/file-targeting-service.js +128 -7
- package/core/interfaces/rule-plugin.interface.js +207 -0
- package/core/plugin-manager.js +448 -0
- package/core/rule-selection-service.js +42 -15
- package/core/semantic-engine.js +658 -0
- package/core/semantic-rule-base.js +433 -0
- package/core/unified-rule-registry.js +484 -0
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/core/base-engine.js +249 -0
- package/engines/engine-factory.js +275 -0
- package/engines/eslint-engine.js +171 -19
- package/engines/heuristic-engine.js +569 -78
- package/integrations/eslint/plugin/index.js +26 -28
- package/origin-rules/common-en.md +8 -8
- package/package.json +10 -6
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
- package/rules/common/C033_separate_service_repository/README.md +78 -0
- package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
- package/rules/common/C033_separate_service_repository/config.json +50 -0
- package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
- package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
- package/rules/common/C035_error_logging_context/analyzer.js +230 -0
- package/rules/common/C035_error_logging_context/config.json +54 -0
- package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
- package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
- package/rules/common/C040_centralized_validation/analyzer.js +165 -0
- package/rules/common/C040_centralized_validation/config.json +46 -0
- package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
- package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
- package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
- package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
- package/rules/common/C076_explicit_function_types/README.md +30 -0
- package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
- package/rules/common/C076_explicit_function_types/config.json +15 -0
- package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
- package/rules/index.js +8 -0
- package/rules/parser/rule-parser.js +13 -2
- package/rules/security/S005_no_origin_auth/README.md +226 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
- package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
- package/rules/security/S005_no_origin_auth/config.json +85 -0
- package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
- package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
- package/rules/security/S007_no_plaintext_otp/README.md +198 -0
- package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
- package/rules/security/S007_no_plaintext_otp/config.json +79 -0
- package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
- package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
- package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
- package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
- package/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -0
- package/scripts/migrate-rule-registry.js +157 -0
- package/scripts/prepare-release.sh +1 -1
- package/scripts/validate-system.js +48 -0
- package/.sunlint.json +0 -35
- package/config/README.md +0 -88
- package/config/engines/eslint-rule-mapping.json +0 -74
- package/config/schemas/sunlint-schema.json +0 -0
- package/config/testing/test-s005-working.ts +0 -22
- package/core/multi-rule-runner.js +0 -0
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
- package/docs/FUTURE_PACKAGES.md +0 -83
- package/docs/HEURISTIC_VS_AI.md +0 -113
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
- package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
- package/docs/RELEASE_GUIDE.md +0 -230
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regex-based analyzer for: C033 – Tách logic xử lý và truy vấn dữ liệu trong service layer
|
|
3
|
+
* Purpose: Use regex patterns to detect violations (fallback approach)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class C033RegexBasedAnalyzer {
|
|
7
|
+
constructor(semanticEngine = null) {
|
|
8
|
+
this.ruleId = 'C033';
|
|
9
|
+
this.ruleName = 'Separate Service and Repository Logic';
|
|
10
|
+
this.description = 'Tách logic xử lý và truy vấn dữ liệu trong service layer - Repository chỉ chứa CRUD, Service chứa business logic';
|
|
11
|
+
this.semanticEngine = semanticEngine;
|
|
12
|
+
this.verbose = false;
|
|
13
|
+
|
|
14
|
+
// Database method patterns to detect in Services - be very specific to avoid array methods
|
|
15
|
+
this.dbMethods = [
|
|
16
|
+
// ORM specific methods
|
|
17
|
+
'findOneBy', 'findBy', 'findAndCount', 'findByIds',
|
|
18
|
+
// Generic CRUD but avoid conflict with array methods
|
|
19
|
+
'createQueryBuilder', 'getRepository', 'getManager', 'getConnection',
|
|
20
|
+
'save', 'insert', 'upsert', 'persist',
|
|
21
|
+
'update', 'patch', 'merge',
|
|
22
|
+
'delete', 'remove', 'softDelete', 'destroy',
|
|
23
|
+
'query', 'exec', 'execute', 'run',
|
|
24
|
+
// Specific ORM methods
|
|
25
|
+
'flush', 'clear', 'refresh', 'reload',
|
|
26
|
+
// SQL builder methods - be careful about join (conflicts with array.join)
|
|
27
|
+
'select', 'from', 'where', 'innerJoin', 'leftJoin', 'rightJoin',
|
|
28
|
+
'orderBy', 'groupBy', 'having', 'limit', 'offset'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Business logic indicators to detect in Repositories - be more specific
|
|
32
|
+
this.businessLogicIndicators = [
|
|
33
|
+
'calculateTotal', 'computeAmount', 'processPayment', 'transformData',
|
|
34
|
+
'validateInput', 'verifyCredentials', 'checkPermission', 'ensureValid',
|
|
35
|
+
'formatOutput', 'convertCurrency', 'parseRequest', 'serializeResponse',
|
|
36
|
+
'applyBusinessRule', 'enforcePolicy', 'executeWorkflow'
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize with semantic engine
|
|
42
|
+
*/
|
|
43
|
+
async initialize(semanticEngine = null) {
|
|
44
|
+
if (semanticEngine) {
|
|
45
|
+
this.semanticEngine = semanticEngine;
|
|
46
|
+
}
|
|
47
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
48
|
+
|
|
49
|
+
if (this.verbose) {
|
|
50
|
+
console.log(`[DEBUG] 🔧 C033: Semantic analyzer initialized`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async analyze(files, language, options = {}) {
|
|
55
|
+
const violations = [];
|
|
56
|
+
|
|
57
|
+
// Prefer semantic analysis if available
|
|
58
|
+
if (this.semanticEngine?.project) {
|
|
59
|
+
for (const filePath of files) {
|
|
60
|
+
try {
|
|
61
|
+
const fileViolations = await this.analyzeWithSemantics(filePath, options);
|
|
62
|
+
violations.push(...fileViolations);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (this.verbose || options.verbose) {
|
|
65
|
+
console.warn(`[C033] Semantic analysis failed for ${filePath}:`, error.message);
|
|
66
|
+
}
|
|
67
|
+
// Fallback to basic heuristic analysis
|
|
68
|
+
const fallbackViolations = await this.analyzeFileBasic(filePath, options);
|
|
69
|
+
violations.push(...fallbackViolations);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// Fallback to basic analysis without ts-morph
|
|
74
|
+
for (const filePath of files) {
|
|
75
|
+
const fileViolations = await this.analyzeFileBasic(filePath, options);
|
|
76
|
+
violations.push(...fileViolations);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return violations;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Analyze file using ts-morph semantic engine
|
|
85
|
+
*/
|
|
86
|
+
async analyzeWithSemantics(filePath, options = {}) {
|
|
87
|
+
const violations = [];
|
|
88
|
+
|
|
89
|
+
const sourceFile = this.semanticEngine.project.getSourceFileByFilePath(filePath);
|
|
90
|
+
if (!sourceFile) {
|
|
91
|
+
if (this.verbose) {
|
|
92
|
+
console.warn(`[C033] Source file not found in ts-morph project: ${filePath}`);
|
|
93
|
+
}
|
|
94
|
+
return violations;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Classify file type based on semantic analysis
|
|
98
|
+
const fileType = this.classifyFileWithSemantics(sourceFile, filePath);
|
|
99
|
+
|
|
100
|
+
if (fileType === 'service') {
|
|
101
|
+
violations.push(...this.analyzeServiceWithSemantics(sourceFile, filePath));
|
|
102
|
+
} else if (fileType === 'repository') {
|
|
103
|
+
violations.push(...this.analyzeRepositoryWithSemantics(sourceFile, filePath));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return violations;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Classify file type using semantic analysis
|
|
111
|
+
*/
|
|
112
|
+
classifyFileWithSemantics(sourceFile, filePath) {
|
|
113
|
+
const fileName = sourceFile.getBaseName().toLowerCase();
|
|
114
|
+
|
|
115
|
+
// First check if this is just a type definition file - skip these
|
|
116
|
+
const hasOnlyTypes = this.isTypeDefinitionFile(sourceFile);
|
|
117
|
+
if (hasOnlyTypes) {
|
|
118
|
+
return 'unknown';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check filename patterns first - be more specific
|
|
122
|
+
if (/service\.ts$|service\.js$/i.test(fileName)) return 'service';
|
|
123
|
+
if (/repository\.ts$|repository\.js$|repo\.ts$|repo\.js$/i.test(fileName)) return 'repository';
|
|
124
|
+
|
|
125
|
+
// Analyze class names and decorators - only if there are actual classes
|
|
126
|
+
const classes = sourceFile.getClasses();
|
|
127
|
+
if (classes.length === 0) {
|
|
128
|
+
return 'unknown'; // No classes, likely just functions/types
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const cls of classes) {
|
|
132
|
+
const className = cls.getName()?.toLowerCase() || '';
|
|
133
|
+
|
|
134
|
+
// Check class names - be more specific
|
|
135
|
+
if (/service$/.test(className)) return 'service';
|
|
136
|
+
if (/repository$|repo$/.test(className)) return 'repository';
|
|
137
|
+
|
|
138
|
+
// Check decorators
|
|
139
|
+
const decorators = cls.getDecorators();
|
|
140
|
+
for (const decorator of decorators) {
|
|
141
|
+
const decoratorName = decorator.getName().toLowerCase();
|
|
142
|
+
if (decoratorName.includes('service')) return 'service';
|
|
143
|
+
if (decoratorName.includes('repository')) return 'repository';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if class has methods that indicate it's a service/repository
|
|
147
|
+
const methods = cls.getMethods();
|
|
148
|
+
if (methods.length > 0) {
|
|
149
|
+
const hasDbMethods = methods.some(m =>
|
|
150
|
+
this.dbMethods.some(dbMethod => m.getName().toLowerCase().includes(dbMethod))
|
|
151
|
+
);
|
|
152
|
+
const hasBusinessMethods = methods.some(m =>
|
|
153
|
+
this.businessLogicIndicators.some(indicator => m.getName().toLowerCase().includes(indicator))
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (hasDbMethods && !hasBusinessMethods) return 'repository';
|
|
157
|
+
if (hasBusinessMethods && !hasDbMethods) return 'service';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check imports for framework patterns - but only as last resort
|
|
162
|
+
const imports = sourceFile.getImportDeclarations();
|
|
163
|
+
let hasOrmImports = false;
|
|
164
|
+
let hasServiceImports = false;
|
|
165
|
+
|
|
166
|
+
for (const importDecl of imports) {
|
|
167
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue().toLowerCase();
|
|
168
|
+
|
|
169
|
+
if (/typeorm|sequelize|mongoose|prisma|knex/.test(moduleSpecifier)) {
|
|
170
|
+
hasOrmImports = true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (/service|business|usecase/.test(moduleSpecifier)) {
|
|
174
|
+
hasServiceImports = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Only classify based on imports if we have clear indicators AND actual implementations
|
|
179
|
+
if (hasOrmImports && !hasServiceImports && classes.length > 0) return 'repository';
|
|
180
|
+
if (hasServiceImports && !hasOrmImports && classes.length > 0) return 'service';
|
|
181
|
+
|
|
182
|
+
return 'unknown';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if file contains only type definitions (interfaces, types, enums)
|
|
187
|
+
*/
|
|
188
|
+
isTypeDefinitionFile(sourceFile) {
|
|
189
|
+
const interfaces = sourceFile.getInterfaces();
|
|
190
|
+
const typeAliases = sourceFile.getTypeAliases();
|
|
191
|
+
const enums = sourceFile.getEnums();
|
|
192
|
+
const classes = sourceFile.getClasses();
|
|
193
|
+
const functions = sourceFile.getFunctions();
|
|
194
|
+
const variableStatements = sourceFile.getVariableStatements();
|
|
195
|
+
|
|
196
|
+
// If we have only interfaces, types, and enums, it's a type definition file
|
|
197
|
+
const hasOnlyTypes = (interfaces.length > 0 || typeAliases.length > 0 || enums.length > 0) &&
|
|
198
|
+
classes.length === 0 &&
|
|
199
|
+
functions.length === 0 &&
|
|
200
|
+
variableStatements.length === 0;
|
|
201
|
+
|
|
202
|
+
return hasOnlyTypes;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Analyze Service files using semantic analysis
|
|
207
|
+
*/
|
|
208
|
+
analyzeServiceWithSemantics(sourceFile, filePath) {
|
|
209
|
+
const violations = [];
|
|
210
|
+
const classes = sourceFile.getClasses();
|
|
211
|
+
|
|
212
|
+
for (const cls of classes) {
|
|
213
|
+
const methods = cls.getMethods();
|
|
214
|
+
|
|
215
|
+
for (const method of methods) {
|
|
216
|
+
violations.push(...this.analyzeServiceMethod(method, filePath, cls.getName()));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return violations;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Analyze Service method for direct database calls using AST
|
|
225
|
+
*/
|
|
226
|
+
analyzeServiceMethod(method, filePath, className) {
|
|
227
|
+
const violations = [];
|
|
228
|
+
const methodName = method.getName();
|
|
229
|
+
|
|
230
|
+
// Get all call expressions in the method
|
|
231
|
+
const callExpressions = method.getDescendantsOfKind(this.getKind('CallExpression'));
|
|
232
|
+
|
|
233
|
+
for (const callExpr of callExpressions) {
|
|
234
|
+
const expression = callExpr.getExpression();
|
|
235
|
+
|
|
236
|
+
// Check for property access patterns (obj.method())
|
|
237
|
+
if (expression.getKind() === this.getKind('PropertyAccessExpression')) {
|
|
238
|
+
const propertyName = expression.getNameNode().getText();
|
|
239
|
+
|
|
240
|
+
// Exclude queue/job operations first (before checking dbMethods)
|
|
241
|
+
if (this.isQueueOperation(callExpr, propertyName)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check if it's a database method call
|
|
246
|
+
if (this.dbMethods.includes(propertyName)) {
|
|
247
|
+
// Check if it's not going through repository
|
|
248
|
+
if (!this.isCallThroughRepository(callExpr)) {
|
|
249
|
+
const lineNumber = callExpr.getStartLineNumber();
|
|
250
|
+
const columnNumber = callExpr.getStart() - sourceFile.getLineStartPos(lineNumber - 1) + 1;
|
|
251
|
+
|
|
252
|
+
violations.push({
|
|
253
|
+
ruleId: 'C033',
|
|
254
|
+
severity: 'warning',
|
|
255
|
+
message: `Service should not contain direct database calls`,
|
|
256
|
+
source: 'C033',
|
|
257
|
+
file: filePath,
|
|
258
|
+
line: lineNumber,
|
|
259
|
+
column: columnNumber,
|
|
260
|
+
description: `[REGEX-FALLBACK] Direct database call '${propertyName}()' found in Service method '${methodName}'. Move database access to Repository layer.`,
|
|
261
|
+
suggestion: 'Inject Repository dependency and use repository methods for data access',
|
|
262
|
+
category: 'architecture'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return violations;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Analyze Repository files using semantic analysis
|
|
274
|
+
*/
|
|
275
|
+
analyzeRepositoryWithSemantics(sourceFile, filePath) {
|
|
276
|
+
const violations = [];
|
|
277
|
+
const classes = sourceFile.getClasses();
|
|
278
|
+
|
|
279
|
+
for (const cls of classes) {
|
|
280
|
+
const methods = cls.getMethods();
|
|
281
|
+
|
|
282
|
+
for (const method of methods) {
|
|
283
|
+
violations.push(...this.analyzeRepositoryMethod(method, filePath, cls.getName()));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return violations;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Analyze Repository method for business logic using AST
|
|
292
|
+
*/
|
|
293
|
+
analyzeRepositoryMethod(method, filePath, className) {
|
|
294
|
+
const violations = [];
|
|
295
|
+
const methodName = method.getName();
|
|
296
|
+
|
|
297
|
+
// Skip basic CRUD methods from strict checking
|
|
298
|
+
if (this.isBasicCrudMethod(methodName)) {
|
|
299
|
+
return violations;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check for complex control flow
|
|
303
|
+
const ifStatements = method.getDescendantsOfKind(this.getKind('IfStatement'));
|
|
304
|
+
const forStatements = method.getDescendantsOfKind(this.getKind('ForStatement'));
|
|
305
|
+
const whileStatements = method.getDescendantsOfKind(this.getKind('WhileStatement'));
|
|
306
|
+
const switchStatements = method.getDescendantsOfKind(this.getKind('SwitchStatement'));
|
|
307
|
+
|
|
308
|
+
// Flag complex conditional logic
|
|
309
|
+
if (ifStatements.length > 2) {
|
|
310
|
+
const firstIf = ifStatements[0];
|
|
311
|
+
const lineNumber = firstIf.getStartLineNumber();
|
|
312
|
+
|
|
313
|
+
violations.push({
|
|
314
|
+
ruleId: 'C033',
|
|
315
|
+
severity: 'warning',
|
|
316
|
+
message: `Repository should not contain business logic`,
|
|
317
|
+
source: 'C033',
|
|
318
|
+
file: filePath,
|
|
319
|
+
line: lineNumber,
|
|
320
|
+
column: 1,
|
|
321
|
+
description: `Complex conditional logic (${ifStatements.length} if statements) found in Repository method '${methodName}'. Move business logic to Service layer.`,
|
|
322
|
+
suggestion: 'Move business logic to Service class and keep Repository methods simple',
|
|
323
|
+
category: 'architecture'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check for business logic in method names and identifiers
|
|
328
|
+
const methodBody = method.getBodyText() || '';
|
|
329
|
+
for (const indicator of this.businessLogicIndicators) {
|
|
330
|
+
if (new RegExp(`\\b${indicator}\\b`, 'i').test(methodBody)) {
|
|
331
|
+
const lineNumber = method.getStartLineNumber();
|
|
332
|
+
|
|
333
|
+
violations.push({
|
|
334
|
+
ruleId: 'C033',
|
|
335
|
+
severity: 'warning',
|
|
336
|
+
message: `Repository should not contain business logic`,
|
|
337
|
+
source: 'C033',
|
|
338
|
+
file: filePath,
|
|
339
|
+
line: lineNumber,
|
|
340
|
+
column: 1,
|
|
341
|
+
description: `Business logic pattern '${indicator}' found in Repository method '${methodName}'. Move to Service layer.`,
|
|
342
|
+
suggestion: 'Keep Repository focused on data access only',
|
|
343
|
+
category: 'architecture'
|
|
344
|
+
});
|
|
345
|
+
break; // Only report once per method
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return violations;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Check if call is made through repository variable
|
|
354
|
+
*/
|
|
355
|
+
isCallThroughRepository(callExpr) {
|
|
356
|
+
const expression = callExpr.getExpression();
|
|
357
|
+
|
|
358
|
+
if (expression.getKind() === this.getKind('PropertyAccessExpression')) {
|
|
359
|
+
const object = expression.getExpression();
|
|
360
|
+
const objectText = object.getText().toLowerCase();
|
|
361
|
+
|
|
362
|
+
// Check if the object variable name suggests it's a repository
|
|
363
|
+
return /repository|repo|dao|store/.test(objectText);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Check if method is basic CRUD
|
|
371
|
+
*/
|
|
372
|
+
isBasicCrudMethod(methodName) {
|
|
373
|
+
const crudPatterns = [
|
|
374
|
+
/^find/, /^get/, /^save/, /^create/, /^update/, /^delete/, /^remove/,
|
|
375
|
+
/^list/, /^search/, /^count/, /^exists/, /^has/
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
return crudPatterns.some(pattern => pattern.test(methodName.toLowerCase()));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get SyntaxKind with fallback
|
|
383
|
+
*/
|
|
384
|
+
getKind(kindName) {
|
|
385
|
+
// Try to get from semantic engine
|
|
386
|
+
if (this.semanticEngine?.SyntaxKind?.[kindName]) {
|
|
387
|
+
return this.semanticEngine.SyntaxKind[kindName];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Fallback to common TypeScript SyntaxKind values
|
|
391
|
+
const fallbackKinds = {
|
|
392
|
+
'CallExpression': 214,
|
|
393
|
+
'PropertyAccessExpression': 212,
|
|
394
|
+
'IfStatement': 243,
|
|
395
|
+
'ForStatement': 247,
|
|
396
|
+
'WhileStatement': 248,
|
|
397
|
+
'SwitchStatement': 259,
|
|
398
|
+
'Identifier': 79
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return fallbackKinds[kindName] || 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Basic analysis without ts-morph (fallback)
|
|
406
|
+
*/
|
|
407
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
408
|
+
const fs = require('fs');
|
|
409
|
+
const path = require('path');
|
|
410
|
+
|
|
411
|
+
if (!fs.existsSync(filePath)) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
416
|
+
const violations = [];
|
|
417
|
+
const lines = content.split('\n');
|
|
418
|
+
|
|
419
|
+
// More precise file classification - avoid false positives
|
|
420
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
421
|
+
|
|
422
|
+
// Check if it's likely just a type definition file
|
|
423
|
+
const looksLikeTypeFile = this.isLikelyTypeDefinitionFile(content, fileName);
|
|
424
|
+
if (looksLikeTypeFile) {
|
|
425
|
+
return []; // Skip type definition files
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Only classify as service/repository if filename or class patterns match precisely
|
|
429
|
+
const isService = /service\.ts$|service\.js$/i.test(fileName) ||
|
|
430
|
+
/class\s+\w*Service\b/i.test(content) ||
|
|
431
|
+
/@Service/i.test(content);
|
|
432
|
+
|
|
433
|
+
const isRepository = /repository\.ts$|repository\.js$|repo\.ts$|repo\.js$/i.test(fileName) ||
|
|
434
|
+
/class\s+\w*Repository\b/i.test(content) ||
|
|
435
|
+
/class\s+\w*Repo\b/i.test(content) ||
|
|
436
|
+
/@Repository/i.test(content);
|
|
437
|
+
|
|
438
|
+
if (isService) {
|
|
439
|
+
// Look for direct database calls in Service
|
|
440
|
+
lines.forEach((line, index) => {
|
|
441
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('*')) return;
|
|
442
|
+
|
|
443
|
+
for (const method of this.dbMethods) {
|
|
444
|
+
const pattern = new RegExp(`\\.${method}\\s*\\(`, 'i');
|
|
445
|
+
if (pattern.test(line) && !/repository|repo/i.test(line)) {
|
|
446
|
+
// Avoid false positives from built-in objects and array methods
|
|
447
|
+
if (/Array\.|Object\.|String\.|Number\.|Date\.|Math\.|JSON\.|console\./i.test(line)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Avoid false positives from Node.js built-in APIs
|
|
452
|
+
if (/Buffer\.|crypto\.|createHash\.|\.digest\(|\.alloc\(/i.test(line)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Avoid false positives from Lodash utility methods
|
|
457
|
+
if (/chain\(|_\.|lodash\.|\.map\(|\.orderBy\(|\.pick\(|\.value\(|\.filter\(/i.test(line)) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Avoid false positives from service-to-service calls
|
|
462
|
+
if (/Service\.|\.service\./i.test(line)) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Avoid false positives from this.method() calls (internal service methods)
|
|
467
|
+
if (/this\./i.test(line) && pattern.test(line)) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Avoid false positives from command/pattern/interface methods
|
|
472
|
+
if (/command\.|pattern\.|interface\.|regex\.|objPattern\./i.test(line)) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Avoid false positives from job/queue operations (acceptable in services)
|
|
477
|
+
if (/job\.|queue\.|bull\./i.test(line)) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
violations.push({
|
|
482
|
+
ruleId: 'C033',
|
|
483
|
+
severity: 'warning',
|
|
484
|
+
message: `Service should not contain direct database calls`,
|
|
485
|
+
source: 'C033',
|
|
486
|
+
file: filePath,
|
|
487
|
+
line: index + 1,
|
|
488
|
+
column: line.search(pattern) + 1,
|
|
489
|
+
description: `Direct database call '${method}()' found in Service`,
|
|
490
|
+
suggestion: 'Use Repository pattern for data access',
|
|
491
|
+
category: 'architecture'
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (isRepository) {
|
|
499
|
+
// Look for business logic in Repository
|
|
500
|
+
lines.forEach((line, index) => {
|
|
501
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('*')) return;
|
|
502
|
+
|
|
503
|
+
for (const indicator of this.businessLogicIndicators) {
|
|
504
|
+
const pattern = new RegExp(`\\b${indicator}\\b`, 'i');
|
|
505
|
+
if (pattern.test(line)) {
|
|
506
|
+
violations.push({
|
|
507
|
+
ruleId: 'C033',
|
|
508
|
+
severity: 'warning',
|
|
509
|
+
message: `Repository should not contain business logic`,
|
|
510
|
+
source: 'C033',
|
|
511
|
+
file: filePath,
|
|
512
|
+
line: index + 1,
|
|
513
|
+
column: line.search(pattern) + 1,
|
|
514
|
+
description: `Business logic pattern '${indicator}' found in Repository`,
|
|
515
|
+
suggestion: 'Move business logic to Service layer',
|
|
516
|
+
category: 'architecture'
|
|
517
|
+
});
|
|
518
|
+
break; // Only report once per line
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return violations;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Check if content looks like a type definition file (for fallback analysis)
|
|
529
|
+
*/
|
|
530
|
+
isLikelyTypeDefinitionFile(content, fileName) {
|
|
531
|
+
// Check file extension patterns that suggest types
|
|
532
|
+
if (/\.types?\.ts$|\.d\.ts$|type\.ts$/i.test(fileName)) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Count different kinds of declarations
|
|
537
|
+
const interfaceCount = (content.match(/export\s+interface\s+/g) || []).length;
|
|
538
|
+
const typeCount = (content.match(/export\s+type\s+/g) || []).length;
|
|
539
|
+
const enumCount = (content.match(/export\s+enum\s+/g) || []).length;
|
|
540
|
+
const classCount = (content.match(/export\s+class\s+/g) || []).length;
|
|
541
|
+
const functionCount = (content.match(/export\s+(function|const\s+\w+\s*=\s*\()/g) || []).length;
|
|
542
|
+
|
|
543
|
+
const typeDeclarations = interfaceCount + typeCount + enumCount;
|
|
544
|
+
const codeDeclarations = classCount + functionCount;
|
|
545
|
+
|
|
546
|
+
// If we have mostly type declarations and few/no code declarations
|
|
547
|
+
return typeDeclarations > 0 && (codeDeclarations === 0 || typeDeclarations > codeDeclarations * 2);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Check if this is a queue/job operation (should be excluded from database detection)
|
|
552
|
+
*/
|
|
553
|
+
isQueueOperation(callExpr, methodName) {
|
|
554
|
+
const queueMethods = [
|
|
555
|
+
'remove', 'isFailed', 'isCompleted', 'isActive', 'isWaiting', 'isDelayed',
|
|
556
|
+
'getJob', 'getJobs', 'add', 'process', 'on', 'off',
|
|
557
|
+
'retry', 'moveToCompleted', 'moveToFailed'
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
if (!queueMethods.includes(methodName)) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Check the object being called - look for queue/job patterns
|
|
565
|
+
const expression = callExpr.getExpression();
|
|
566
|
+
if (expression.getKind() === this.getKind('PropertyAccessExpression')) {
|
|
567
|
+
const objectExpr = expression.getExpression();
|
|
568
|
+
const objectText = objectExpr.getText().toLowerCase();
|
|
569
|
+
|
|
570
|
+
// Check if object looks like queue or job
|
|
571
|
+
const queuePatterns = ['queue', 'job', 'bull'];
|
|
572
|
+
const isQueueObject = queuePatterns.some(pattern => objectText.includes(pattern));
|
|
573
|
+
|
|
574
|
+
if (this.verbose || queueMethods.includes(methodName)) {
|
|
575
|
+
console.log(`[DEBUG] Queue check: object="${objectText}", method="${methodName}", isQueue=${isQueueObject}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return isQueueObject;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
module.exports = C033RegexBasedAnalyzer;
|