@sun-asterisk/sunlint 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/CONTRIBUTING.md +249 -605
  3. package/README.md +3 -4
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/large-project.json +143 -0
  7. package/config/presets/all.json +0 -1
  8. package/config/release.json +70 -0
  9. package/config/rule-analysis-strategies.js +38 -3
  10. package/config/rules/enhanced-rules-registry.json +474 -1179
  11. package/config/rules/rules-registry-generated.json +3 -3
  12. package/core/cli-action-handler.js +24 -30
  13. package/core/cli-program.js +11 -3
  14. package/core/config-merger.js +29 -2
  15. package/core/enhanced-rules-registry.js +3 -2
  16. package/core/semantic-engine.js +129 -19
  17. package/core/semantic-rule-base.js +4 -2
  18. package/core/unified-rule-registry.js +1 -1
  19. package/docs/COMMAND-EXAMPLES.md +134 -0
  20. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  21. package/engines/heuristic-engine.js +135 -16
  22. package/integrations/eslint/plugin/index.js +0 -2
  23. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
  24. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
  25. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
  26. package/origin-rules/common-en.md +19 -15
  27. package/package.json +1 -1
  28. package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
  29. package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
  30. package/rules/common/C006_function_naming/analyzer.js +29 -3
  31. package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
  32. package/rules/common/C010_limit_block_nesting/config.json +64 -0
  33. package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
  34. package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
  35. package/rules/common/C013_no_dead_code/analyzer.js +75 -177
  36. package/rules/common/C013_no_dead_code/config.json +61 -0
  37. package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
  38. package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
  39. package/rules/common/C014_dependency_injection/analyzer.js +48 -313
  40. package/rules/common/C014_dependency_injection/config.json +26 -0
  41. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
  42. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  43. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  44. package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
  45. package/rules/common/C018_no_throw_generic_error/config.json +50 -0
  46. package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
  47. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
  48. package/rules/common/C019_log_level_usage/analyzer.js +110 -317
  49. package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
  50. package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
  51. package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
  52. package/rules/common/C023_no_duplicate_variable/config.json +50 -0
  53. package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
  54. package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
  55. package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
  56. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
  57. package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
  58. package/rules/common/C033_separate_service_repository/README.md +78 -0
  59. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  60. package/rules/common/C033_separate_service_repository/config.json +50 -0
  61. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  62. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  63. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  64. package/rules/common/C035_error_logging_context/analyzer.js +232 -0
  65. package/rules/common/C035_error_logging_context/config.json +54 -0
  66. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  67. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  68. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  69. package/rules/common/C040_centralized_validation/config.json +46 -0
  70. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  71. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  72. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  73. package/rules/common/C076_explicit_function_types/README.md +30 -0
  74. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  75. package/rules/common/C076_explicit_function_types/config.json +15 -0
  76. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  77. package/rules/index.js +6 -1
  78. package/rules/parser/rule-parser.js +13 -2
  79. package/rules/security/S005_no_origin_auth/README.md +226 -0
  80. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  81. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  82. package/rules/security/S005_no_origin_auth/config.json +85 -0
  83. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  84. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  85. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  86. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  87. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  88. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  89. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  90. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  91. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  92. package/rules/security/S009_no_insecure_encryption/README.md +158 -0
  93. package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
  94. package/rules/security/S009_no_insecure_encryption/config.json +55 -0
  95. package/rules/security/S010_no_insecure_encryption/README.md +224 -0
  96. package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
  97. package/rules/security/S010_no_insecure_encryption/config.json +48 -0
  98. package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
  99. package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
  100. package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
  101. package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
  102. package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
  103. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  104. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  105. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  106. package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
  107. package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
  108. package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
  109. package/rules/security/S055_content_type_validation/README.md +176 -0
  110. package/rules/security/S055_content_type_validation/analyzer.js +312 -0
  111. package/rules/security/S055_content_type_validation/config.json +48 -0
  112. package/rules/utils/rule-helpers.js +140 -1
  113. package/scripts/consolidate-config.js +116 -0
  114. package/scripts/prepare-release.sh +1 -1
  115. package/config/rules/rules-registry.json +0 -765
  116. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  117. package/docs/FUTURE_PACKAGES.md +0 -83
  118. package/docs/HEURISTIC_VS_AI.md +0 -113
  119. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  120. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  121. package/docs/RELEASE_GUIDE.md +0 -230
  122. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  123. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  124. package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
@@ -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;