@sun-asterisk/sunlint 1.3.23 → 1.3.25
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/rules/enhanced-rules-registry.json +32 -0
- package/core/github-annotate-service.js +1 -4
- package/package.json +1 -1
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +40 -11
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +104 -28
- package/rules/common/C019_log_level_usage/analyzer.js +30 -27
- package/rules/common/C019_log_level_usage/config.json +4 -2
- package/rules/common/C019_log_level_usage/ts-morph-analyzer.js +274 -0
- package/rules/common/C020_unused_imports/analyzer.js +88 -0
- package/rules/common/C020_unused_imports/config.json +64 -0
- package/rules/common/C020_unused_imports/ts-morph-analyzer.js +358 -0
- package/rules/common/C021_import_organization/analyzer.js +88 -0
- package/rules/common/C021_import_organization/config.json +77 -0
- package/rules/common/C021_import_organization/ts-morph-analyzer.js +373 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +106 -31
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +377 -87
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C021 ts-morph Analyzer - Import Organization
|
|
3
|
+
*
|
|
4
|
+
* Enforces organized imports with proper grouping and sorting:
|
|
5
|
+
* 1. Built-in modules (fs, path, etc.)
|
|
6
|
+
* 2. External dependencies (express, axios, etc.)
|
|
7
|
+
* 3. Internal modules (./utils, ../services, etc.)
|
|
8
|
+
*
|
|
9
|
+
* Within each group, imports should be sorted alphabetically.
|
|
10
|
+
* Blank lines should separate groups.
|
|
11
|
+
*
|
|
12
|
+
* Following Rule C005: Single responsibility - import organization only
|
|
13
|
+
* Following Rule C006: Verb-noun naming
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { Project, SyntaxKind, Node } = require('ts-morph');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
class C021TsMorphAnalyzer {
|
|
21
|
+
constructor(semanticEngine = null, options = {}) {
|
|
22
|
+
this.ruleId = 'C021';
|
|
23
|
+
this.ruleName = 'Import Organization';
|
|
24
|
+
this.description = 'Enforce organized imports with grouping and sorting';
|
|
25
|
+
this.semanticEngine = semanticEngine;
|
|
26
|
+
this.project = null;
|
|
27
|
+
this.verbose = false;
|
|
28
|
+
|
|
29
|
+
// Load config
|
|
30
|
+
this.config = this.loadConfig();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
loadConfig() {
|
|
34
|
+
try {
|
|
35
|
+
const configPath = path.join(__dirname, 'config.json');
|
|
36
|
+
const configData = fs.readFileSync(configPath, 'utf8');
|
|
37
|
+
return JSON.parse(configData).config;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn('[C021] Could not load config, using defaults');
|
|
40
|
+
return {
|
|
41
|
+
groups: [
|
|
42
|
+
{
|
|
43
|
+
name: 'builtin',
|
|
44
|
+
patterns: ['^(fs|path|http|https|crypto)$']
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'external',
|
|
48
|
+
patterns: ['^[^.]']
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'internal',
|
|
52
|
+
patterns: ['^\\\\.|^@/|^~/']
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
sortOrder: {
|
|
56
|
+
groupOrder: ['builtin', 'external', 'internal'],
|
|
57
|
+
withinGroup: 'alphabetical'
|
|
58
|
+
},
|
|
59
|
+
spacing: {
|
|
60
|
+
requireBlankLineBetweenGroups: true
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async initialize(semanticEngine = null) {
|
|
67
|
+
if (semanticEngine) {
|
|
68
|
+
this.semanticEngine = semanticEngine;
|
|
69
|
+
}
|
|
70
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
71
|
+
|
|
72
|
+
// Use semantic engine's project if available
|
|
73
|
+
if (this.semanticEngine?.project) {
|
|
74
|
+
this.project = this.semanticEngine.project;
|
|
75
|
+
if (this.verbose) {
|
|
76
|
+
console.log('[DEBUG] 🎯 C021: Using semantic engine project');
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
this.project = new Project({
|
|
80
|
+
compilerOptions: {
|
|
81
|
+
target: 99,
|
|
82
|
+
module: 99,
|
|
83
|
+
allowJs: true,
|
|
84
|
+
checkJs: false,
|
|
85
|
+
jsx: 2,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (this.verbose) {
|
|
89
|
+
console.log('[DEBUG] 🎯 C021: Created standalone ts-morph project');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async analyze(files, language, options = {}) {
|
|
95
|
+
this.verbose = options.verbose || this.verbose;
|
|
96
|
+
|
|
97
|
+
if (!this.project) {
|
|
98
|
+
await this.initialize();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const violations = [];
|
|
102
|
+
|
|
103
|
+
for (const filePath of files) {
|
|
104
|
+
try {
|
|
105
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
106
|
+
violations.push(...fileViolations);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (this.verbose) {
|
|
109
|
+
console.warn(`[C021] Error analyzing ${filePath}:`, error.message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return violations;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async analyzeFile(filePath, options = {}) {
|
|
118
|
+
// Get or add source file
|
|
119
|
+
let sourceFile = this.project.getSourceFile(filePath);
|
|
120
|
+
|
|
121
|
+
if (!sourceFile && fs.existsSync(filePath)) {
|
|
122
|
+
sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!sourceFile) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const violations = [];
|
|
130
|
+
|
|
131
|
+
// Get all import declarations
|
|
132
|
+
const imports = sourceFile.getImportDeclarations();
|
|
133
|
+
|
|
134
|
+
if (imports.length === 0) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Organize imports into groups
|
|
139
|
+
const importGroups = this.organizeImports(imports);
|
|
140
|
+
|
|
141
|
+
// Check group order
|
|
142
|
+
const orderViolations = this.checkGroupOrder(importGroups, imports);
|
|
143
|
+
violations.push(...orderViolations);
|
|
144
|
+
|
|
145
|
+
// Check sorting within groups
|
|
146
|
+
const sortViolations = this.checkGroupSorting(importGroups);
|
|
147
|
+
violations.push(...sortViolations);
|
|
148
|
+
|
|
149
|
+
// Check spacing between groups
|
|
150
|
+
const spacingViolations = this.checkGroupSpacing(importGroups, sourceFile);
|
|
151
|
+
violations.push(...spacingViolations);
|
|
152
|
+
|
|
153
|
+
return violations.map(v => ({
|
|
154
|
+
...v,
|
|
155
|
+
filePath: sourceFile.getFilePath()
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Organize imports into groups based on config
|
|
161
|
+
*/
|
|
162
|
+
organizeImports(imports) {
|
|
163
|
+
const groups = {};
|
|
164
|
+
|
|
165
|
+
// Initialize groups
|
|
166
|
+
for (const groupConfig of this.config.groups) {
|
|
167
|
+
groups[groupConfig.name] = [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Categorize each import
|
|
171
|
+
for (const importDecl of imports) {
|
|
172
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
173
|
+
const groupName = this.categorizeImport(moduleSpecifier);
|
|
174
|
+
|
|
175
|
+
if (groupName && groups[groupName]) {
|
|
176
|
+
groups[groupName].push({
|
|
177
|
+
node: importDecl,
|
|
178
|
+
moduleSpecifier,
|
|
179
|
+
line: importDecl.getStartLineNumber()
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return groups;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Categorize an import into a group
|
|
189
|
+
*/
|
|
190
|
+
categorizeImport(moduleSpecifier) {
|
|
191
|
+
for (const groupConfig of this.config.groups) {
|
|
192
|
+
for (const pattern of groupConfig.patterns) {
|
|
193
|
+
const regex = new RegExp(pattern);
|
|
194
|
+
if (regex.test(moduleSpecifier)) {
|
|
195
|
+
return groupConfig.name;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check if groups appear in correct order
|
|
204
|
+
*/
|
|
205
|
+
checkGroupOrder(importGroups, allImports) {
|
|
206
|
+
const violations = [];
|
|
207
|
+
const expectedOrder = this.config.sortOrder.groupOrder;
|
|
208
|
+
|
|
209
|
+
// Get actual order of groups
|
|
210
|
+
const actualOrder = [];
|
|
211
|
+
const groupFirstLines = new Map();
|
|
212
|
+
|
|
213
|
+
for (const importInfo of allImports.map(imp => ({
|
|
214
|
+
node: imp,
|
|
215
|
+
moduleSpecifier: imp.getModuleSpecifierValue(),
|
|
216
|
+
line: imp.getStartLineNumber()
|
|
217
|
+
}))) {
|
|
218
|
+
const groupName = this.categorizeImport(importInfo.moduleSpecifier);
|
|
219
|
+
if (groupName && !groupFirstLines.has(groupName)) {
|
|
220
|
+
groupFirstLines.set(groupName, importInfo.line);
|
|
221
|
+
actualOrder.push(groupName);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check if actual order matches expected order
|
|
226
|
+
const expectedFiltered = expectedOrder.filter(g => actualOrder.includes(g));
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < actualOrder.length; i++) {
|
|
229
|
+
if (actualOrder[i] !== expectedFiltered[i]) {
|
|
230
|
+
// Find the misplaced import
|
|
231
|
+
const misplacedGroup = actualOrder[i];
|
|
232
|
+
const imports = importGroups[misplacedGroup];
|
|
233
|
+
|
|
234
|
+
if (imports && imports.length > 0) {
|
|
235
|
+
violations.push({
|
|
236
|
+
ruleId: this.ruleId,
|
|
237
|
+
message: `Import group "${misplacedGroup}" should come ${this.getOrderMessage(misplacedGroup, expectedFiltered, i)}`,
|
|
238
|
+
severity: 'info',
|
|
239
|
+
location: {
|
|
240
|
+
start: {
|
|
241
|
+
line: imports[0].line,
|
|
242
|
+
column: 1
|
|
243
|
+
},
|
|
244
|
+
end: {
|
|
245
|
+
line: imports[0].line,
|
|
246
|
+
column: 1
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
context: {
|
|
250
|
+
violationType: 'group-order',
|
|
251
|
+
currentGroup: misplacedGroup,
|
|
252
|
+
expectedOrder: expectedFiltered
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return violations;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get descriptive message for group order violation
|
|
265
|
+
*/
|
|
266
|
+
getOrderMessage(groupName, expectedOrder, currentIndex) {
|
|
267
|
+
const expectedIndex = expectedOrder.indexOf(groupName);
|
|
268
|
+
if (expectedIndex < currentIndex) {
|
|
269
|
+
return `before "${expectedOrder[currentIndex]}" imports`;
|
|
270
|
+
} else {
|
|
271
|
+
return `after "${expectedOrder[currentIndex - 1]}" imports`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check sorting within each group
|
|
277
|
+
*/
|
|
278
|
+
checkGroupSorting(importGroups) {
|
|
279
|
+
const violations = [];
|
|
280
|
+
|
|
281
|
+
for (const [groupName, imports] of Object.entries(importGroups)) {
|
|
282
|
+
if (imports.length <= 1) continue;
|
|
283
|
+
|
|
284
|
+
// Check if sorted alphabetically
|
|
285
|
+
for (let i = 0; i < imports.length - 1; i++) {
|
|
286
|
+
const current = imports[i].moduleSpecifier;
|
|
287
|
+
const next = imports[i + 1].moduleSpecifier;
|
|
288
|
+
|
|
289
|
+
if (current.toLowerCase() > next.toLowerCase()) {
|
|
290
|
+
violations.push({
|
|
291
|
+
ruleId: this.ruleId,
|
|
292
|
+
message: `Import "${current}" should come after "${next}" (alphabetical order within ${groupName} group)`,
|
|
293
|
+
severity: 'info',
|
|
294
|
+
location: {
|
|
295
|
+
start: {
|
|
296
|
+
line: imports[i].line,
|
|
297
|
+
column: 1
|
|
298
|
+
},
|
|
299
|
+
end: {
|
|
300
|
+
line: imports[i].line,
|
|
301
|
+
column: 1
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
context: {
|
|
305
|
+
violationType: 'sorting',
|
|
306
|
+
group: groupName,
|
|
307
|
+
currentImport: current,
|
|
308
|
+
nextImport: next
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return violations;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check spacing between groups
|
|
320
|
+
*/
|
|
321
|
+
checkGroupSpacing(importGroups, sourceFile) {
|
|
322
|
+
const violations = [];
|
|
323
|
+
|
|
324
|
+
if (!this.config.spacing.requireBlankLineBetweenGroups) {
|
|
325
|
+
return violations;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const expectedOrder = this.config.sortOrder.groupOrder;
|
|
329
|
+
const nonEmptyGroups = expectedOrder.filter(groupName =>
|
|
330
|
+
importGroups[groupName] && importGroups[groupName].length > 0
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Check spacing between consecutive groups
|
|
334
|
+
for (let i = 0; i < nonEmptyGroups.length - 1; i++) {
|
|
335
|
+
const currentGroup = importGroups[nonEmptyGroups[i]];
|
|
336
|
+
const nextGroup = importGroups[nonEmptyGroups[i + 1]];
|
|
337
|
+
|
|
338
|
+
const lastImportOfCurrentGroup = currentGroup[currentGroup.length - 1];
|
|
339
|
+
const firstImportOfNextGroup = nextGroup[0];
|
|
340
|
+
|
|
341
|
+
const lastLine = lastImportOfCurrentGroup.line;
|
|
342
|
+
const nextLine = firstImportOfNextGroup.line;
|
|
343
|
+
|
|
344
|
+
// Should have at least 1 blank line between groups
|
|
345
|
+
if (nextLine - lastLine < 2) {
|
|
346
|
+
violations.push({
|
|
347
|
+
ruleId: this.ruleId,
|
|
348
|
+
message: `Missing blank line between "${nonEmptyGroups[i]}" and "${nonEmptyGroups[i + 1]}" import groups`,
|
|
349
|
+
severity: 'info',
|
|
350
|
+
location: {
|
|
351
|
+
start: {
|
|
352
|
+
line: lastLine,
|
|
353
|
+
column: 1
|
|
354
|
+
},
|
|
355
|
+
end: {
|
|
356
|
+
line: nextLine,
|
|
357
|
+
column: 1
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
context: {
|
|
361
|
+
violationType: 'spacing',
|
|
362
|
+
currentGroup: nonEmptyGroups[i],
|
|
363
|
+
nextGroup: nonEmptyGroups[i + 1]
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return violations;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
module.exports = C021TsMorphAnalyzer;
|
|
@@ -49,7 +49,7 @@ class C029Analyzer {
|
|
|
49
49
|
],
|
|
50
50
|
|
|
51
51
|
// Test file patterns (more lenient checking)
|
|
52
|
-
testPatterns: ['__tests__', '.test.', '.spec.', '/test/', '/tests/', '.stories.']
|
|
52
|
+
testPatterns: ['__tests__', '.test.', '.spec.', '/test/', '/tests/', '.stories.', '-test.', '-spec.', 'test-fixtures']
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -154,6 +154,35 @@ class C029Analyzer {
|
|
|
154
154
|
|
|
155
155
|
// STAGE 1: Check if catch block is empty
|
|
156
156
|
if (this.isEmptyCatchBlock(block)) {
|
|
157
|
+
// Allow empty catch in test files if there's any comment explaining it
|
|
158
|
+
const hasComment = this.hasAnyComment(catchClause);
|
|
159
|
+
if (this.isTestFile(filePath) && hasComment) {
|
|
160
|
+
// Test files with explanatory comments are OK
|
|
161
|
+
return violations;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Allow empty catch if there's a finally block with cleanup
|
|
165
|
+
// Common pattern: try { } catch { } finally { setLoading(false); }
|
|
166
|
+
const tryStatement = catchClause.getParent();
|
|
167
|
+
if (tryStatement && tryStatement.getFinallyBlock()) {
|
|
168
|
+
const finallyBlock = tryStatement.getFinallyBlock();
|
|
169
|
+
const finallyText = finallyBlock.getText();
|
|
170
|
+
|
|
171
|
+
// Check if finally block does cleanup (setState, setLoading, cleanup, etc.)
|
|
172
|
+
const cleanupPatterns = [
|
|
173
|
+
/set[A-Z]\w*\s*\(/, // setLoading, setState, setError, etc.
|
|
174
|
+
/cleanup/i, // cleanup function
|
|
175
|
+
/reset/i, // reset function
|
|
176
|
+
/\.close\s*\(/, // close connections
|
|
177
|
+
/\.disconnect\s*\(/, // disconnect
|
|
178
|
+
/finally/i // any finally-related code
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
if (cleanupPatterns.some(pattern => pattern.test(finallyText))) {
|
|
182
|
+
return violations; // Acceptable: empty catch with finally cleanup
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
157
186
|
violations.push(this.createViolation(
|
|
158
187
|
filePath,
|
|
159
188
|
startLine,
|
|
@@ -338,7 +367,7 @@ class C029Analyzer {
|
|
|
338
367
|
const issues = [];
|
|
339
368
|
|
|
340
369
|
// Issue 1: Using inappropriate log level (log/info/debug instead of error/warn)
|
|
341
|
-
const hasInappropriateLevel = loggingInfo.logLevels.some(level =>
|
|
370
|
+
const hasInappropriateLevel = loggingInfo.logLevels.some(level =>
|
|
342
371
|
this.config.inappropriateLevels.includes(level)
|
|
343
372
|
);
|
|
344
373
|
|
|
@@ -361,26 +390,9 @@ class C029Analyzer {
|
|
|
361
390
|
});
|
|
362
391
|
}
|
|
363
392
|
|
|
364
|
-
// Issue 3
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
}
|
|
393
|
+
// REMOVED: Issue 3 & 4 (missing stack trace and context data)
|
|
394
|
+
// These are optional best practices, not violations. They created too much noise.
|
|
395
|
+
// Teams can enable them separately if desired through configuration.
|
|
384
396
|
|
|
385
397
|
return issues;
|
|
386
398
|
}
|
|
@@ -414,7 +426,20 @@ class C029Analyzer {
|
|
|
414
426
|
}
|
|
415
427
|
}
|
|
416
428
|
|
|
417
|
-
// Pattern
|
|
429
|
+
// Pattern 2b: Return default value (defensive programming)
|
|
430
|
+
// Common in frontend: catch { return []; } or catch { return null; }
|
|
431
|
+
// This is acceptable - returning safe default instead of crashing
|
|
432
|
+
if (/\breturn\b/.test(text)) {
|
|
433
|
+
const returnStatements = block.getDescendantsOfKind(SyntaxKind.ReturnStatement);
|
|
434
|
+
const statements = block.getStatements();
|
|
435
|
+
|
|
436
|
+
// If catch block ONLY contains a return statement, it's defensive programming
|
|
437
|
+
if (returnStatements.length === 1 && statements.length === 1) {
|
|
438
|
+
return true; // Acceptable pattern: return default value
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Pattern 3: Common error handler functions and external error trackers
|
|
418
443
|
const errorHandlerPatterns = [
|
|
419
444
|
/handleError\s*\(/,
|
|
420
445
|
/processError\s*\(/,
|
|
@@ -425,35 +450,72 @@ class C029Analyzer {
|
|
|
425
450
|
/externalErrorHandler\s*\(/,
|
|
426
451
|
/logError\s*\(/,
|
|
427
452
|
/captureError\s*\(/,
|
|
428
|
-
/recordError\s*\(
|
|
453
|
+
/recordError\s*\(/,
|
|
454
|
+
// External error tracking services
|
|
455
|
+
/Sentry\.captureException\s*\(/,
|
|
456
|
+
/Bugsnag\.notify\s*\(/,
|
|
457
|
+
/Rollbar\.error\s*\(/,
|
|
458
|
+
/Airbrake\.notify\s*\(/,
|
|
459
|
+
/Raygun\.send\s*\(/,
|
|
460
|
+
// Common utility patterns
|
|
461
|
+
/sendToSentry\s*\(/,
|
|
462
|
+
/utils?\.log/i,
|
|
463
|
+
/helpers?\.log/i,
|
|
464
|
+
/ErrorUtils/,
|
|
465
|
+
/ErrorService/
|
|
429
466
|
];
|
|
430
467
|
|
|
431
468
|
if (errorHandlerPatterns.some(pattern => pattern.test(text))) {
|
|
432
469
|
return true;
|
|
433
470
|
}
|
|
434
471
|
|
|
435
|
-
// Pattern 4:
|
|
472
|
+
// Pattern 4: React/Vue state management (error stored in state)
|
|
473
|
+
// Examples: setError(error), dispatch(setError(error)), setState({error})
|
|
474
|
+
if (exceptionVar) {
|
|
475
|
+
const stateManagementPatterns = [
|
|
476
|
+
/set[A-Z]\w*\s*\(/, // setState, setError, setLoading
|
|
477
|
+
/dispatch\s*\(/, // Redux dispatch
|
|
478
|
+
/commit\s*\(/, // Vuex commit
|
|
479
|
+
/this\.setState\s*\(/, // Class component setState
|
|
480
|
+
/useState/, // React hooks
|
|
481
|
+
/useReducer/ // React reducer hook
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
if (stateManagementPatterns.some(pattern => pattern.test(text))) {
|
|
485
|
+
// Check if error is passed to state function
|
|
486
|
+
const callExpressions = block.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
487
|
+
for (const call of callExpressions) {
|
|
488
|
+
const args = call.getArguments();
|
|
489
|
+
const argsText = args.map(arg => arg.getText()).join(' ');
|
|
490
|
+
if (new RegExp(`\\b${exceptionVar}\\b`).test(argsText)) {
|
|
491
|
+
return true; // Error stored in state
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Pattern 5: Delegation - calling ANY function/helper with exception variable
|
|
436
498
|
// Examples: handleApiError(error), utils.logException(err), sendToSentry(e)
|
|
437
499
|
if (exceptionVar) {
|
|
438
500
|
const callExpressions = block.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
439
|
-
|
|
501
|
+
|
|
440
502
|
for (const call of callExpressions) {
|
|
441
503
|
const args = call.getArguments();
|
|
442
|
-
|
|
504
|
+
|
|
443
505
|
// Check if exception variable is passed to any function
|
|
444
506
|
for (const arg of args) {
|
|
445
507
|
const argText = arg.getText().trim();
|
|
446
|
-
|
|
508
|
+
|
|
447
509
|
// Direct usage: someFunction(error)
|
|
448
510
|
if (argText === exceptionVar) {
|
|
449
511
|
return true;
|
|
450
512
|
}
|
|
451
|
-
|
|
513
|
+
|
|
452
514
|
// Property access: someFunction(error.message)
|
|
453
515
|
if (argText.startsWith(exceptionVar + '.')) {
|
|
454
516
|
return true;
|
|
455
517
|
}
|
|
456
|
-
|
|
518
|
+
|
|
457
519
|
// In object/array: someFunction({error: error})
|
|
458
520
|
if (new RegExp(`\\b${exceptionVar}\\b`).test(argText)) {
|
|
459
521
|
return true;
|
|
@@ -482,12 +544,25 @@ class C029Analyzer {
|
|
|
482
544
|
return (matches.length - declarationMatches.length) > 0;
|
|
483
545
|
}
|
|
484
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Check if catch clause has any comment (for test files)
|
|
549
|
+
*/
|
|
550
|
+
hasAnyComment(catchClause) {
|
|
551
|
+
const text = catchClause.getText();
|
|
552
|
+
|
|
553
|
+
// Check for any line or block comments
|
|
554
|
+
const hasLineComment = /\/\/.*\S/.test(text);
|
|
555
|
+
const hasBlockComment = /\/\*[\s\S]*?\*\//.test(text);
|
|
556
|
+
|
|
557
|
+
return hasLineComment || hasBlockComment;
|
|
558
|
+
}
|
|
559
|
+
|
|
485
560
|
/**
|
|
486
561
|
* Check if catch clause has explicit ignore comment
|
|
487
562
|
*/
|
|
488
563
|
hasExplicitIgnoreComment(catchClause) {
|
|
489
564
|
const text = catchClause.getText();
|
|
490
|
-
|
|
565
|
+
|
|
491
566
|
const ignorePatterns = [
|
|
492
567
|
/\/\/\s*ignore/i,
|
|
493
568
|
/\/\/\s*TODO/i,
|