@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,1267 @@
1
+ const { SyntaxKind } = require('ts-morph');
2
+
3
+ /**
4
+ * C019 System Log Analyzer - Simplified Version
5
+ *
6
+ * Focus Areas:
7
+ * 1. Đúng chỗ, đúng level (không bàn chuyện message/cause/fields)
8
+ * 2. Thiếu hay thừa log (ở những điểm bắt buộc phải có / nên không có)
9
+ */
10
+ class C019SystemLogAnalyzer {
11
+ constructor(semanticEngine = null) {
12
+ this.semanticEngine = semanticEngine;
13
+ this.verbose = false;
14
+
15
+ // Configuration for system-level logging rules
16
+ this.config = {
17
+ layerClassifier: {
18
+ controller: ['controller', 'route', 'handler', 'api', 'endpoint'],
19
+ job: ['job', 'worker', 'cron', 'task', 'queue', 'processor'],
20
+ service: ['service', 'business', 'domain', 'logic', 'usecase'],
21
+ infra: ['client', 'adapter', 'gateway', 'repository', 'dao', 'external']
22
+ },
23
+ requiredLogEvents: {
24
+ 'http_5xx_boundary': {
25
+ level: 'error',
26
+ confidence: 0.9,
27
+ message: 'HTTP 5xx responses must have error log at boundary',
28
+ suggestion: 'Add error log before returning 5xx status'
29
+ },
30
+ 'retry_exhausted': {
31
+ level: 'error',
32
+ confidence: 0.8,
33
+ message: 'Retry exhaustion must be logged as error',
34
+ suggestion: 'Add error log when all retry attempts fail'
35
+ }
36
+ },
37
+ overusedLogPatterns: {
38
+ 'hot_path_over_logging': {
39
+ threshold: 8, // Max logs per function (increased for business logic)
40
+ confidence: 0.5, // Lower confidence for less strict enforcement
41
+ message: 'Too many log statements in hot path function',
42
+ suggestion: 'Reduce logging frequency or use conditional logging'
43
+ },
44
+ 'loop_over_logging': {
45
+ threshold: 2, // Max logs per loop
46
+ confidence: 0.8,
47
+ message: 'Logging inside loops can impact performance',
48
+ suggestion: 'Move logs outside loop or use sampling'
49
+ }
50
+ },
51
+ missingLogPatterns: {
52
+ 'auth_failure_silent': {
53
+ confidence: 0.9,
54
+ message: 'Authentication failures should be logged for security',
55
+ suggestion: 'Add warn/error log for failed authentication attempts'
56
+ },
57
+ 'payment_transaction_silent': {
58
+ confidence: 0.9,
59
+ message: 'Payment transactions should be logged for audit',
60
+ suggestion: 'Add info log for payment processing events'
61
+ }
62
+ },
63
+ redundancyPatterns: {
64
+ 'duplicate_log_events': {
65
+ maxDistance: 10, // Lines between similar logs
66
+ confidence: 0.7,
67
+ message: 'Duplicate log events detected',
68
+ suggestion: 'Consolidate similar log statements'
69
+ }
70
+ },
71
+ distributedPatterns: {
72
+ 'external_call_silent': {
73
+ confidence: 0.7, // Reduced confidence for better precision
74
+ message: 'External service calls should be logged for monitoring',
75
+ suggestion: 'Add logs for external API/service interactions or use centralized logging'
76
+ }
77
+ },
78
+ wrongLevelPatterns: {
79
+ 'missing_data_error': {
80
+ expectedLevel: 'warn',
81
+ confidence: 0.6,
82
+ message: 'Missing/invalid data should use warn level',
83
+ suggestion: 'Use warn for expected validation failures'
84
+ },
85
+ 'retry_attempt_error': {
86
+ expectedLevel: 'warn',
87
+ confidence: 0.8,
88
+ message: 'Individual retry attempts should use warn level',
89
+ suggestion: 'Use warn for retry attempts, error only when exhausted'
90
+ }
91
+ }
92
+ };
93
+ }
94
+
95
+ async initialize(semanticEngine = null) {
96
+ if (semanticEngine) {
97
+ this.semanticEngine = semanticEngine;
98
+ }
99
+ this.verbose = semanticEngine?.verbose || false;
100
+ }
101
+
102
+ async analyzeFileBasic(filePath, options = {}) {
103
+ const violations = [];
104
+
105
+ try {
106
+ let sourceFile = this.semanticEngine.project.getSourceFile(filePath);
107
+
108
+ if (!sourceFile) {
109
+ sourceFile = this.semanticEngine.project.addSourceFileAtPath(filePath);
110
+ }
111
+
112
+ if (!sourceFile) {
113
+ sourceFile = this.semanticEngine.project.createSourceFile(filePath, '');
114
+ }
115
+
116
+ if (!sourceFile) {
117
+ throw new Error(`Could not load or create source file: ${filePath}`);
118
+ }
119
+
120
+ if (this.verbose) {
121
+ console.log(`[DEBUG] 🎯 C019: Using comprehensive system-level analysis for ${filePath.split('/').pop()}`);
122
+ }
123
+
124
+ // Skip test files - logs in tests have no production value
125
+ if (this.isTestFile(filePath)) {
126
+ if (this.verbose) {
127
+ console.log(`[DEBUG] ❌ Skipping test file: ${filePath}`);
128
+ }
129
+ return [];
130
+ }
131
+
132
+ // Skip client-side files - client logs have limited operational value
133
+ if (this.isClientSideFile(filePath, sourceFile)) {
134
+ if (this.verbose) {
135
+ console.log(`[DEBUG] ❌ Skipping client-side file: ${filePath}`);
136
+ }
137
+ return [];
138
+ }
139
+
140
+ // Classify file layer
141
+ const layer = this.classifyFileLayer(filePath, sourceFile);
142
+
143
+ // Find logging events and patterns
144
+ const logCalls = this.findLogCalls(sourceFile);
145
+ const httpReturns = this.findHttpStatusReturns(sourceFile);
146
+ const retryPatterns = this.findRetryPatterns(sourceFile);
147
+
148
+ if (this.verbose) {
149
+ console.log(`[DEBUG] 🔍 C019-System: Analyzing logging patterns in ${filePath.split('/').pop()}`);
150
+ }
151
+
152
+ // Phase 1: Analyze must-have logs
153
+ violations.push(...this.analyzeRequiredLogs(filePath, sourceFile, layer, {
154
+ logCalls, httpReturns, retryPatterns
155
+ }));
156
+
157
+ // Phase 1: Analyze wrong level usage
158
+ violations.push(...this.analyzeWrongLevelUsage(filePath, sourceFile, layer, {
159
+ logCalls, httpReturns, retryPatterns
160
+ }));
161
+
162
+ // Phase 2: Analyze overused logs
163
+ violations.push(...this.analyzeOverusedLogs(filePath, sourceFile, layer, {
164
+ logCalls
165
+ }));
166
+
167
+ // Phase 2: Analyze missing critical logs
168
+ violations.push(...this.analyzeMissingCriticalLogs(filePath, sourceFile, layer, {
169
+ logCalls, httpReturns
170
+ }));
171
+
172
+ // Phase 2: Analyze log redundancy
173
+ violations.push(...this.analyzeLogRedundancy(filePath, sourceFile, layer, {
174
+ logCalls
175
+ }));
176
+
177
+ // Phase 3: Only essential distributed logging
178
+ violations.push(...this.analyzeDistributedPatterns(filePath, sourceFile, layer, {
179
+ logCalls, httpReturns
180
+ }));
181
+
182
+ if (this.verbose) {
183
+ console.log(`[DEBUG] 🔍 C019-System: Found ${violations.length} system-level violations`);
184
+ }
185
+
186
+ return violations;
187
+ } catch (error) {
188
+ if (this.verbose) {
189
+ console.error(`[DEBUG] ❌ C019-System: Analysis error: ${error.message}`);
190
+ }
191
+ throw error;
192
+ }
193
+ }
194
+
195
+ // ===== FILE FILTERING =====
196
+
197
+ isTestFile(filePath) {
198
+ const testPatterns = [
199
+ /\.test\./i, /\.spec\./i, /__tests__/i, /__test__/i,
200
+ /test\//i, /tests\//i, /spec\//i, /specs\//i,
201
+ /\.test$/i, /\.spec$/i, /mock/i, /fixture/i
202
+ ];
203
+
204
+ return testPatterns.some(pattern => pattern.test(filePath));
205
+ }
206
+
207
+ isClientSideFile(filePath, sourceFile) {
208
+ // API routes are server-side even in frontend projects
209
+ if (/\/api\/.*\/route\./i.test(filePath)) {
210
+ return false;
211
+ }
212
+
213
+ if (this.verbose) {
214
+ console.log(`[DEBUG] 🔍 Checking client-side for: ${filePath}`);
215
+ }
216
+
217
+ const hasUseClient = sourceFile.getFullText().includes("'use client'") ||
218
+ sourceFile.getFullText().includes('"use client"');
219
+
220
+ const isReactComponent = /component/i.test(filePath) ||
221
+ /\.tsx?$/.test(filePath) && sourceFile.getFullText().includes('React');
222
+
223
+ const clientSidePaths = [
224
+ /\/components\//i, /\/pages\//i,
225
+ /\/hooks\//i, /\/context\//i, /\/providers\//i
226
+ ];
227
+
228
+ const serverSidePatterns = [
229
+ /\/api\//i, /\/server\//i, /\/backend\//i,
230
+ /\/utils\/.*(?:server|api|request)/i,
231
+ /\/lib\/.*(?:thunk|api|server)/i,
232
+ /middleware\./i, /route\./i
233
+ ];
234
+
235
+ const isServerSide = serverSidePatterns.some(pattern => pattern.test(filePath));
236
+ const isClientPath = clientSidePaths.some(pattern => pattern.test(filePath));
237
+
238
+ if (this.verbose) {
239
+ console.log(`[DEBUG] 📊 Analysis for ${filePath}:`);
240
+ console.log(`[DEBUG] - hasUseClient: ${hasUseClient}`);
241
+ console.log(`[DEBUG] - isReactComponent: ${isReactComponent}`);
242
+ console.log(`[DEBUG] - isServerSide: ${isServerSide}`);
243
+ console.log(`[DEBUG] - isClientPath: ${isClientPath}`);
244
+ }
245
+
246
+ if (isServerSide) {
247
+ if (this.verbose) {
248
+ console.log(`[DEBUG] ✅ Keeping server-side file: ${filePath}`);
249
+ }
250
+ return false;
251
+ }
252
+
253
+ const shouldExclude = hasUseClient || (isReactComponent && isClientPath);
254
+
255
+ if (this.verbose) {
256
+ console.log(`[DEBUG] ${shouldExclude ? '❌ Excluding' : '✅ Keeping'} file: ${filePath}`);
257
+ }
258
+
259
+ return shouldExclude;
260
+ }
261
+
262
+ classifyFileLayer(filePath, sourceFile) {
263
+ const lowerPath = filePath.toLowerCase();
264
+ const fileContent = sourceFile.getFullText().toLowerCase();
265
+
266
+ for (const [layer, patterns] of Object.entries(this.config.layerClassifier)) {
267
+ if (patterns.some(pattern =>
268
+ lowerPath.includes(pattern) || fileContent.includes(pattern)
269
+ )) {
270
+ return layer;
271
+ }
272
+ }
273
+
274
+ return 'unknown';
275
+ }
276
+
277
+ // ===== LOG DETECTION =====
278
+
279
+ findLogCalls(sourceFile) {
280
+ const logCalls = [];
281
+
282
+ const traverse = (node) => {
283
+ if (node.getKind() === SyntaxKind.CallExpression) {
284
+ const callExpr = node;
285
+ const logInfo = this.extractLogInfo(callExpr, sourceFile);
286
+
287
+ if (logInfo) {
288
+ logCalls.push({
289
+ node: callExpr,
290
+ level: logInfo.level,
291
+ message: logInfo.message,
292
+ fullCall: logInfo.fullCall,
293
+ position: sourceFile.getLineAndColumnAtPos(callExpr.getStart()),
294
+ surroundingCode: this.getSurroundingCode(callExpr, sourceFile)
295
+ });
296
+ }
297
+ }
298
+
299
+ node.forEachChild(child => traverse(child));
300
+ };
301
+
302
+ traverse(sourceFile);
303
+ return logCalls;
304
+ }
305
+
306
+ extractLogInfo(callExpr, sourceFile) {
307
+ const callText = callExpr.getText();
308
+
309
+ const logPatterns = [
310
+ { pattern: /(?:console|logger|log|winston|bunyan|pino)\.error\(/i, level: 'error' },
311
+ { pattern: /(?:console|logger|log|winston|bunyan|pino)\.warn\(/i, level: 'warn' },
312
+ { pattern: /(?:console|logger|log|winston|bunyan|pino)\.info\(/i, level: 'info' },
313
+ { pattern: /(?:console|logger|log|winston|bunyan|pino)\.debug\(/i, level: 'debug' },
314
+ { pattern: /Log\.e\(/i, level: 'error' },
315
+ { pattern: /Timber\.e\(/i, level: 'error' },
316
+ { pattern: /\.logError\(/i, level: 'error' },
317
+ { pattern: /\.logWarn\(/i, level: 'warn' },
318
+ { pattern: /\.logInfo\(/i, level: 'info' }
319
+ ];
320
+
321
+ for (const { pattern, level } of logPatterns) {
322
+ if (pattern.test(callText)) {
323
+ return {
324
+ level,
325
+ fullCall: callText,
326
+ message: this.extractLogMessage(callExpr)
327
+ };
328
+ }
329
+ }
330
+
331
+ return null;
332
+ }
333
+
334
+ extractLogMessage(callExpr) {
335
+ const args = callExpr.getArguments();
336
+ if (args.length === 0) return '';
337
+
338
+ const firstArg = args[0];
339
+
340
+ if (firstArg.getKind() === SyntaxKind.StringLiteral) {
341
+ return firstArg.getLiteralText();
342
+ }
343
+
344
+ if (firstArg.getKind() === SyntaxKind.TemplateExpression) {
345
+ return firstArg.getText();
346
+ }
347
+
348
+ return firstArg.getText();
349
+ }
350
+
351
+ getSurroundingCode(node, sourceFile) {
352
+ const startPos = Math.max(0, node.getStart() - 150);
353
+ const endPos = Math.min(sourceFile.getFullText().length, node.getEnd() + 150);
354
+ return sourceFile.getFullText().slice(startPos, endPos);
355
+ }
356
+
357
+ findHttpStatusReturns(sourceFile) {
358
+ const httpReturns = [];
359
+
360
+ const traverse = (node) => {
361
+ if (node.getKind() === SyntaxKind.CallExpression) {
362
+ const callExpr = node;
363
+ const callText = callExpr.getText();
364
+
365
+ // Next.js patterns
366
+ const nextJsMatch = callText.match(/NextResponse\.json\([^,]*,\s*{\s*status:\s*(\d+)/i);
367
+ if (nextJsMatch) {
368
+ httpReturns.push({
369
+ node: callExpr,
370
+ status: nextJsMatch[1],
371
+ type: 'NextResponse',
372
+ position: sourceFile.getLineAndColumnAtPos(callExpr.getStart()),
373
+ surroundingCode: this.getSurroundingCode(callExpr, sourceFile)
374
+ });
375
+ }
376
+
377
+ // Express patterns
378
+ const expressMatch = callText.match(/\.status\((\d+)\)/i);
379
+ if (expressMatch) {
380
+ httpReturns.push({
381
+ node: callExpr,
382
+ status: expressMatch[1],
383
+ type: 'Express',
384
+ position: sourceFile.getLineAndColumnAtPos(callExpr.getStart()),
385
+ surroundingCode: this.getSurroundingCode(callExpr, sourceFile)
386
+ });
387
+ }
388
+ }
389
+
390
+ node.forEachChild(child => traverse(child));
391
+ };
392
+
393
+ traverse(sourceFile);
394
+ return httpReturns;
395
+ }
396
+
397
+ findRetryPatterns(sourceFile) {
398
+ const retryPatterns = [];
399
+ const fileText = sourceFile.getFullText();
400
+
401
+ const retryIndicators = [
402
+ 'retry', 'attempt', 'backoff', 'maxRetries', 'retryCount',
403
+ 'maxAttempts', 'attemptCount', 'retryable', 'canRetry'
404
+ ];
405
+
406
+ const hasRetryPattern = retryIndicators.some(indicator =>
407
+ new RegExp(indicator, 'i').test(fileText)
408
+ );
409
+
410
+ if (hasRetryPattern) {
411
+ const traverse = (node) => {
412
+ if (node.getKind() === SyntaxKind.ForStatement ||
413
+ node.getKind() === SyntaxKind.WhileStatement) {
414
+
415
+ const loopText = node.getText();
416
+ const isRetryLoop = retryIndicators.some(indicator =>
417
+ new RegExp(indicator, 'i').test(loopText)
418
+ );
419
+
420
+ if (isRetryLoop) {
421
+ retryPatterns.push({
422
+ node: node,
423
+ type: 'retry_loop',
424
+ position: sourceFile.getLineAndColumnAtPos(node.getStart()),
425
+ surroundingCode: this.getSurroundingCode(node, sourceFile)
426
+ });
427
+ }
428
+ }
429
+
430
+ node.forEachChild(child => traverse(child));
431
+ };
432
+
433
+ traverse(sourceFile);
434
+ }
435
+
436
+ return retryPatterns;
437
+ }
438
+
439
+ // ===== PHASE 1: REQUIRED LOGS & WRONG LEVELS =====
440
+
441
+ analyzeRequiredLogs(filePath, sourceFile, layer, patterns) {
442
+ const violations = [];
443
+ const { logCalls, httpReturns, retryPatterns } = patterns;
444
+
445
+ // Rule 1: HTTP 5xx at boundary must have error log
446
+ if (layer === 'controller') {
447
+ const http5xxReturns = httpReturns.filter(ret =>
448
+ ret.status.startsWith('5')
449
+ );
450
+
451
+ for (const http5xx of http5xxReturns) {
452
+ const hasNearbyErrorLog = this.hasNearbyLog(http5xx, logCalls, 'error', 5);
453
+
454
+ if (!hasNearbyErrorLog) {
455
+ violations.push({
456
+ ruleId: 'C019',
457
+ type: 'missing_required_log',
458
+ message: this.config.requiredLogEvents.http_5xx_boundary.message,
459
+ filePath: filePath,
460
+ line: http5xx.position.line,
461
+ column: http5xx.position.column,
462
+ severity: 'warning',
463
+ category: 'logging',
464
+ confidence: this.config.requiredLogEvents.http_5xx_boundary.confidence,
465
+ suggestion: this.config.requiredLogEvents.http_5xx_boundary.suggestion,
466
+ context: {
467
+ eventType: 'http_5xx_boundary',
468
+ layer: layer,
469
+ statusCode: http5xx.status
470
+ }
471
+ });
472
+ }
473
+ }
474
+ }
475
+
476
+ // Rule 2: Retry exhausted must have error log
477
+ for (const retryPattern of retryPatterns) {
478
+ const hasExhaustedErrorLog = this.hasRetryExhaustedLog(retryPattern, logCalls);
479
+
480
+ if (!hasExhaustedErrorLog) {
481
+ violations.push({
482
+ ruleId: 'C019',
483
+ type: 'missing_required_log',
484
+ message: this.config.requiredLogEvents.retry_exhausted.message,
485
+ filePath: filePath,
486
+ line: retryPattern.position.line,
487
+ column: retryPattern.position.column,
488
+ severity: 'warning',
489
+ category: 'logging',
490
+ confidence: this.config.requiredLogEvents.retry_exhausted.confidence,
491
+ suggestion: this.config.requiredLogEvents.retry_exhausted.suggestion,
492
+ context: {
493
+ eventType: 'retry_exhausted',
494
+ layer: layer
495
+ }
496
+ });
497
+ }
498
+ }
499
+
500
+ return violations;
501
+ }
502
+
503
+ analyzeWrongLevelUsage(filePath, sourceFile, layer, patterns) {
504
+ const violations = [];
505
+ const { logCalls, httpReturns } = patterns;
506
+
507
+ for (const logCall of logCalls) {
508
+ if (logCall.level !== 'error') continue;
509
+
510
+ // Skip error logs in catch blocks (legitimate exceptions)
511
+ if (this.isInCatchBlock(logCall.node)) {
512
+ continue;
513
+ }
514
+
515
+ // Rule 1: 4xx validation should not be error
516
+ const nearby4xx = this.findNearbyHttpStatus(logCall, httpReturns, '4');
517
+ if (nearby4xx && this.isMissingDataValidation(logCall)) {
518
+ violations.push({
519
+ ruleId: 'C019',
520
+ type: 'wrong_log_level',
521
+ message: this.config.wrongLevelPatterns.missing_data_error.message,
522
+ filePath: filePath,
523
+ line: logCall.position.line,
524
+ column: logCall.position.column,
525
+ severity: 'warning',
526
+ category: 'logging',
527
+ confidence: this.config.wrongLevelPatterns.missing_data_error.confidence,
528
+ suggestion: this.config.wrongLevelPatterns.missing_data_error.suggestion,
529
+ context: {
530
+ currentLevel: 'error',
531
+ suggestedLevel: this.config.wrongLevelPatterns.missing_data_error.expectedLevel,
532
+ eventType: 'missing_data_validation',
533
+ statusCode: nearby4xx.status
534
+ }
535
+ });
536
+ }
537
+
538
+ // Rule 2: Retry attempts should not be error
539
+ if (this.isRetryAttemptLog(logCall)) {
540
+ violations.push({
541
+ ruleId: 'C019',
542
+ type: 'wrong_log_level',
543
+ message: this.config.wrongLevelPatterns.retry_attempt_error.message,
544
+ filePath: filePath,
545
+ line: logCall.position.line,
546
+ column: logCall.position.column,
547
+ severity: 'warning',
548
+ category: 'logging',
549
+ confidence: this.config.wrongLevelPatterns.retry_attempt_error.confidence,
550
+ suggestion: this.config.wrongLevelPatterns.retry_attempt_error.suggestion,
551
+ context: {
552
+ currentLevel: 'error',
553
+ suggestedLevel: this.config.wrongLevelPatterns.retry_attempt_error.expectedLevel,
554
+ eventType: 'retry_attempt'
555
+ }
556
+ });
557
+ }
558
+ }
559
+
560
+ return violations;
561
+ }
562
+
563
+ // ===== PHASE 2: OVERUSED & MISSING LOGS =====
564
+
565
+ analyzeOverusedLogs(filePath, sourceFile, layer, patterns) {
566
+ const violations = [];
567
+ const { logCalls } = patterns;
568
+
569
+ // Group logs by function/method
570
+ const functionLogs = this.groupLogsByFunction(sourceFile, logCalls);
571
+
572
+ // Check for hot path over-logging
573
+ for (const [funcNode, logs] of functionLogs) {
574
+ if (logs.length > this.config.overusedLogPatterns.hot_path_over_logging.threshold) {
575
+ const funcName = this.getFunctionName(funcNode);
576
+
577
+ violations.push({
578
+ ruleId: 'C019',
579
+ type: 'overused_logs',
580
+ message: this.config.overusedLogPatterns.hot_path_over_logging.message,
581
+ filePath: filePath,
582
+ line: logs[0].position.line,
583
+ column: logs[0].position.column,
584
+ severity: 'info',
585
+ category: 'performance',
586
+ confidence: this.config.overusedLogPatterns.hot_path_over_logging.confidence,
587
+ suggestion: this.config.overusedLogPatterns.hot_path_over_logging.suggestion,
588
+ context: {
589
+ functionName: funcName,
590
+ logCount: logs.length,
591
+ threshold: this.config.overusedLogPatterns.hot_path_over_logging.threshold,
592
+ eventType: 'hot_path_over_logging'
593
+ }
594
+ });
595
+ }
596
+ }
597
+
598
+ // Check for loop over-logging
599
+ const loopLogs = this.findLogsInLoops(sourceFile, logCalls);
600
+ for (const loopLog of loopLogs) {
601
+ violations.push({
602
+ ruleId: 'C019',
603
+ type: 'overused_logs',
604
+ message: this.config.overusedLogPatterns.loop_over_logging.message,
605
+ filePath: filePath,
606
+ line: loopLog.position.line,
607
+ column: loopLog.position.column,
608
+ severity: 'warning',
609
+ category: 'performance',
610
+ confidence: this.config.overusedLogPatterns.loop_over_logging.confidence,
611
+ suggestion: this.config.overusedLogPatterns.loop_over_logging.suggestion,
612
+ context: {
613
+ eventType: 'loop_over_logging',
614
+ loopType: loopLog.loopType
615
+ }
616
+ });
617
+ }
618
+
619
+ return violations;
620
+ }
621
+
622
+ analyzeMissingCriticalLogs(filePath, sourceFile, layer, patterns) {
623
+ const violations = [];
624
+ const { logCalls, httpReturns } = patterns;
625
+
626
+ // Check for authentication failures without logs
627
+ const authFailures = this.findAuthFailures(sourceFile, httpReturns);
628
+ for (const authFailure of authFailures) {
629
+ const hasNearbyLog = this.hasNearbyLog(authFailure, logCalls, ['warn', 'error'], 5);
630
+
631
+ if (!hasNearbyLog) {
632
+ violations.push({
633
+ ruleId: 'C019',
634
+ type: 'missing_critical_log',
635
+ message: this.config.missingLogPatterns.auth_failure_silent.message,
636
+ filePath: filePath,
637
+ line: authFailure.position.line,
638
+ column: authFailure.position.column,
639
+ severity: 'warning',
640
+ category: 'security',
641
+ confidence: this.config.missingLogPatterns.auth_failure_silent.confidence,
642
+ suggestion: this.config.missingLogPatterns.auth_failure_silent.suggestion,
643
+ context: {
644
+ eventType: 'auth_failure_silent',
645
+ statusCode: authFailure.status
646
+ }
647
+ });
648
+ }
649
+ }
650
+
651
+ // Check for payment transactions without logs
652
+ const paymentEvents = this.findPaymentEvents(sourceFile);
653
+ for (const paymentEvent of paymentEvents) {
654
+ const hasNearbyLog = this.hasNearbyLog(paymentEvent, logCalls, ['info', 'warn', 'error'], 10);
655
+
656
+ if (!hasNearbyLog) {
657
+ violations.push({
658
+ ruleId: 'C019',
659
+ type: 'missing_critical_log',
660
+ message: this.config.missingLogPatterns.payment_transaction_silent.message,
661
+ filePath: filePath,
662
+ line: paymentEvent.position.line,
663
+ column: paymentEvent.position.column,
664
+ severity: 'warning',
665
+ category: 'audit',
666
+ confidence: this.config.missingLogPatterns.payment_transaction_silent.confidence,
667
+ suggestion: this.config.missingLogPatterns.payment_transaction_silent.suggestion,
668
+ context: {
669
+ eventType: 'payment_transaction_silent',
670
+ operation: paymentEvent.operation
671
+ }
672
+ });
673
+ }
674
+ }
675
+
676
+ return violations;
677
+ }
678
+
679
+ analyzeLogRedundancy(filePath, sourceFile, layer, patterns) {
680
+ const violations = [];
681
+ const { logCalls } = patterns;
682
+
683
+ // Find duplicate log patterns
684
+ for (let i = 0; i < logCalls.length; i++) {
685
+ for (let j = i + 1; j < logCalls.length; j++) {
686
+ const log1 = logCalls[i];
687
+ const log2 = logCalls[j];
688
+
689
+ if (this.isDuplicateLogViolation(log1, log2, this.config.redundancyPatterns.duplicate_log_events.maxDistance)) {
690
+ const distance = Math.abs(log1.position.line - log2.position.line);
691
+ const similarity = this.calculateLogSimilarity(log1.message, log2.message);
692
+
693
+ violations.push({
694
+ ruleId: 'C019',
695
+ type: 'redundant_logs',
696
+ message: this.config.redundancyPatterns.duplicate_log_events.message,
697
+ filePath: filePath,
698
+ line: log2.position.line,
699
+ column: log2.position.column,
700
+ severity: 'info',
701
+ category: 'maintainability',
702
+ confidence: this.config.redundancyPatterns.duplicate_log_events.confidence,
703
+ suggestion: this.config.redundancyPatterns.duplicate_log_events.suggestion,
704
+ context: {
705
+ eventType: 'duplicate_log_events',
706
+ firstLogLine: log1.position.line,
707
+ secondLogLine: log2.position.line,
708
+ similarity: Math.round(similarity * 100),
709
+ distance: distance
710
+ }
711
+ });
712
+ }
713
+ }
714
+ }
715
+
716
+ return violations;
717
+ }
718
+
719
+ // ===== PHASE 3: ESSENTIAL DISTRIBUTED LOGGING =====
720
+
721
+ analyzeDistributedPatterns(filePath, sourceFile, layer, patterns) {
722
+ const violations = [];
723
+ const { logCalls, httpReturns } = patterns;
724
+
725
+ // Check for centralized logging first
726
+ const hasCentralizedLogging = this.hasProjectCentralizedLogging(sourceFile, filePath);
727
+
728
+ if (this.verbose) {
729
+ console.log(`[DEBUG] 🔧 Centralized logging detected: ${hasCentralizedLogging} for ${filePath.split('/').pop()}`);
730
+ }
731
+
732
+ // Skip external call logging check if centralized logging is detected
733
+ if (hasCentralizedLogging) {
734
+ if (this.verbose) {
735
+ console.log(`[DEBUG] ✅ Skipping external call logging check - centralized logging detected`);
736
+ }
737
+ return violations;
738
+ }
739
+
740
+ // Check for silent external calls only if no centralized logging
741
+ const externalCalls = this.findExternalServiceCalls(sourceFile);
742
+ for (const extCall of externalCalls) {
743
+ const hasNearbyLog = this.hasNearbyLog(extCall, logCalls, ['info', 'warn', 'error'], 5);
744
+
745
+ if (!hasNearbyLog) {
746
+ violations.push({
747
+ ruleId: 'C019',
748
+ type: 'distributed_gap',
749
+ message: this.config.distributedPatterns.external_call_silent.message,
750
+ filePath: filePath,
751
+ line: extCall.position.line,
752
+ column: extCall.position.column,
753
+ severity: 'warning',
754
+ category: 'monitoring',
755
+ confidence: this.config.distributedPatterns.external_call_silent.confidence,
756
+ suggestion: this.config.distributedPatterns.external_call_silent.suggestion,
757
+ context: {
758
+ eventType: 'external_call_silent',
759
+ serviceUrl: extCall.url,
760
+ method: extCall.method
761
+ }
762
+ });
763
+ }
764
+ }
765
+
766
+ return violations;
767
+ }
768
+
769
+ // ===== HELPER METHODS =====
770
+
771
+ groupLogsByFunction(sourceFile, logCalls) {
772
+ const functionLogs = new Map();
773
+
774
+ for (const logCall of logCalls) {
775
+ const funcNode = this.findContainingFunction(logCall.node);
776
+ if (funcNode) {
777
+ if (!functionLogs.has(funcNode)) {
778
+ functionLogs.set(funcNode, []);
779
+ }
780
+ functionLogs.get(funcNode).push(logCall);
781
+ }
782
+ }
783
+
784
+ return functionLogs;
785
+ }
786
+
787
+ findContainingFunction(node) {
788
+ let current = node.getParent();
789
+
790
+ while (current) {
791
+ const kind = current.getKind();
792
+ if (kind === SyntaxKind.FunctionDeclaration ||
793
+ kind === SyntaxKind.MethodDeclaration ||
794
+ kind === SyntaxKind.ArrowFunction ||
795
+ kind === SyntaxKind.FunctionExpression) {
796
+ return current;
797
+ }
798
+ current = current.getParent();
799
+ }
800
+
801
+ return null;
802
+ }
803
+
804
+ getFunctionName(funcNode) {
805
+ if (!funcNode) return 'anonymous';
806
+
807
+ const kind = funcNode.getKind();
808
+
809
+ if (kind === SyntaxKind.FunctionDeclaration || kind === SyntaxKind.MethodDeclaration) {
810
+ const nameNode = funcNode.getNameNode();
811
+ return nameNode ? nameNode.getText() : 'anonymous';
812
+ }
813
+
814
+ if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression) {
815
+ const parent = funcNode.getParent();
816
+ if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
817
+ const nameNode = parent.getNameNode();
818
+ return nameNode ? nameNode.getText() : 'anonymous';
819
+ }
820
+
821
+ if (parent && parent.getKind() === SyntaxKind.PropertyAssignment) {
822
+ const propName = parent.getNameNode();
823
+ return propName ? propName.getText() : 'anonymous';
824
+ }
825
+
826
+ if (parent && parent.getKind() === SyntaxKind.BinaryExpression) {
827
+ const left = parent.getLeft();
828
+ if (left && left.getKind() === SyntaxKind.PropertyAccessExpression) {
829
+ const prop = left.getNameNode();
830
+ return prop ? prop.getText() : 'anonymous';
831
+ }
832
+ }
833
+
834
+ return 'anonymous';
835
+ }
836
+
837
+ return 'anonymous';
838
+ }
839
+
840
+ findLogsInLoops(sourceFile, logCalls) {
841
+ const loopLogs = [];
842
+
843
+ for (const logCall of logCalls) {
844
+ let current = logCall.node.getParent();
845
+
846
+ while (current) {
847
+ const kind = current.getKind();
848
+ if (kind === SyntaxKind.ForStatement ||
849
+ kind === SyntaxKind.WhileStatement ||
850
+ kind === SyntaxKind.DoStatement ||
851
+ kind === SyntaxKind.ForInStatement ||
852
+ kind === SyntaxKind.ForOfStatement) {
853
+
854
+ loopLogs.push({
855
+ ...logCall,
856
+ loopType: this.getLoopTypeName(kind)
857
+ });
858
+ break;
859
+ }
860
+ current = current.getParent();
861
+ }
862
+ }
863
+
864
+ return loopLogs;
865
+ }
866
+
867
+ getLoopTypeName(kind) {
868
+ switch (kind) {
869
+ case SyntaxKind.ForStatement: return 'for';
870
+ case SyntaxKind.WhileStatement: return 'while';
871
+ case SyntaxKind.DoStatement: return 'do-while';
872
+ case SyntaxKind.ForInStatement: return 'for-in';
873
+ case SyntaxKind.ForOfStatement: return 'for-of';
874
+ default: return 'unknown';
875
+ }
876
+ }
877
+
878
+ findAuthFailures(sourceFile, httpReturns) {
879
+ return httpReturns.filter(ret =>
880
+ ret.status === '401' || ret.status === '403'
881
+ );
882
+ }
883
+
884
+ findPaymentEvents(sourceFile) {
885
+ const paymentEvents = [];
886
+ const fileText = sourceFile.getFullText().toLowerCase();
887
+
888
+ // Skip Redux slices and frontend state management
889
+ if (fileText.includes('createslice') || fileText.includes('createappslice') ||
890
+ fileText.includes('extrareducers') || fileText.includes('state.')) {
891
+ if (this.verbose) {
892
+ console.log(`[DEBUG] 💰 Skipping payment detection - Redux slice detected`);
893
+ }
894
+ return paymentEvents;
895
+ }
896
+
897
+ const paymentPatterns = [
898
+ 'payment', 'transaction', 'charge', 'refund', 'billing',
899
+ 'invoice', 'subscription', 'purchase', 'checkout'
900
+ ];
901
+
902
+ const hasPaymentPattern = paymentPatterns.some(pattern =>
903
+ new RegExp(pattern, 'i').test(fileText)
904
+ );
905
+
906
+ if (hasPaymentPattern) {
907
+ const traverse = (node) => {
908
+ if (node.getKind() === SyntaxKind.CallExpression) {
909
+ const callText = node.getText().toLowerCase();
910
+
911
+ // Only detect actual payment processing calls, not UI calculations
912
+ const paymentActionPatterns = [
913
+ /payment.*(?:process|execute|submit|create|confirm)/i,
914
+ /transaction.*(?:process|execute|submit|create|confirm)/i,
915
+ /charge.*(?:process|execute|submit|create)/i,
916
+ /refund.*(?:process|execute|submit|create)/i,
917
+ /purchase.*(?:process|execute|submit|create|complete)/i,
918
+ /checkout.*(?:process|execute|submit|complete)/i
919
+ ];
920
+
921
+ if (paymentActionPatterns.some(pattern => pattern.test(callText))) {
922
+ paymentEvents.push({
923
+ node: node,
924
+ operation: callText,
925
+ position: sourceFile.getLineAndColumnAtPos(node.getStart())
926
+ });
927
+ }
928
+ }
929
+
930
+ node.forEachChild(child => traverse(child));
931
+ };
932
+
933
+ traverse(sourceFile);
934
+ }
935
+
936
+ return paymentEvents;
937
+ }
938
+
939
+ findExternalServiceCalls(sourceFile) {
940
+ const externalCalls = [];
941
+
942
+ const traverse = (node) => {
943
+ if (node.getKind() === SyntaxKind.CallExpression) {
944
+ const callText = node.getText();
945
+
946
+ // Exclude common false positives first
947
+ const excludePatterns = [
948
+ // Config service calls (not external)
949
+ /configService\.get/i,
950
+ /process\.env\./i,
951
+ /config\.get/i,
952
+
953
+ // Local library operations (not external services)
954
+ /jwt\.(?:verify|sign|decode)/i,
955
+ /bcrypt\.(?:hash|compare)/i,
956
+ /crypto\.(?:createHash|randomBytes)/i,
957
+
958
+ // Database ORM operations (not external service calls)
959
+ /(?:repository|entity|model)\.(?:find|save|update|delete)/i,
960
+ /queryBuilder\./i,
961
+
962
+ // Internal service dependencies (NestJS/DI pattern)
963
+ /this\.[\w]+Service\./i,
964
+ /this\.[\w]+Repository\./i,
965
+ /this\.[\w]+Manager\./i,
966
+ /this\.[\w]+Client\.(?!http|fetch|post|get)/i,
967
+
968
+ // Specific service calls that are internal
969
+ /this\.service\./i,
970
+ /this\.commonCustomerService\./i,
971
+ /[\w]+Service\.get[\w]+/i,
972
+
973
+ // Cache operations (not external)
974
+ /cacheManager\./i,
975
+ /redis\.(?:get|set|del)/i,
976
+
977
+ // Local file/path operations
978
+ /path\.(?:join|resolve)/i,
979
+ /fs\.(?:readFile|writeFile)/i,
980
+ /__dirname|__filename/i
981
+ ];
982
+
983
+ const isExcluded = excludePatterns.some(pattern => pattern.test(callText));
984
+ if (isExcluded) {
985
+ return;
986
+ }
987
+
988
+ // More specific patterns for REAL external service calls
989
+ const realExternalPatterns = [
990
+ // HTTP calls with URLs
991
+ /(?:fetch|axios|http).*https?:\/\//i,
992
+ // API service calls
993
+ /(?:api|service|client)\.(?:get|post|put|delete|call|request)/i,
994
+ // Third-party service integrations
995
+ /(?:stripe|paypal|payment|billing)\.(?:charge|process|create)/i,
996
+ /(?:twilio|sendgrid|mailgun)\.(?:send|create)/i,
997
+ /(?:aws|gcp|azure)\.(?:upload|send|publish)/i,
998
+ // External auth providers
999
+ /(?:google|facebook|auth0)\.(?:verify|authenticate)/i
1000
+ ];
1001
+
1002
+ const isRealExternal = realExternalPatterns.some(pattern => pattern.test(callText));
1003
+
1004
+ if (isRealExternal) {
1005
+ externalCalls.push({
1006
+ node: node,
1007
+ url: this.extractUrl(callText),
1008
+ method: this.extractHttpMethod(callText),
1009
+ position: sourceFile.getLineAndColumnAtPos(node.getStart())
1010
+ });
1011
+ }
1012
+ }
1013
+
1014
+ node.forEachChild(child => traverse(child));
1015
+ };
1016
+
1017
+ traverse(sourceFile);
1018
+ return externalCalls;
1019
+ }
1020
+
1021
+ extractUrl(callText) {
1022
+ const urlMatch = callText.match(/['"`]([^'"`]*(?:api|http)[^'"`]*)['"`]/i);
1023
+ return urlMatch ? urlMatch[1] : 'unknown';
1024
+ }
1025
+
1026
+ extractHttpMethod(callText) {
1027
+ const methodPatterns = ['get', 'post', 'put', 'delete', 'patch'];
1028
+ for (const method of methodPatterns) {
1029
+ if (new RegExp(`\\.${method}\\s*\\(`, 'i').test(callText)) {
1030
+ return method.toUpperCase();
1031
+ }
1032
+ }
1033
+ return 'UNKNOWN';
1034
+ }
1035
+
1036
+ hasNearbyLog(targetNode, logCalls, levels, maxDistance = 10) {
1037
+ const targetLine = targetNode.position.line;
1038
+
1039
+ return logCalls.some(logCall => {
1040
+ const logLine = logCall.position.line;
1041
+ const distance = Math.abs(targetLine - logLine);
1042
+ const levelMatch = Array.isArray(levels) ? levels.includes(logCall.level) : logCall.level === levels;
1043
+ return levelMatch && distance <= maxDistance;
1044
+ });
1045
+ }
1046
+
1047
+ hasProjectCentralizedLogging(sourceFile, filePath) {
1048
+ const text = sourceFile.getFullText();
1049
+
1050
+ // Check for centralized logging patterns in the file
1051
+ const centralizedLoggingPatterns = [
1052
+ // Error handling with built-in logging
1053
+ /handleAxiosErrorWithModal/i,
1054
+ /handleError.*Modal/i,
1055
+ /interceptors\.response\.use/i,
1056
+ /interceptors\.request\.use/i,
1057
+
1058
+ // Global error handlers
1059
+ /globalErrorHandler/i,
1060
+ /global.*error.*handler/i,
1061
+ /centralized.*error/i,
1062
+ /error.*interceptor/i,
1063
+
1064
+ // API services with built-in logging
1065
+ /apiService/i,
1066
+ /service\..*error/i,
1067
+ /\.catch\(\s*handleError/i,
1068
+
1069
+ // Redux/Thunk error handlers with logging
1070
+ /rejectWithValue/i,
1071
+ /\.unwrap\(\)/i,
1072
+
1073
+ // Logger imports/usage indicating centralized approach
1074
+ /import.*logger.*from/i,
1075
+ /const.*logger.*=.*require/i,
1076
+ /logger\.error/i,
1077
+ /logger\.warn/i,
1078
+
1079
+ // Try-catch with error handling that includes logging
1080
+ /catch\s*\([^)]*\)\s*\{[^}]*(?:console\.error|logger\.error|handleError)[^}]*\}/s
1081
+ ];
1082
+
1083
+ const hasCentralizedPattern = centralizedLoggingPatterns.some(pattern => pattern.test(text));
1084
+
1085
+ // Additional check: if it's a thunk file, check for Redux error patterns
1086
+ if (filePath.includes('thunk') || filePath.includes('Thunk')) {
1087
+ const reduxErrorPatterns = [
1088
+ /rejectWithValue/i,
1089
+ /extraReducers/i,
1090
+ /\.rejected/i,
1091
+ /handleError/i,
1092
+ /errorHandler/i
1093
+ ];
1094
+
1095
+ const hasReduxErrorHandling = reduxErrorPatterns.some(pattern => pattern.test(text));
1096
+ if (hasReduxErrorHandling && this.verbose) {
1097
+ console.log(`[DEBUG] 🔄 Redux error handling patterns detected in thunk file`);
1098
+ }
1099
+
1100
+ return hasCentralizedPattern || hasReduxErrorHandling;
1101
+ }
1102
+
1103
+ return hasCentralizedPattern;
1104
+ }
1105
+
1106
+ findNearbyHttpStatus(logCall, httpReturns, statusPrefix) {
1107
+ const logLine = logCall.position.line;
1108
+
1109
+ return httpReturns.find(httpReturn => {
1110
+ const distance = Math.abs(logLine - httpReturn.position.line);
1111
+ return httpReturn.status.startsWith(statusPrefix) && distance <= 10;
1112
+ });
1113
+ }
1114
+
1115
+ hasRetryExhaustedLog(retryPattern, logCalls) {
1116
+ const retryText = retryPattern.surroundingCode.toLowerCase();
1117
+
1118
+ const exhaustedPatterns = [
1119
+ 'exhausted', 'failed', 'max.*attempt', 'max.*retr',
1120
+ 'give.*up', 'no.*more', 'final.*attempt'
1121
+ ];
1122
+
1123
+ return logCalls.some(logCall => {
1124
+ if (logCall.level !== 'error') return false;
1125
+
1126
+ const logText = logCall.surroundingCode.toLowerCase();
1127
+ return exhaustedPatterns.some(pattern =>
1128
+ new RegExp(pattern, 'i').test(logText)
1129
+ );
1130
+ });
1131
+ }
1132
+
1133
+ isInCatchBlock(node) {
1134
+ let current = node.getParent();
1135
+
1136
+ while (current) {
1137
+ if (current.getKind() === SyntaxKind.CatchClause) {
1138
+ return true;
1139
+ }
1140
+ current = current.getParent();
1141
+ }
1142
+
1143
+ return false;
1144
+ }
1145
+
1146
+ isMissingDataValidation(logCall) {
1147
+ const message = logCall.message.toLowerCase();
1148
+ const surroundingCode = logCall.surroundingCode.toLowerCase();
1149
+
1150
+ const missingDataPatterns = [
1151
+ 'missing', 'not.*found', 'empty', 'null', 'undefined',
1152
+ 'required', 'invalid.*format', 'invalid.*input'
1153
+ ];
1154
+
1155
+ return missingDataPatterns.some(pattern =>
1156
+ new RegExp(pattern, 'i').test(message + ' ' + surroundingCode)
1157
+ );
1158
+ }
1159
+
1160
+ isRetryAttemptLog(logCall) {
1161
+ const message = logCall.message.toLowerCase();
1162
+ const surroundingCode = logCall.surroundingCode.toLowerCase();
1163
+ const combinedText = message + ' ' + surroundingCode;
1164
+
1165
+ const retryAttemptPatterns = [
1166
+ /attempt\s*\d+.*fail/i,
1167
+ /retry\s*\d+.*fail/i,
1168
+ /try\s*\d+.*fail/i,
1169
+ /retrying.*\(\s*\d+\s*\/\s*\d+\s*\)/i,
1170
+ /attempt.*\(\s*\d+\s*\/\s*\d+\s*\)/i
1171
+ ];
1172
+
1173
+ const hasRetryPattern = retryAttemptPatterns.some(pattern =>
1174
+ pattern.test(combinedText)
1175
+ );
1176
+
1177
+ const isExhausted = /exhausted|final|last|max|all.*attempts|no.*more|after.*retries/i.test(combinedText);
1178
+
1179
+ return hasRetryPattern && !isExhausted;
1180
+ }
1181
+
1182
+ isDuplicateLogViolation(log1, log2, maxDistance) {
1183
+ // Skip if either log is in a utility function
1184
+ const log1Function = this.findContainingFunction(log1.node);
1185
+ const log2Function = this.findContainingFunction(log2.node);
1186
+
1187
+ const isLog1Utility = this.isUtilityFunction(log1Function);
1188
+ const isLog2Utility = this.isUtilityFunction(log2Function);
1189
+
1190
+ if (this.verbose) {
1191
+ console.log(`[DEBUG] 🔧 Checking duplicate logs at lines ${log1.position.line} and ${log2.position.line}`);
1192
+ console.log(`[DEBUG] 🔧 Log1 function: ${this.getFunctionName(log1Function) || 'unknown'}, utility: ${isLog1Utility}`);
1193
+ console.log(`[DEBUG] 🔧 Log2 function: ${this.getFunctionName(log2Function) || 'unknown'}, utility: ${isLog2Utility}`);
1194
+ }
1195
+
1196
+ if (isLog1Utility || isLog2Utility) {
1197
+ if (this.verbose) {
1198
+ console.log(`[DEBUG] ✅ Skipping duplicate log check - utility function detected`);
1199
+ }
1200
+ return false;
1201
+ }
1202
+
1203
+ // Skip if logs are in different functions (legitimate error handling)
1204
+ if (log1Function !== log2Function) {
1205
+ if (this.verbose) {
1206
+ console.log(`[DEBUG] ✅ Skipping duplicate log check - different functions`);
1207
+ }
1208
+ return false;
1209
+ }
1210
+
1211
+ const distance = Math.abs(log1.position.line - log2.position.line);
1212
+ if (distance > maxDistance) {
1213
+ return false;
1214
+ }
1215
+
1216
+ // Check if they are error handling logs (legitimate duplicates)
1217
+ const isErrorHandling = log1.level === 'error' || log2.level === 'error' ||
1218
+ log1.message.toLowerCase().includes('error') ||
1219
+ log2.message.toLowerCase().includes('error') ||
1220
+ log1.surroundingCode.includes('catch') ||
1221
+ log2.surroundingCode.includes('catch');
1222
+
1223
+ if (isErrorHandling && distance > 3) {
1224
+ if (this.verbose) {
1225
+ console.log(`[DEBUG] ✅ Skipping duplicate log check - error handling context`);
1226
+ }
1227
+ return false;
1228
+ }
1229
+
1230
+ // Check message similarity
1231
+ const similarity = this.calculateLogSimilarity(log1.message, log2.message);
1232
+ return similarity > 0.8; // 80% similar
1233
+ }
1234
+
1235
+ isUtilityFunction(functionNode) {
1236
+ if (!functionNode) return false;
1237
+
1238
+ const functionName = this.getFunctionName(functionNode);
1239
+ if (!functionName) return false;
1240
+
1241
+ const utilityPatterns = [
1242
+ /^write/, /^log/, /^handle/, /^process/, /^format/,
1243
+ /helper/, /util/, /wrapper/, /middleware/
1244
+ ];
1245
+
1246
+ return utilityPatterns.some(pattern => pattern.test(functionName.toLowerCase()));
1247
+ }
1248
+
1249
+ calculateLogSimilarity(message1, message2) {
1250
+ if (!message1 || !message2) return 0;
1251
+
1252
+ const clean1 = message1.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
1253
+ const clean2 = message2.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
1254
+
1255
+ if (clean1 === clean2) return 1;
1256
+
1257
+ const words1 = clean1.split(/\s+/);
1258
+ const words2 = clean2.split(/\s+/);
1259
+
1260
+ const intersection = words1.filter(word => words2.includes(word));
1261
+ const union = [...new Set([...words1, ...words2])];
1262
+
1263
+ return intersection.length / union.length;
1264
+ }
1265
+ }
1266
+
1267
+ module.exports = C019SystemLogAnalyzer;