@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.
- package/CHANGELOG.md +115 -1
- package/CONTRIBUTING.md +249 -605
- package/README.md +3 -4
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- 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 +38 -3
- package/config/rules/enhanced-rules-registry.json +474 -1179
- package/config/rules/rules-registry-generated.json +3 -3
- package/core/cli-action-handler.js +24 -30
- package/core/cli-program.js +11 -3
- package/core/config-merger.js +29 -2
- package/core/enhanced-rules-registry.js +3 -2
- package/core/semantic-engine.js +129 -19
- package/core/semantic-rule-base.js +4 -2
- package/core/unified-rule-registry.js +1 -1
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/heuristic-engine.js +135 -16
- package/integrations/eslint/plugin/index.js +0 -2
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +19 -15
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- 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 +232 -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/{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 +6 -1
- 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/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -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/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/consolidate-config.js +116 -0
- package/scripts/prepare-release.sh +1 -1
- package/config/rules/rules-registry.json +0 -765
- 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/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- 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;
|