@sun-asterisk/sunlint 1.3.16 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -1,140 +1,550 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* C029 Analyzer -
|
|
2
|
+
* C029 Analyzer - ts-morph based
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Validates catch block logging with AST-based analysis:
|
|
5
|
+
* 1. Detects empty catch blocks
|
|
6
|
+
* 2. Validates logging presence and quality
|
|
7
|
+
* 3. Checks log levels (error vs warn vs log)
|
|
8
|
+
* 4. Validates error variable usage
|
|
9
|
+
* 5. Checks stack trace inclusion
|
|
10
|
+
* 6. Validates context data
|
|
11
|
+
* 7. Supports delegation pattern (calling helper functions)
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
|
-
const
|
|
14
|
+
const { Project, SyntaxKind, Node } = require('ts-morph');
|
|
8
15
|
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
9
17
|
|
|
10
18
|
class C029Analyzer {
|
|
11
|
-
constructor(
|
|
19
|
+
constructor(semanticEngine = null) {
|
|
12
20
|
this.ruleId = 'C029';
|
|
13
|
-
this.ruleName = '
|
|
14
|
-
this.description = '
|
|
15
|
-
this.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
this.ruleName = 'Catch Block Logging Validation';
|
|
22
|
+
this.description = 'Validates comprehensive error logging in catch blocks';
|
|
23
|
+
this.semanticEngine = semanticEngine;
|
|
24
|
+
this.project = null;
|
|
25
|
+
this.verbose = false;
|
|
26
|
+
|
|
27
|
+
// Logging patterns configuration
|
|
28
|
+
this.config = {
|
|
29
|
+
// Valid logging methods (console, logger, etc.)
|
|
30
|
+
loggingMethods: [
|
|
31
|
+
'console.error', 'console.warn', 'console.log', 'console.info', 'console.debug',
|
|
32
|
+
'logger.error', 'logger.warn', 'logger.info', 'logger.debug', 'logger.log',
|
|
33
|
+
'log.error', 'log.warn', 'log.info', 'log.debug',
|
|
34
|
+
'this.logger.error', 'this.logger.warn', 'this.logger.info',
|
|
35
|
+
'this.logError', 'this.logErrors', 'this.couponLogErrors',
|
|
36
|
+
'Logger.error', 'Logger.warn', 'Logger.info'
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
// Appropriate log levels for catch blocks (should use error/warn)
|
|
40
|
+
appropriateLevels: ['error', 'warn'],
|
|
41
|
+
|
|
42
|
+
// Inappropriate log levels (should avoid log/info/debug in catch)
|
|
43
|
+
inappropriateLevels: ['log', 'info', 'debug'],
|
|
44
|
+
|
|
45
|
+
// Error handling methods (throw, rethrow)
|
|
46
|
+
errorHandlingMethods: [
|
|
47
|
+
'throw', 'rethrow', 'handleError', 'processError',
|
|
48
|
+
'reportError', 'trackError', 'sendError'
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
// Test file patterns (more lenient checking)
|
|
52
|
+
testPatterns: ['__tests__', '.test.', '.spec.', '/test/', '/tests/', '.stories.']
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async initialize(semanticEngine = null) {
|
|
57
|
+
if (semanticEngine) {
|
|
58
|
+
this.semanticEngine = semanticEngine;
|
|
59
|
+
}
|
|
60
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
61
|
+
|
|
62
|
+
// Use semantic engine's project if available, otherwise create standalone
|
|
63
|
+
if (this.semanticEngine?.project) {
|
|
64
|
+
this.project = this.semanticEngine.project;
|
|
22
65
|
if (this.verbose) {
|
|
23
|
-
console.log('[DEBUG] 🎯 C029:
|
|
66
|
+
console.log('[DEBUG] 🎯 C029: Using semantic engine project');
|
|
24
67
|
}
|
|
25
|
-
}
|
|
68
|
+
} else {
|
|
69
|
+
this.project = new Project({
|
|
70
|
+
compilerOptions: {
|
|
71
|
+
target: 99, // Latest
|
|
72
|
+
module: 99, // ESNext
|
|
73
|
+
allowJs: true,
|
|
74
|
+
checkJs: false,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
26
77
|
if (this.verbose) {
|
|
27
|
-
console.
|
|
78
|
+
console.log('[DEBUG] 🎯 C029: Created standalone ts-morph project');
|
|
28
79
|
}
|
|
29
|
-
this.smartPipeline = null;
|
|
30
80
|
}
|
|
31
81
|
}
|
|
32
82
|
|
|
33
83
|
async analyze(files, language, options = {}) {
|
|
34
|
-
|
|
35
|
-
this.verbose = options.verbose || this.verbose || false;
|
|
84
|
+
this.verbose = options.verbose || this.verbose;
|
|
36
85
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (this.verbose) {
|
|
40
|
-
console.log('[DEBUG] 🎯 C029: Using Smart Pipeline (3-stage analysis)...');
|
|
41
|
-
}
|
|
42
|
-
return await this.smartPipeline.analyze(files, language, options);
|
|
43
|
-
} else {
|
|
44
|
-
if (this.verbose) {
|
|
45
|
-
console.log('[DEBUG] 🔍 C029: Using fallback regex analysis...');
|
|
46
|
-
}
|
|
47
|
-
return await this.analyzeWithRegex(files, language, options);
|
|
86
|
+
if (!this.project) {
|
|
87
|
+
await this.initialize();
|
|
48
88
|
}
|
|
49
|
-
}
|
|
50
89
|
|
|
51
|
-
async analyzeWithRegex(files, language, options = {}) {
|
|
52
90
|
const violations = [];
|
|
53
|
-
|
|
91
|
+
|
|
54
92
|
for (const filePath of files) {
|
|
55
|
-
if (options.verbose) {
|
|
56
|
-
console.log(`🔍 C029 Regex: Processing ${path.basename(filePath)}...`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
93
|
try {
|
|
60
|
-
const
|
|
61
|
-
const fileViolations = await this.analyzeFile(filePath, content, language);
|
|
94
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
62
95
|
violations.push(...fileViolations);
|
|
63
96
|
} catch (error) {
|
|
64
|
-
|
|
97
|
+
if (this.verbose) {
|
|
98
|
+
console.warn(`[C029] Error analyzing ${filePath}:`, error.message);
|
|
99
|
+
}
|
|
65
100
|
}
|
|
66
101
|
}
|
|
67
|
-
|
|
102
|
+
|
|
103
|
+
if (this.verbose) {
|
|
104
|
+
console.log(`[C029] Total violations found: ${violations.length}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
68
107
|
return violations;
|
|
69
108
|
}
|
|
70
109
|
|
|
71
|
-
async analyzeFile(filePath,
|
|
110
|
+
async analyzeFile(filePath, options = {}) {
|
|
72
111
|
const violations = [];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
112
|
+
|
|
113
|
+
// Get or add source file
|
|
114
|
+
let sourceFile = this.project.getSourceFile(filePath);
|
|
115
|
+
if (!sourceFile) {
|
|
116
|
+
sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.verbose) {
|
|
120
|
+
console.log(`[C029] Analyzing ${path.basename(filePath)}...`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Find all try statements
|
|
124
|
+
const tryStatements = sourceFile.getDescendantsOfKind(SyntaxKind.TryStatement);
|
|
125
|
+
|
|
126
|
+
if (this.verbose) {
|
|
127
|
+
console.log(`[C029] Found ${tryStatements.length} try statements`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const tryStatement of tryStatements) {
|
|
131
|
+
const catchClause = tryStatement.getCatchClause();
|
|
77
132
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (this.isCatchBlockEmpty(catchBlock.content)) {
|
|
83
|
-
violations.push({
|
|
84
|
-
file: filePath,
|
|
85
|
-
line: i + 1,
|
|
86
|
-
column: line.indexOf('catch') + 1,
|
|
87
|
-
message: 'Empty catch block detected',
|
|
88
|
-
severity: 'error',
|
|
89
|
-
ruleId: this.ruleId,
|
|
90
|
-
type: 'empty_catch'
|
|
91
|
-
});
|
|
92
|
-
}
|
|
133
|
+
if (catchClause) {
|
|
134
|
+
const catchViolations = this.analyzeCatchClause(catchClause, filePath, sourceFile);
|
|
135
|
+
violations.push(...catchViolations);
|
|
93
136
|
}
|
|
94
137
|
}
|
|
138
|
+
|
|
139
|
+
return violations;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
analyzeCatchClause(catchClause, filePath, sourceFile) {
|
|
143
|
+
const violations = [];
|
|
144
|
+
const block = catchClause.getBlock();
|
|
145
|
+
const variableDeclaration = catchClause.getVariableDeclaration();
|
|
95
146
|
|
|
147
|
+
// Get exception variable name (e, error, err, etc.)
|
|
148
|
+
const exceptionVar = variableDeclaration
|
|
149
|
+
? variableDeclaration.getName()
|
|
150
|
+
: null;
|
|
151
|
+
|
|
152
|
+
const startLine = catchClause.getStartLineNumber();
|
|
153
|
+
const startColumn = catchClause.getStart() - catchClause.getStartLinePos() + 1;
|
|
154
|
+
|
|
155
|
+
// STAGE 1: Check if catch block is empty
|
|
156
|
+
if (this.isEmptyCatchBlock(block)) {
|
|
157
|
+
violations.push(this.createViolation(
|
|
158
|
+
filePath,
|
|
159
|
+
startLine,
|
|
160
|
+
startColumn,
|
|
161
|
+
'empty_catch',
|
|
162
|
+
'Empty catch block - errors are silently ignored',
|
|
163
|
+
'Add error logging or explicit comment explaining why error is ignored',
|
|
164
|
+
0.95
|
|
165
|
+
));
|
|
166
|
+
return violations; // No need to check further
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// STAGE 2: Find logging calls in catch block
|
|
170
|
+
const loggingInfo = this.findLoggingCalls(block, exceptionVar);
|
|
171
|
+
|
|
172
|
+
if (loggingInfo.hasLogging) {
|
|
173
|
+
// STAGE 3: Validate logging quality
|
|
174
|
+
const qualityIssues = this.validateLoggingQuality(loggingInfo, exceptionVar, filePath);
|
|
175
|
+
violations.push(...qualityIssues.map(issue =>
|
|
176
|
+
this.createViolation(
|
|
177
|
+
filePath,
|
|
178
|
+
startLine,
|
|
179
|
+
startColumn,
|
|
180
|
+
issue.type,
|
|
181
|
+
issue.message,
|
|
182
|
+
issue.suggestion,
|
|
183
|
+
issue.confidence
|
|
184
|
+
)
|
|
185
|
+
));
|
|
186
|
+
} else {
|
|
187
|
+
// No logging found - check if there's valid error handling
|
|
188
|
+
const hasErrorHandling = this.hasValidErrorHandling(block, exceptionVar);
|
|
189
|
+
|
|
190
|
+
if (!hasErrorHandling && !this.isTestFile(filePath)) {
|
|
191
|
+
violations.push(this.createViolation(
|
|
192
|
+
filePath,
|
|
193
|
+
startLine,
|
|
194
|
+
startColumn,
|
|
195
|
+
'no_logging',
|
|
196
|
+
exceptionVar
|
|
197
|
+
? `Catch block does not log exception '${exceptionVar}'`
|
|
198
|
+
: 'Catch block does not log exception',
|
|
199
|
+
'Add console.error() or logger.error() with error details',
|
|
200
|
+
0.85
|
|
201
|
+
));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// STAGE 4: Check for unused exception variable
|
|
206
|
+
if (exceptionVar && !this.isExceptionUsed(block, exceptionVar) && !this.hasExplicitIgnoreComment(catchClause)) {
|
|
207
|
+
violations.push(this.createViolation(
|
|
208
|
+
filePath,
|
|
209
|
+
startLine,
|
|
210
|
+
startColumn,
|
|
211
|
+
'unused_exception',
|
|
212
|
+
`Exception variable '${exceptionVar}' is declared but never used`,
|
|
213
|
+
`Use '${exceptionVar}' in error logging or add comment explaining why it's ignored`,
|
|
214
|
+
0.80
|
|
215
|
+
));
|
|
216
|
+
}
|
|
217
|
+
|
|
96
218
|
return violations;
|
|
97
219
|
}
|
|
98
220
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Check if catch block is empty
|
|
223
|
+
*/
|
|
224
|
+
isEmptyCatchBlock(block) {
|
|
225
|
+
const statements = block.getStatements();
|
|
103
226
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
227
|
+
if (statements.length === 0) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check if only contains comments
|
|
232
|
+
const text = block.getText()
|
|
233
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
234
|
+
.replace(/\/\/.*$/gm, '') // Remove line comments
|
|
235
|
+
.replace(/\{|\}/g, '') // Remove braces
|
|
236
|
+
.trim();
|
|
237
|
+
|
|
238
|
+
return text.length === 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Find all logging calls in catch block
|
|
243
|
+
*/
|
|
244
|
+
findLoggingCalls(block, exceptionVar) {
|
|
245
|
+
const loggingInfo = {
|
|
246
|
+
hasLogging: false,
|
|
247
|
+
calls: [],
|
|
248
|
+
usesExceptionVar: false,
|
|
249
|
+
logLevels: [],
|
|
250
|
+
hasStackTrace: false,
|
|
251
|
+
hasContextData: false
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Find all call expressions
|
|
255
|
+
const callExpressions = block.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
256
|
+
|
|
257
|
+
for (const call of callExpressions) {
|
|
258
|
+
const expression = call.getExpression();
|
|
259
|
+
const expressionText = expression.getText();
|
|
260
|
+
|
|
261
|
+
// Check if it's a logging call
|
|
262
|
+
const isLogging = this.config.loggingMethods.some(method =>
|
|
263
|
+
expressionText.includes(method.split('.')[0]) &&
|
|
264
|
+
(expressionText.endsWith('.error') ||
|
|
265
|
+
expressionText.endsWith('.warn') ||
|
|
266
|
+
expressionText.endsWith('.log') ||
|
|
267
|
+
expressionText.endsWith('.info') ||
|
|
268
|
+
expressionText.endsWith('.debug') ||
|
|
269
|
+
expressionText.includes('logError') ||
|
|
270
|
+
expressionText.includes('logErrors'))
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (isLogging) {
|
|
274
|
+
loggingInfo.hasLogging = true;
|
|
275
|
+
|
|
276
|
+
// Extract log level
|
|
277
|
+
const logLevel = this.extractLogLevel(expressionText);
|
|
278
|
+
if (logLevel) {
|
|
279
|
+
loggingInfo.logLevels.push(logLevel);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check if exception variable is used in arguments
|
|
283
|
+
const args = call.getArguments();
|
|
284
|
+
const argsText = args.map(arg => arg.getText()).join(' ');
|
|
285
|
+
|
|
286
|
+
if (exceptionVar && argsText.includes(exceptionVar)) {
|
|
287
|
+
loggingInfo.usesExceptionVar = true;
|
|
288
|
+
|
|
289
|
+
// Check for stack trace
|
|
290
|
+
if (argsText.includes(`${exceptionVar}.stack`) ||
|
|
291
|
+
argsText.includes(`${exceptionVar}.message`)) {
|
|
292
|
+
loggingInfo.hasStackTrace = true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check for context data (objects, multiple arguments)
|
|
297
|
+
if (args.length > 1 || this.hasObjectLiteral(args)) {
|
|
298
|
+
loggingInfo.hasContextData = true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
loggingInfo.calls.push({
|
|
302
|
+
expression: expressionText,
|
|
303
|
+
level: logLevel,
|
|
304
|
+
arguments: argsText,
|
|
305
|
+
line: call.getStartLineNumber()
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return loggingInfo;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Extract log level from expression (error, warn, log, info, debug)
|
|
315
|
+
*/
|
|
316
|
+
extractLogLevel(expressionText) {
|
|
317
|
+
if (expressionText.includes('.error') || expressionText.includes('logError')) return 'error';
|
|
318
|
+
if (expressionText.includes('.warn')) return 'warn';
|
|
319
|
+
if (expressionText.includes('.log')) return 'log';
|
|
320
|
+
if (expressionText.includes('.info')) return 'info';
|
|
321
|
+
if (expressionText.includes('.debug')) return 'debug';
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if arguments contain object literals (context data)
|
|
327
|
+
*/
|
|
328
|
+
hasObjectLiteral(args) {
|
|
329
|
+
return args.some(arg =>
|
|
330
|
+
arg.getKind() === SyntaxKind.ObjectLiteralExpression
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Validate logging quality
|
|
336
|
+
*/
|
|
337
|
+
validateLoggingQuality(loggingInfo, exceptionVar, filePath) {
|
|
338
|
+
const issues = [];
|
|
339
|
+
|
|
340
|
+
// Issue 1: Using inappropriate log level (log/info/debug instead of error/warn)
|
|
341
|
+
const hasInappropriateLevel = loggingInfo.logLevels.some(level =>
|
|
342
|
+
this.config.inappropriateLevels.includes(level)
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
if (hasInappropriateLevel && !this.isTestFile(filePath)) {
|
|
346
|
+
issues.push({
|
|
347
|
+
type: 'inappropriate_log_level',
|
|
348
|
+
message: `Catch block uses console.${loggingInfo.logLevels.join('/')} instead of console.error or console.warn`,
|
|
349
|
+
suggestion: 'Use console.error() or console.warn() for error logging in catch blocks',
|
|
350
|
+
confidence: 0.75
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Issue 2: Exception variable not included in logging
|
|
355
|
+
if (exceptionVar && !loggingInfo.usesExceptionVar) {
|
|
356
|
+
issues.push({
|
|
357
|
+
type: 'exception_not_logged',
|
|
358
|
+
message: `Exception variable '${exceptionVar}' is not included in logging`,
|
|
359
|
+
suggestion: `Include '${exceptionVar}' in console.error() to preserve error details`,
|
|
360
|
+
confidence: 0.85
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Issue 3: No stack trace (optional but recommended)
|
|
365
|
+
if (exceptionVar && loggingInfo.usesExceptionVar && !loggingInfo.hasStackTrace) {
|
|
366
|
+
// This is a warning, not critical
|
|
367
|
+
issues.push({
|
|
368
|
+
type: 'missing_stack_trace',
|
|
369
|
+
message: `Logging does not include stack trace (${exceptionVar}.stack)`,
|
|
370
|
+
suggestion: `Consider logging ${exceptionVar}.stack for better debugging`,
|
|
371
|
+
confidence: 0.60
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Issue 4: No context data (optional but recommended)
|
|
376
|
+
if (!loggingInfo.hasContextData && !this.isTestFile(filePath)) {
|
|
377
|
+
issues.push({
|
|
378
|
+
type: 'missing_context_data',
|
|
379
|
+
message: 'Error logging lacks context information (user ID, request ID, parameters)',
|
|
380
|
+
suggestion: 'Add context object with relevant data: { userId, requestId, ...params, error }',
|
|
381
|
+
confidence: 0.55
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return issues;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Check if catch block has valid error handling
|
|
390
|
+
* Accepts: throw, return with error, error handlers, delegation to any function with exception
|
|
391
|
+
*/
|
|
392
|
+
hasValidErrorHandling(block, exceptionVar) {
|
|
393
|
+
const text = block.getText();
|
|
394
|
+
|
|
395
|
+
// Pattern 1: Re-throwing error
|
|
396
|
+
if (/\bthrow\b/.test(text)) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Pattern 2: Return statement with exception variable
|
|
401
|
+
// Accept: return error, return handleError(error), return {error}
|
|
402
|
+
// Reject: return null, return false (no error info)
|
|
403
|
+
if (exceptionVar && /\breturn\b/.test(text)) {
|
|
404
|
+
const returnStatements = block.getDescendantsOfKind(SyntaxKind.ReturnStatement);
|
|
405
|
+
for (const returnStmt of returnStatements) {
|
|
406
|
+
const returnExpr = returnStmt.getExpression();
|
|
407
|
+
if (returnExpr) {
|
|
408
|
+
const returnText = returnExpr.getText();
|
|
409
|
+
// Check if return expression includes exception variable
|
|
410
|
+
if (new RegExp(`\\b${exceptionVar}\\b`).test(returnText)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Pattern 3: Common error handler functions
|
|
418
|
+
const errorHandlerPatterns = [
|
|
419
|
+
/handleError\s*\(/,
|
|
420
|
+
/processError\s*\(/,
|
|
421
|
+
/reportError\s*\(/,
|
|
422
|
+
/trackError\s*\(/,
|
|
423
|
+
/sendError\s*\(/,
|
|
424
|
+
/errorHandler\s*\(/,
|
|
425
|
+
/externalErrorHandler\s*\(/,
|
|
426
|
+
/logError\s*\(/,
|
|
427
|
+
/captureError\s*\(/,
|
|
428
|
+
/recordError\s*\(/
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
if (errorHandlerPatterns.some(pattern => pattern.test(text))) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Pattern 4: Delegation - calling ANY function/helper with exception variable
|
|
436
|
+
// Examples: handleApiError(error), utils.logException(err), sendToSentry(e)
|
|
437
|
+
if (exceptionVar) {
|
|
438
|
+
const callExpressions = block.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
107
439
|
|
|
108
|
-
for (const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
440
|
+
for (const call of callExpressions) {
|
|
441
|
+
const args = call.getArguments();
|
|
442
|
+
|
|
443
|
+
// Check if exception variable is passed to any function
|
|
444
|
+
for (const arg of args) {
|
|
445
|
+
const argText = arg.getText().trim();
|
|
446
|
+
|
|
447
|
+
// Direct usage: someFunction(error)
|
|
448
|
+
if (argText === exceptionVar) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Property access: someFunction(error.message)
|
|
453
|
+
if (argText.startsWith(exceptionVar + '.')) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// In object/array: someFunction({error: error})
|
|
458
|
+
if (new RegExp(`\\b${exceptionVar}\\b`).test(argText)) {
|
|
459
|
+
return true;
|
|
116
460
|
}
|
|
117
461
|
}
|
|
118
462
|
}
|
|
119
463
|
}
|
|
120
|
-
|
|
121
|
-
return
|
|
464
|
+
|
|
465
|
+
return false;
|
|
122
466
|
}
|
|
123
467
|
|
|
124
|
-
|
|
125
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Check if exception variable is used anywhere in catch block
|
|
470
|
+
*/
|
|
471
|
+
isExceptionUsed(block, exceptionVar) {
|
|
472
|
+
const text = block.getText();
|
|
126
473
|
|
|
127
|
-
//
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
|
131
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
132
|
-
.trim();
|
|
474
|
+
// Simple regex to find usage of exception variable
|
|
475
|
+
const usageRegex = new RegExp(`\\b${exceptionVar}\\b`, 'g');
|
|
476
|
+
const matches = text.match(usageRegex) || [];
|
|
133
477
|
|
|
134
|
-
//
|
|
135
|
-
const
|
|
478
|
+
// Exclude the catch declaration itself
|
|
479
|
+
const catchDeclaration = new RegExp(`catch\\s*\\(\\s*${exceptionVar}\\s*\\)`, 'g');
|
|
480
|
+
const declarationMatches = text.match(catchDeclaration) || [];
|
|
481
|
+
|
|
482
|
+
return (matches.length - declarationMatches.length) > 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Check if catch clause has explicit ignore comment
|
|
487
|
+
*/
|
|
488
|
+
hasExplicitIgnoreComment(catchClause) {
|
|
489
|
+
const text = catchClause.getText();
|
|
136
490
|
|
|
137
|
-
|
|
491
|
+
const ignorePatterns = [
|
|
492
|
+
/\/\/\s*ignore/i,
|
|
493
|
+
/\/\/\s*TODO/i,
|
|
494
|
+
/\/\/\s*FIXME/i,
|
|
495
|
+
/\/\/\s*eslint-disable/i,
|
|
496
|
+
/\/\*\s*ignore/i,
|
|
497
|
+
/\/\*\s*TODO/i,
|
|
498
|
+
/\/\*\s*FIXME/i,
|
|
499
|
+
/\/\*\s*eslint-disable/i
|
|
500
|
+
];
|
|
501
|
+
|
|
502
|
+
return ignorePatterns.some(pattern => pattern.test(text));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Check if file is a test file (more lenient rules)
|
|
507
|
+
*/
|
|
508
|
+
isTestFile(filePath) {
|
|
509
|
+
return this.config.testPatterns.some(pattern => filePath.includes(pattern));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Create violation object
|
|
514
|
+
*/
|
|
515
|
+
createViolation(filePath, line, column, type, message, suggestion, confidence) {
|
|
516
|
+
return {
|
|
517
|
+
ruleId: this.ruleId,
|
|
518
|
+
file: filePath,
|
|
519
|
+
line: line,
|
|
520
|
+
column: column,
|
|
521
|
+
message: message,
|
|
522
|
+
severity: this.getSeverity(type, confidence),
|
|
523
|
+
type: type,
|
|
524
|
+
confidence: confidence,
|
|
525
|
+
suggestion: suggestion,
|
|
526
|
+
analyzer: 'ts-morph'
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Determine severity based on type and confidence
|
|
532
|
+
*/
|
|
533
|
+
getSeverity(type, confidence) {
|
|
534
|
+
// High severity violations
|
|
535
|
+
const highSeverityTypes = ['empty_catch', 'no_logging', 'unused_exception'];
|
|
536
|
+
if (highSeverityTypes.includes(type) && confidence > 0.8) {
|
|
537
|
+
return 'error';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Medium severity violations
|
|
541
|
+
const mediumSeverityTypes = ['exception_not_logged', 'inappropriate_log_level'];
|
|
542
|
+
if (mediumSeverityTypes.includes(type) && confidence > 0.7) {
|
|
543
|
+
return 'warning';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Low severity violations (recommendations)
|
|
547
|
+
return 'info';
|
|
138
548
|
}
|
|
139
549
|
}
|
|
140
550
|
|