@sun-asterisk/sunlint 1.1.7 → 1.2.0
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/.sunlint.json +1 -1
- package/CHANGELOG.md +83 -0
- package/README.md +66 -4
- package/config/presets/all.json +125 -0
- package/config/presets/beginner.json +16 -8
- package/config/presets/ci.json +12 -4
- package/config/presets/maintainability.json +38 -0
- package/config/presets/performance.json +32 -0
- package/config/presets/quality.json +103 -0
- package/config/presets/recommended.json +36 -12
- package/config/presets/security.json +88 -0
- package/config/presets/strict.json +15 -5
- package/config/rules/rules-registry-generated.json +6312 -0
- package/config/rules-summary.json +1941 -0
- package/core/adapters/sunlint-rule-adapter.js +452 -0
- package/core/analysis-orchestrator.js +4 -4
- package/core/config-manager.js +28 -5
- package/core/rule-selection-service.js +52 -55
- package/docs/CONFIGURATION.md +111 -3
- package/docs/LANGUAGE-SPECIFIC-RULES.md +308 -0
- package/docs/README.md +3 -0
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +156 -0
- package/engines/eslint-engine.js +92 -2
- package/engines/heuristic-engine.js +8 -31
- package/origin-rules/common-en.md +1320 -0
- package/origin-rules/dart-en.md +289 -0
- package/origin-rules/java-en.md +60 -0
- package/origin-rules/kotlin-mobile-en.md +453 -0
- package/origin-rules/reactjs-en.md +102 -0
- package/origin-rules/security-en.md +1055 -0
- package/origin-rules/swift-en.md +449 -0
- package/origin-rules/typescript-en.md +136 -0
- package/package.json +6 -5
- package/scripts/copy-rules.js +86 -0
- package/rules/README.md +0 -252
- package/rules/common/C002_no_duplicate_code/analyzer.js +0 -65
- package/rules/common/C002_no_duplicate_code/config.json +0 -23
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +0 -418
- package/rules/common/C003_no_vague_abbreviations/config.json +0 -35
- package/rules/common/C006_function_naming/analyzer.js +0 -349
- package/rules/common/C006_function_naming/config.json +0 -86
- package/rules/common/C010_limit_block_nesting/analyzer.js +0 -389
- package/rules/common/C013_no_dead_code/analyzer.js +0 -206
- package/rules/common/C014_dependency_injection/analyzer.js +0 -338
- package/rules/common/C017_constructor_logic/analyzer.js +0 -314
- package/rules/common/C019_log_level_usage/analyzer.js +0 -362
- package/rules/common/C019_log_level_usage/config.json +0 -121
- package/rules/common/C029_catch_block_logging/analyzer.js +0 -373
- package/rules/common/C029_catch_block_logging/config.json +0 -59
- package/rules/common/C031_validation_separation/analyzer.js +0 -186
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +0 -292
- package/rules/common/C042_boolean_name_prefix/analyzer.js +0 -300
- package/rules/common/C043_no_console_or_print/analyzer.js +0 -304
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +0 -351
- package/rules/common/C075_explicit_return_types/analyzer.js +0 -103
- package/rules/common/C076_single_test_behavior/analyzer.js +0 -121
- package/rules/docs/C002_no_duplicate_code.md +0 -57
- package/rules/docs/C031_validation_separation.md +0 -72
- package/rules/index.js +0 -149
- package/rules/migration/converter.js +0 -385
- package/rules/migration/mapping.json +0 -164
- package/rules/security/S026_json_schema_validation/analyzer.js +0 -251
- package/rules/security/S026_json_schema_validation/config.json +0 -27
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +0 -263
- package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
- package/rules/security/S029_csrf_protection/analyzer.js +0 -264
- package/rules/tests/C002_no_duplicate_code.test.js +0 -50
- package/rules/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
- package/rules/utils/ast-utils.js +0 -191
- package/rules/utils/base-analyzer.js +0 -98
- package/rules/utils/pattern-matchers.js +0 -239
- package/rules/utils/rule-helpers.js +0 -264
- package/rules/utils/severity-constants.js +0 -93
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* C010 Rule: Limit Block Nesting Depth
|
|
3
|
-
* Prevents deep nesting of blocks (if/for/while/switch/try) to improve code readability
|
|
4
|
-
* Maximum recommended depth: 3 levels
|
|
5
|
-
* Severity: warning
|
|
6
|
-
* Category: Maintainability
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
|
|
12
|
-
class C010LimitBlockNestingAnalyzer {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.ruleId = 'C010';
|
|
15
|
-
this.ruleName = 'Limit Block Nesting';
|
|
16
|
-
this.description = 'Do not exceed maximum block nesting depth for better readability';
|
|
17
|
-
this.severity = 'warning';
|
|
18
|
-
this.maxDepth = 3;
|
|
19
|
-
|
|
20
|
-
// Control flow blocks that create nesting - Match ESLint rule exactly
|
|
21
|
-
// Only: if, for, while, do-while, switch, try-catch statements
|
|
22
|
-
this.blockPatterns = [
|
|
23
|
-
// if/else patterns - handle both same-line and multi-line blocks
|
|
24
|
-
{ pattern: /^\s*if\s*\(.*\)\s*\{/, type: 'if', opens: true },
|
|
25
|
-
{ pattern: /^\s*if\s*\(.*\)\s*$/, type: 'if-pending', opens: false, needsBrace: true },
|
|
26
|
-
{ pattern: /^\s*else\s*if\s*\(.*\)\s*\{/, type: 'else-if', opens: true },
|
|
27
|
-
{ pattern: /^\s*else\s*if\s*\(.*\)\s*$/, type: 'else-if-pending', opens: false, needsBrace: true },
|
|
28
|
-
{ pattern: /^\s*else\s*\{/, type: 'else', opens: true },
|
|
29
|
-
{ pattern: /^\s*else\s*$/, type: 'else-pending', opens: false, needsBrace: true },
|
|
30
|
-
|
|
31
|
-
// Loop patterns - handle both same-line and multi-line blocks
|
|
32
|
-
{ pattern: /^\s*for\s*\(.*\)\s*\{/, type: 'for', opens: true },
|
|
33
|
-
{ pattern: /^\s*for\s*\(.*\)\s*$/, type: 'for-pending', opens: false, needsBrace: true },
|
|
34
|
-
{ pattern: /^\s*while\s*\(.*\)\s*\{/, type: 'while', opens: true },
|
|
35
|
-
{ pattern: /^\s*while\s*\(.*\)\s*$/, type: 'while-pending', opens: false, needsBrace: true },
|
|
36
|
-
{ pattern: /^\s*do\s*\{/, type: 'do-while', opens: true },
|
|
37
|
-
{ pattern: /^\s*do\s*$/, type: 'do-while-pending', opens: false, needsBrace: true },
|
|
38
|
-
|
|
39
|
-
// Switch statements (not individual case blocks)
|
|
40
|
-
{ pattern: /^\s*switch\s*\(.*\)\s*\{/, type: 'switch', opens: true },
|
|
41
|
-
{ pattern: /^\s*switch\s*\(.*\)\s*$/, type: 'switch-pending', opens: false, needsBrace: true },
|
|
42
|
-
|
|
43
|
-
// Try-catch patterns
|
|
44
|
-
{ pattern: /^\s*try\s*\{/, type: 'try', opens: true },
|
|
45
|
-
{ pattern: /^\s*try\s*$/, type: 'try-pending', opens: false, needsBrace: true },
|
|
46
|
-
{ pattern: /^\s*catch\s*\(.*\)\s*\{/, type: 'catch', opens: true },
|
|
47
|
-
{ pattern: /^\s*catch\s*\(.*\)\s*$/, type: 'catch-pending', opens: false, needsBrace: true },
|
|
48
|
-
{ pattern: /^\s*finally\s*\{/, type: 'finally', opens: true },
|
|
49
|
-
{ pattern: /^\s*finally\s*$/, type: 'finally-pending', opens: false, needsBrace: true },
|
|
50
|
-
|
|
51
|
-
// With statements (rarely used but included for completeness)
|
|
52
|
-
{ pattern: /^\s*with\s*\(.*\)\s*\{/, type: 'with', opens: true },
|
|
53
|
-
{ pattern: /^\s*with\s*\(.*\)\s*$/, type: 'with-pending', opens: false, needsBrace: true },
|
|
54
|
-
|
|
55
|
-
// Standalone opening brace (follows pending blocks)
|
|
56
|
-
{ pattern: /^\s*\{\s*$/, type: 'brace-block', opens: true }
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
// Track pending blocks that expect a brace on next line
|
|
60
|
-
this.pendingBlocks = [];
|
|
61
|
-
|
|
62
|
-
// Patterns for inline blocks (without braces)
|
|
63
|
-
this.inlineBlockPatterns = [
|
|
64
|
-
{ pattern: /^\s*if\s*\(.*\)\s*[^{]/, type: 'if-inline' },
|
|
65
|
-
{ pattern: /^\s*else\s+if\s*\(.*\)\s*[^{]/, type: 'else-if-inline' },
|
|
66
|
-
{ pattern: /^\s*else\s+[^{]/, type: 'else-inline' },
|
|
67
|
-
{ pattern: /^\s*for\s*\(.*\)\s*[^{]/, type: 'for-inline' },
|
|
68
|
-
{ pattern: /^\s*while\s*\(.*\)\s*[^{]/, type: 'while-inline' }
|
|
69
|
-
];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async analyze(files, language, config = {}) {
|
|
73
|
-
const violations = [];
|
|
74
|
-
|
|
75
|
-
if (config?.rules?.C010?.maxDepth) {
|
|
76
|
-
this.maxDepth = config.rules.C010.maxDepth;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
for (const filePath of files) {
|
|
80
|
-
try {
|
|
81
|
-
if (this.isTestFile(filePath)) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
86
|
-
const fileViolations = await this.analyzeFile(filePath, fileContent, language, config);
|
|
87
|
-
violations.push(...fileViolations);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.warn(`C010 analysis error for ${filePath}:`, error.message);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return violations;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async analyzeFile(filePath, fileContent, language, config) {
|
|
97
|
-
const violations = [];
|
|
98
|
-
|
|
99
|
-
// Reset pending blocks for each file
|
|
100
|
-
this.pendingBlocks = [];
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const lines = fileContent.split('\n');
|
|
104
|
-
let controlFlowStack = []; // Only track control flow blocks
|
|
105
|
-
let commentState = { inMultiLine: false, startChar: 0 };
|
|
106
|
-
|
|
107
|
-
for (let i = 0; i < lines.length; i++) {
|
|
108
|
-
const line = lines[i];
|
|
109
|
-
const lineNumber = i + 1;
|
|
110
|
-
|
|
111
|
-
// Update comment state and skip if in comment
|
|
112
|
-
commentState = this.updateCommentState(line, commentState);
|
|
113
|
-
if (this.isLineInComment(line, commentState)) {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const trimmedLine = line.trim();
|
|
118
|
-
if (!trimmedLine) continue;
|
|
119
|
-
|
|
120
|
-
// Track control flow statements
|
|
121
|
-
const controlFlowMatch = this.detectControlFlow(trimmedLine);
|
|
122
|
-
if (controlFlowMatch) {
|
|
123
|
-
// Check if this line has opening brace (same line)
|
|
124
|
-
if (trimmedLine.includes('{')) {
|
|
125
|
-
controlFlowStack.push({
|
|
126
|
-
type: controlFlowMatch.type,
|
|
127
|
-
line: lineNumber,
|
|
128
|
-
column: this.getBlockStartColumn(line)
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Check depth violation at the control flow statement
|
|
132
|
-
if (controlFlowStack.length > this.maxDepth) {
|
|
133
|
-
violations.push(this.createViolation(
|
|
134
|
-
filePath,
|
|
135
|
-
lineNumber,
|
|
136
|
-
this.getBlockStartColumn(line),
|
|
137
|
-
line,
|
|
138
|
-
controlFlowStack.length,
|
|
139
|
-
controlFlowStack
|
|
140
|
-
));
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
// Look ahead for opening brace on next line
|
|
144
|
-
let braceLineIndex = -1;
|
|
145
|
-
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
146
|
-
if (lines[j].trim() === '{') {
|
|
147
|
-
braceLineIndex = j;
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
if (lines[j].trim() !== '') break; // Stop if non-empty, non-brace line
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (braceLineIndex >= 0) {
|
|
154
|
-
controlFlowStack.push({
|
|
155
|
-
type: controlFlowMatch.type,
|
|
156
|
-
line: braceLineIndex + 1,
|
|
157
|
-
column: this.getBlockStartColumn(lines[braceLineIndex])
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Check depth violation at the opening brace
|
|
161
|
-
if (controlFlowStack.length > this.maxDepth) {
|
|
162
|
-
violations.push(this.createViolation(
|
|
163
|
-
filePath,
|
|
164
|
-
braceLineIndex + 1,
|
|
165
|
-
this.getBlockStartColumn(lines[braceLineIndex]),
|
|
166
|
-
lines[braceLineIndex],
|
|
167
|
-
controlFlowStack.length,
|
|
168
|
-
controlFlowStack
|
|
169
|
-
));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Handle closing braces - only remove if we have control flow blocks
|
|
176
|
-
const closeBraces = (line.match(/\}/g) || []).length;
|
|
177
|
-
for (let j = 0; j < closeBraces && controlFlowStack.length > 0; j++) {
|
|
178
|
-
controlFlowStack.pop();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
} catch (error) {
|
|
183
|
-
console.warn(`C010 analysis error for ${filePath}:`, error.message);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return violations;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
detectControlFlow(line) {
|
|
190
|
-
// Match control flow keywords that create nesting
|
|
191
|
-
const patterns = [
|
|
192
|
-
{ pattern: /^\s*if\s*\(/, type: 'if' },
|
|
193
|
-
{ pattern: /^\s*else\s+if\s*\(/, type: 'else-if' },
|
|
194
|
-
{ pattern: /^\s*else\s*$/, type: 'else' },
|
|
195
|
-
{ pattern: /^\s*for\s*\(/, type: 'for' },
|
|
196
|
-
{ pattern: /^\s*while\s*\(/, type: 'while' },
|
|
197
|
-
{ pattern: /^\s*do\s*$/, type: 'do-while' },
|
|
198
|
-
{ pattern: /^\s*switch\s*\(/, type: 'switch' },
|
|
199
|
-
{ pattern: /^\s*try\s*$/, type: 'try' },
|
|
200
|
-
{ pattern: /^\s*catch\s*\(/, type: 'catch' },
|
|
201
|
-
{ pattern: /^\s*finally\s*$/, type: 'finally' },
|
|
202
|
-
{ pattern: /^\s*with\s*\(/, type: 'with' },
|
|
203
|
-
// Handle closing brace followed by control flow
|
|
204
|
-
{ pattern: /^\s*}\s*else\s+if\s*\(/, type: 'else-if' },
|
|
205
|
-
{ pattern: /^\s*}\s*else\s*$/, type: 'else' },
|
|
206
|
-
{ pattern: /^\s*}\s*catch\s*\(/, type: 'catch' },
|
|
207
|
-
{ pattern: /^\s*}\s*finally\s*$/, type: 'finally' }
|
|
208
|
-
];
|
|
209
|
-
|
|
210
|
-
for (const pattern of patterns) {
|
|
211
|
-
if (pattern.pattern.test(line)) {
|
|
212
|
-
return { type: pattern.type };
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
updateCommentState(line, currentState) {
|
|
220
|
-
let { inMultiLine, startChar } = currentState;
|
|
221
|
-
let pos = startChar;
|
|
222
|
-
|
|
223
|
-
while (pos < line.length) {
|
|
224
|
-
if (!inMultiLine) {
|
|
225
|
-
// Check for single line comment
|
|
226
|
-
if (line.substr(pos, 2) === '//') {
|
|
227
|
-
return { inMultiLine: false, startChar: line.length };
|
|
228
|
-
}
|
|
229
|
-
// Check for multi-line comment start
|
|
230
|
-
if (line.substr(pos, 2) === '/*') {
|
|
231
|
-
inMultiLine = true;
|
|
232
|
-
pos += 2;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
// Check for multi-line comment end
|
|
237
|
-
if (line.substr(pos, 2) === '*/') {
|
|
238
|
-
inMultiLine = false;
|
|
239
|
-
pos += 2;
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
pos++;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return { inMultiLine, startChar: 0 };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
isLineInComment(line, commentState) {
|
|
250
|
-
const trimmed = line.trim();
|
|
251
|
-
|
|
252
|
-
// Single line comment
|
|
253
|
-
if (trimmed.startsWith('//')) return true;
|
|
254
|
-
|
|
255
|
-
// JSDoc style comment
|
|
256
|
-
if (trimmed.startsWith('*') && !trimmed.startsWith('*/')) return true;
|
|
257
|
-
|
|
258
|
-
// In multi-line comment
|
|
259
|
-
if (commentState.inMultiLine) {
|
|
260
|
-
// Unless the line ends the comment, it's a comment line
|
|
261
|
-
return !line.includes('*/');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
detectBlockOpening(trimmedLine, fullLine) {
|
|
268
|
-
// First check if this is a standalone opening brace that follows a pending block
|
|
269
|
-
if (trimmedLine === '{' && this.pendingBlocks.length > 0) {
|
|
270
|
-
const pendingBlock = this.pendingBlocks.pop();
|
|
271
|
-
return {
|
|
272
|
-
opens: true,
|
|
273
|
-
type: pendingBlock.type.replace('-pending', ''),
|
|
274
|
-
column: this.getBlockStartColumn(fullLine),
|
|
275
|
-
inline: false
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Check for block patterns
|
|
280
|
-
for (const blockPattern of this.blockPatterns) {
|
|
281
|
-
if (blockPattern.pattern.test(trimmedLine)) {
|
|
282
|
-
if (blockPattern.needsBrace) {
|
|
283
|
-
// This is a pending block, add to pending list
|
|
284
|
-
this.pendingBlocks.push({
|
|
285
|
-
type: blockPattern.type,
|
|
286
|
-
line: fullLine
|
|
287
|
-
});
|
|
288
|
-
return { opens: false };
|
|
289
|
-
} else {
|
|
290
|
-
return {
|
|
291
|
-
opens: blockPattern.opens,
|
|
292
|
-
type: blockPattern.type,
|
|
293
|
-
column: this.getBlockStartColumn(fullLine),
|
|
294
|
-
inline: false
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return { opens: false };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
detectInlineBlock(trimmedLine) {
|
|
304
|
-
// Skip if line ends with { or ;
|
|
305
|
-
if (trimmedLine.endsWith('{') || trimmedLine.endsWith(';')) {
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
for (const pattern of this.inlineBlockPatterns) {
|
|
310
|
-
if (pattern.pattern.test(trimmedLine)) {
|
|
311
|
-
return { type: pattern.type };
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
isClosingBrace(line) {
|
|
319
|
-
// Match closing brace, possibly followed by else/catch/finally
|
|
320
|
-
return /^\s*}\s*(else|catch|finally)?\s*(\{|$)/.test(line);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
handleClosingBrace(blockStack) {
|
|
324
|
-
if (blockStack.length > 0) {
|
|
325
|
-
// Remove the most recent block
|
|
326
|
-
blockStack.pop();
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
calculateEffectiveDepth(blockStack) {
|
|
331
|
-
// Count only non-inline blocks for depth calculation
|
|
332
|
-
return blockStack.filter(block => !block.inline).length;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
getBlockStartColumn(line) {
|
|
336
|
-
const match = line.match(/^\s*/);
|
|
337
|
-
return match ? match[0].length + 1 : 1;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
isTestFile(filePath) {
|
|
341
|
-
const testPatterns = [
|
|
342
|
-
/\.test\.(js|ts|jsx|tsx)$/,
|
|
343
|
-
/\.spec\.(js|ts|jsx|tsx)$/,
|
|
344
|
-
/\/__tests__\//,
|
|
345
|
-
/\/tests?\//,
|
|
346
|
-
/\.e2e\./,
|
|
347
|
-
/test\.config\./,
|
|
348
|
-
/jest\.config\./,
|
|
349
|
-
/vitest\.config\./,
|
|
350
|
-
/cypress\//
|
|
351
|
-
];
|
|
352
|
-
|
|
353
|
-
return testPatterns.some(pattern => pattern.test(filePath));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
createViolation(filePath, lineNumber, column, sourceLine, depth, blockStack) {
|
|
357
|
-
return {
|
|
358
|
-
ruleId: this.ruleId,
|
|
359
|
-
severity: this.severity,
|
|
360
|
-
message: `Block nesting depth ${depth} exceeds maximum of ${this.maxDepth}. Consider refactoring to reduce complexity.`,
|
|
361
|
-
filePath: filePath,
|
|
362
|
-
line: lineNumber,
|
|
363
|
-
column: column,
|
|
364
|
-
source: sourceLine.trim(),
|
|
365
|
-
suggestion: this.getSuggestion(depth),
|
|
366
|
-
nestingStack: blockStack.map(b => ({
|
|
367
|
-
type: b.type,
|
|
368
|
-
line: b.line,
|
|
369
|
-
inline: b.inline || false
|
|
370
|
-
}))
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
getSuggestion(currentDepth) {
|
|
375
|
-
const suggestions = [
|
|
376
|
-
"Extract nested logic into separate functions",
|
|
377
|
-
"Use early returns to reduce nesting",
|
|
378
|
-
"Consider using guard clauses",
|
|
379
|
-
"Break complex conditions into meaningful variables",
|
|
380
|
-
"Use strategy pattern for complex conditional logic",
|
|
381
|
-
"Consider using a state machine for complex flow control"
|
|
382
|
-
];
|
|
383
|
-
|
|
384
|
-
const index = Math.min(currentDepth - this.maxDepth - 1, suggestions.length - 1);
|
|
385
|
-
return suggestions[Math.max(0, index)];
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
module.exports = C010LimitBlockNestingAnalyzer;
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Heuristic analyzer for: C013 – Do not leave dead code
|
|
3
|
-
* Purpose: Detect unreachable code after return, throw, break, continue statements
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class C013Analyzer {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.ruleId = 'C013';
|
|
9
|
-
this.ruleName = 'No Dead Code';
|
|
10
|
-
this.description = 'Avoid unreachable code after return, throw, break, continue statements';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async analyze(files, language, options = {}) {
|
|
14
|
-
const violations = [];
|
|
15
|
-
|
|
16
|
-
for (const filePath of files) {
|
|
17
|
-
if (options.verbose) {
|
|
18
|
-
console.log(`🔍 Running C013 analysis on ${require('path').basename(filePath)}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
23
|
-
const fileViolations = this.analyzeFile(content, filePath);
|
|
24
|
-
violations.push(...fileViolations);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return violations;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
analyzeFile(content, filePath) {
|
|
34
|
-
const violations = [];
|
|
35
|
-
const lines = content.split('\n');
|
|
36
|
-
|
|
37
|
-
for (let i = 0; i < lines.length; i++) {
|
|
38
|
-
const line = lines[i].trim();
|
|
39
|
-
|
|
40
|
-
// Skip empty lines and comments
|
|
41
|
-
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Only look for return statements (not throw in catch blocks)
|
|
46
|
-
if (this.isSimpleReturn(line)) {
|
|
47
|
-
// Check if there are non-comment, non-empty lines after this return
|
|
48
|
-
// within the same function scope
|
|
49
|
-
const unreachableLines = this.findUnreachableCodeSimple(lines, i);
|
|
50
|
-
|
|
51
|
-
for (const unreachableLine of unreachableLines) {
|
|
52
|
-
violations.push({
|
|
53
|
-
file: filePath,
|
|
54
|
-
line: unreachableLine + 1,
|
|
55
|
-
column: 1,
|
|
56
|
-
message: `Unreachable code detected after return statement. Remove dead code or restructure logic.`,
|
|
57
|
-
severity: 'warning',
|
|
58
|
-
ruleId: this.ruleId
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return violations;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
isSimpleReturn(line) {
|
|
68
|
-
// Only detect simple return statements that actually complete, not multiline returns
|
|
69
|
-
const cleanLine = line.replace(/;?\s*$/, '');
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
// Complete return statements with semicolon or at end of line
|
|
73
|
-
/^return\s*;?\s*$/.test(cleanLine) ||
|
|
74
|
-
/^return\s+[^{}\[\(]+;?\s*$/.test(cleanLine) ||
|
|
75
|
-
// Single-line returns with simple values
|
|
76
|
-
/^return\s+(true|false|null|undefined|\d+|"[^"]*"|'[^']*')\s*;?\s*$/.test(cleanLine) ||
|
|
77
|
-
// Handle single-line conditional returns
|
|
78
|
-
/}\s*else\s*return\s+[^{}\[\(]+;?\s*$/.test(line) ||
|
|
79
|
-
/}\s*return\s+[^{}\[\(]+;?\s*$/.test(line)
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
findUnreachableCodeSimple(lines, terminatingLineIndex) {
|
|
84
|
-
const unreachableLines = [];
|
|
85
|
-
|
|
86
|
-
// Look for code after the return statement until we hit a closing brace or new function
|
|
87
|
-
for (let i = terminatingLineIndex + 1; i < lines.length; i++) {
|
|
88
|
-
const line = lines[i].trim();
|
|
89
|
-
|
|
90
|
-
// Skip empty lines and comments
|
|
91
|
-
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Stop if we hit a closing brace (end of function/block)
|
|
96
|
-
if (line === '}' || line === '};' || line.startsWith('} ')) {
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Stop if we hit catch/finally (these are reachable)
|
|
101
|
-
if (line.includes('catch') || line.includes('finally')) {
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Stop if we hit a new function or method definition
|
|
106
|
-
if (line.includes('function') || line.includes('=>') || line.match(/^\w+\s*\(/)) {
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// This looks like unreachable code
|
|
111
|
-
if (this.isExecutableCode(line)) {
|
|
112
|
-
unreachableLines.push(i);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return unreachableLines;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
findUnreachableCode(lines, terminatingLineIndex, content) {
|
|
120
|
-
const unreachableLines = [];
|
|
121
|
-
let braceDepth = this.getCurrentBraceDepth(lines, terminatingLineIndex, content);
|
|
122
|
-
let currentDepth = braceDepth;
|
|
123
|
-
let inTryCatchFinally = false;
|
|
124
|
-
|
|
125
|
-
// Check if we're inside a try-catch-finally context
|
|
126
|
-
const contextBefore = lines.slice(0, terminatingLineIndex).join(' ');
|
|
127
|
-
if (contextBefore.includes('try') || contextBefore.includes('catch') || contextBefore.includes('finally')) {
|
|
128
|
-
// Need more sophisticated detection of try-catch-finally blocks
|
|
129
|
-
inTryCatchFinally = true;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Look for unreachable code after the terminating statement
|
|
133
|
-
for (let i = terminatingLineIndex + 1; i < lines.length; i++) {
|
|
134
|
-
const line = lines[i].trim();
|
|
135
|
-
|
|
136
|
-
// Skip empty lines and comments
|
|
137
|
-
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Special handling for try-catch-finally: finally blocks are always reachable
|
|
142
|
-
if (line.includes('finally') || (inTryCatchFinally && line === '}')) {
|
|
143
|
-
// Don't mark finally blocks or their closing braces as unreachable
|
|
144
|
-
if (line.includes('finally')) {
|
|
145
|
-
inTryCatchFinally = false; // Reset after seeing finally
|
|
146
|
-
}
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Update brace depth
|
|
151
|
-
const openBraces = (line.match(/{/g) || []).length;
|
|
152
|
-
const closeBraces = (line.match(/}/g) || []).length;
|
|
153
|
-
currentDepth += openBraces - closeBraces;
|
|
154
|
-
|
|
155
|
-
// If we've exited the current block scope, stop looking
|
|
156
|
-
if (currentDepth < braceDepth) {
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// If we're still in the same block scope, this is potentially unreachable
|
|
161
|
-
if (currentDepth === braceDepth) {
|
|
162
|
-
// Check if this line contains executable code (not just closing braces)
|
|
163
|
-
if (this.isExecutableCode(line)) {
|
|
164
|
-
unreachableLines.push(i);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return unreachableLines;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
getCurrentBraceDepth(lines, lineIndex, content) {
|
|
173
|
-
// Calculate the brace depth at the given line
|
|
174
|
-
let depth = 0;
|
|
175
|
-
const textUpToLine = lines.slice(0, lineIndex + 1).join('\n');
|
|
176
|
-
|
|
177
|
-
for (let char of textUpToLine) {
|
|
178
|
-
if (char === '{') depth++;
|
|
179
|
-
if (char === '}') depth--;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return depth;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
isExecutableCode(line) {
|
|
186
|
-
// Exclude lines that are just structural (closing braces, etc.)
|
|
187
|
-
if (line === '}' || line === '};' || line === '},' || line.match(/^\s*}\s*$/)) {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Exclude catch/finally blocks (they are reachable)
|
|
192
|
-
if (line.includes('catch') || line.includes('finally')) {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Exclude case/default statements (they might be reachable)
|
|
197
|
-
if (line.startsWith('case ') || line.startsWith('default:')) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// This looks like executable code
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
module.exports = C013Analyzer;
|