@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,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regex-based analyzer for C010 - Block Nesting Detection
|
|
3
|
+
* Purpose: Fallback analyzer when AST analysis is not available
|
|
4
|
+
* Note: Less accurate than symbol-based approach but more performant
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { CommentDetector } = require('../../utils/rule-helpers');
|
|
10
|
+
|
|
11
|
+
class C010RegexBasedAnalyzer {
|
|
12
|
+
constructor(semanticEngine = null) {
|
|
13
|
+
this.ruleId = 'C010';
|
|
14
|
+
this.ruleName = 'Limit Block Nesting (Regex-Based)';
|
|
15
|
+
this.description = 'Do not exceed maximum block nesting depth for better readability';
|
|
16
|
+
this.severity = 'warning';
|
|
17
|
+
this.maxDepth = 3;
|
|
18
|
+
this.semanticEngine = semanticEngine;
|
|
19
|
+
this.verbose = false;
|
|
20
|
+
|
|
21
|
+
// Control flow blocks that create nesting - Match ESLint rule exactly
|
|
22
|
+
// Only: if, for, while, do-while, switch, try-catch statements
|
|
23
|
+
this.blockPatterns = [
|
|
24
|
+
// if/else patterns - handle both same-line and multi-line blocks
|
|
25
|
+
{ pattern: /^\s*if\s*\(.*\)\s*\{/, type: 'if', opens: true },
|
|
26
|
+
{ pattern: /^\s*if\s*\(.*\)\s*$/, type: 'if-pending', opens: false, needsBrace: true },
|
|
27
|
+
{ pattern: /^\s*else\s*if\s*\(.*\)\s*\{/, type: 'else-if', opens: true },
|
|
28
|
+
{ pattern: /^\s*else\s*if\s*\(.*\)\s*$/, type: 'else-if-pending', opens: false, needsBrace: true },
|
|
29
|
+
{ pattern: /^\s*else\s*\{/, type: 'else', opens: true },
|
|
30
|
+
{ pattern: /^\s*else\s*$/, type: 'else-pending', opens: false, needsBrace: true },
|
|
31
|
+
|
|
32
|
+
// Loop patterns - handle both same-line and multi-line blocks
|
|
33
|
+
{ pattern: /^\s*for\s*\(.*\)\s*\{/, type: 'for', opens: true },
|
|
34
|
+
{ pattern: /^\s*for\s*\(.*\)\s*$/, type: 'for-pending', opens: false, needsBrace: true },
|
|
35
|
+
{ pattern: /^\s*while\s*\(.*\)\s*\{/, type: 'while', opens: true },
|
|
36
|
+
{ pattern: /^\s*while\s*\(.*\)\s*$/, type: 'while-pending', opens: false, needsBrace: true },
|
|
37
|
+
{ pattern: /^\s*do\s*\{/, type: 'do-while', opens: true },
|
|
38
|
+
{ pattern: /^\s*do\s*$/, type: 'do-while-pending', opens: false, needsBrace: true },
|
|
39
|
+
|
|
40
|
+
// Switch statements (not individual case blocks)
|
|
41
|
+
{ pattern: /^\s*switch\s*\(.*\)\s*\{/, type: 'switch', opens: true },
|
|
42
|
+
{ pattern: /^\s*switch\s*\(.*\)\s*$/, type: 'switch-pending', opens: false, needsBrace: true },
|
|
43
|
+
|
|
44
|
+
// Try-catch patterns
|
|
45
|
+
{ pattern: /^\s*try\s*\{/, type: 'try', opens: true },
|
|
46
|
+
{ pattern: /^\s*try\s*$/, type: 'try-pending', opens: false, needsBrace: true },
|
|
47
|
+
{ pattern: /^\s*catch\s*\(.*\)\s*\{/, type: 'catch', opens: true },
|
|
48
|
+
{ pattern: /^\s*catch\s*\(.*\)\s*$/, type: 'catch-pending', opens: false, needsBrace: true },
|
|
49
|
+
{ pattern: /^\s*finally\s*\{/, type: 'finally', opens: true },
|
|
50
|
+
{ pattern: /^\s*finally\s*$/, type: 'finally-pending', opens: false, needsBrace: true },
|
|
51
|
+
|
|
52
|
+
// With statements (rarely used but included for completeness)
|
|
53
|
+
{ pattern: /^\s*with\s*\(.*\)\s*\{/, type: 'with', opens: true },
|
|
54
|
+
{ pattern: /^\s*with\s*\(.*\)\s*$/, type: 'with-pending', opens: false, needsBrace: true },
|
|
55
|
+
|
|
56
|
+
// Standalone opening brace (follows pending blocks)
|
|
57
|
+
{ pattern: /^\s*\{\s*$/, type: 'brace-block', opens: true }
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Track pending blocks that expect a brace on next line
|
|
61
|
+
this.pendingBlocks = [];
|
|
62
|
+
|
|
63
|
+
// Patterns for inline blocks (without braces)
|
|
64
|
+
this.inlineBlockPatterns = [
|
|
65
|
+
{ pattern: /^\s*if\s*\(.*\)\s*[^{]/, type: 'if-inline' },
|
|
66
|
+
{ pattern: /^\s*else\s+if\s*\(.*\)\s*[^{]/, type: 'else-if-inline' },
|
|
67
|
+
{ pattern: /^\s*else\s+[^{]/, type: 'else-inline' },
|
|
68
|
+
{ pattern: /^\s*for\s*\(.*\)\s*[^{]/, type: 'for-inline' },
|
|
69
|
+
{ pattern: /^\s*while\s*\(.*\)\s*[^{]/, type: 'while-inline' }
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async initialize(semanticEngine = null) {
|
|
74
|
+
if (semanticEngine) {
|
|
75
|
+
this.semanticEngine = semanticEngine;
|
|
76
|
+
}
|
|
77
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
78
|
+
|
|
79
|
+
if (this.verbose) {
|
|
80
|
+
console.log(`[DEBUG] 🔧 C010 Regex-Based: Analyzer initialized with max depth: ${this.maxDepth}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
85
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
86
|
+
console.log(`🔧 [C010 Regex] analyzeFileBasic() called for: ${filePath}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
if (this.isTestFile(filePath)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
95
|
+
const violations = await this.analyzeFile(filePath, fileContent, 'typescript', options);
|
|
96
|
+
|
|
97
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
98
|
+
console.log(`🔧 [C010 Regex] Found ${violations.length} violations in ${filePath}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return violations;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn(`C010 regex analysis error for ${filePath}:`, error.message);
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async analyze(files, language, config = {}) {
|
|
109
|
+
const violations = [];
|
|
110
|
+
|
|
111
|
+
if (config?.rules?.C010?.maxDepth) {
|
|
112
|
+
this.maxDepth = config.rules.C010.maxDepth;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const filePath of files) {
|
|
116
|
+
try {
|
|
117
|
+
if (this.isTestFile(filePath)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
122
|
+
const fileViolations = await this.analyzeFile(filePath, fileContent, language, config);
|
|
123
|
+
violations.push(...fileViolations);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn(`C010 analysis error for ${filePath}:`, error.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return violations;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async analyzeFile(filePath, fileContent, language, config) {
|
|
133
|
+
const violations = [];
|
|
134
|
+
|
|
135
|
+
// Reset pending blocks for each file
|
|
136
|
+
this.pendingBlocks = [];
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const lines = fileContent.split('\n');
|
|
140
|
+
let controlFlowStack = []; // Only track control flow blocks
|
|
141
|
+
|
|
142
|
+
// Use CommentDetector to filter comment lines
|
|
143
|
+
const filteredLines = CommentDetector.filterCommentLines(lines);
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < filteredLines.length; i++) {
|
|
146
|
+
const { line, lineNumber, isComment } = filteredLines[i];
|
|
147
|
+
|
|
148
|
+
// Skip comment lines
|
|
149
|
+
if (isComment) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const trimmedLine = line.trim();
|
|
154
|
+
if (!trimmedLine) continue;
|
|
155
|
+
|
|
156
|
+
// Track control flow statements
|
|
157
|
+
const controlFlowMatch = this.detectControlFlow(trimmedLine);
|
|
158
|
+
if (controlFlowMatch) {
|
|
159
|
+
// Check if this line has opening brace (same line)
|
|
160
|
+
if (trimmedLine.includes('{')) {
|
|
161
|
+
controlFlowStack.push({
|
|
162
|
+
type: controlFlowMatch.type,
|
|
163
|
+
line: lineNumber,
|
|
164
|
+
column: this.getBlockStartColumn(line)
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Check depth violation at the control flow statement
|
|
168
|
+
if (controlFlowStack.length > this.maxDepth) {
|
|
169
|
+
violations.push(this.createViolation(
|
|
170
|
+
filePath,
|
|
171
|
+
lineNumber,
|
|
172
|
+
this.getBlockStartColumn(line),
|
|
173
|
+
line,
|
|
174
|
+
controlFlowStack.length,
|
|
175
|
+
controlFlowStack
|
|
176
|
+
));
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
// Look ahead for opening brace on next line
|
|
180
|
+
let braceLineIndex = -1;
|
|
181
|
+
for (let j = i + 1; j < Math.min(i + 3, filteredLines.length); j++) {
|
|
182
|
+
const nextFilteredLine = filteredLines[j];
|
|
183
|
+
if (nextFilteredLine.isComment) continue; // Skip comment lines
|
|
184
|
+
if (nextFilteredLine.line.trim() === '{') {
|
|
185
|
+
braceLineIndex = nextFilteredLine.lineNumber - 1; // Convert back to 0-based
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
if (nextFilteredLine.line.trim() !== '') break; // Stop if non-empty, non-brace line
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (braceLineIndex >= 0) {
|
|
192
|
+
controlFlowStack.push({
|
|
193
|
+
type: controlFlowMatch.type,
|
|
194
|
+
line: braceLineIndex + 1,
|
|
195
|
+
column: this.getBlockStartColumn(lines[braceLineIndex])
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Check depth violation at the opening brace
|
|
199
|
+
if (controlFlowStack.length > this.maxDepth) {
|
|
200
|
+
violations.push(this.createViolation(
|
|
201
|
+
filePath,
|
|
202
|
+
braceLineIndex + 1,
|
|
203
|
+
this.getBlockStartColumn(lines[braceLineIndex]),
|
|
204
|
+
lines[braceLineIndex],
|
|
205
|
+
controlFlowStack.length,
|
|
206
|
+
controlFlowStack
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Handle closing braces - only remove if we have control flow blocks
|
|
214
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
215
|
+
for (let j = 0; j < closeBraces && controlFlowStack.length > 0; j++) {
|
|
216
|
+
controlFlowStack.pop();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.warn(`C010 analysis error for ${filePath}:`, error.message);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return violations;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
detectControlFlow(line) {
|
|
228
|
+
// Match control flow keywords that create nesting
|
|
229
|
+
const patterns = [
|
|
230
|
+
{ pattern: /^\s*if\s*\(/, type: 'if' },
|
|
231
|
+
{ pattern: /^\s*else\s+if\s*\(/, type: 'else-if' },
|
|
232
|
+
{ pattern: /^\s*else\s*$/, type: 'else' },
|
|
233
|
+
{ pattern: /^\s*for\s*\(/, type: 'for' },
|
|
234
|
+
{ pattern: /^\s*while\s*\(/, type: 'while' },
|
|
235
|
+
{ pattern: /^\s*do\s*$/, type: 'do-while' },
|
|
236
|
+
{ pattern: /^\s*switch\s*\(/, type: 'switch' },
|
|
237
|
+
{ pattern: /^\s*try\s*$/, type: 'try' },
|
|
238
|
+
{ pattern: /^\s*catch\s*\(/, type: 'catch' },
|
|
239
|
+
{ pattern: /^\s*finally\s*$/, type: 'finally' },
|
|
240
|
+
{ pattern: /^\s*with\s*\(/, type: 'with' },
|
|
241
|
+
// Handle closing brace followed by control flow
|
|
242
|
+
{ pattern: /^\s*}\s*else\s+if\s*\(/, type: 'else-if' },
|
|
243
|
+
{ pattern: /^\s*}\s*else\s*$/, type: 'else' },
|
|
244
|
+
{ pattern: /^\s*}\s*catch\s*\(/, type: 'catch' },
|
|
245
|
+
{ pattern: /^\s*}\s*finally\s*$/, type: 'finally' }
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
for (const pattern of patterns) {
|
|
249
|
+
if (pattern.pattern.test(line)) {
|
|
250
|
+
return { type: pattern.type };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
detectBlockOpening(trimmedLine, fullLine) {
|
|
258
|
+
// First check if this is a standalone opening brace that follows a pending block
|
|
259
|
+
if (trimmedLine === '{' && this.pendingBlocks.length > 0) {
|
|
260
|
+
const pendingBlock = this.pendingBlocks.pop();
|
|
261
|
+
return {
|
|
262
|
+
opens: true,
|
|
263
|
+
type: pendingBlock.type.replace('-pending', ''),
|
|
264
|
+
column: this.getBlockStartColumn(fullLine),
|
|
265
|
+
inline: false
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check for block patterns
|
|
270
|
+
for (const blockPattern of this.blockPatterns) {
|
|
271
|
+
if (blockPattern.pattern.test(trimmedLine)) {
|
|
272
|
+
if (blockPattern.needsBrace) {
|
|
273
|
+
// This is a pending block, add to pending list
|
|
274
|
+
this.pendingBlocks.push({
|
|
275
|
+
type: blockPattern.type,
|
|
276
|
+
line: fullLine
|
|
277
|
+
});
|
|
278
|
+
return { opens: false };
|
|
279
|
+
} else {
|
|
280
|
+
return {
|
|
281
|
+
opens: blockPattern.opens,
|
|
282
|
+
type: blockPattern.type,
|
|
283
|
+
column: this.getBlockStartColumn(fullLine),
|
|
284
|
+
inline: false
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { opens: false };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
detectInlineBlock(trimmedLine) {
|
|
294
|
+
// Skip if line ends with { or ;
|
|
295
|
+
if (trimmedLine.endsWith('{') || trimmedLine.endsWith(';')) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const pattern of this.inlineBlockPatterns) {
|
|
300
|
+
if (pattern.pattern.test(trimmedLine)) {
|
|
301
|
+
return { type: pattern.type };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
isClosingBrace(line) {
|
|
309
|
+
// Match closing brace, possibly followed by else/catch/finally
|
|
310
|
+
return /^\s*}\s*(else|catch|finally)?\s*(\{|$)/.test(line);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
handleClosingBrace(blockStack) {
|
|
314
|
+
if (blockStack.length > 0) {
|
|
315
|
+
// Remove the most recent block
|
|
316
|
+
blockStack.pop();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
calculateEffectiveDepth(blockStack) {
|
|
321
|
+
// Count only non-inline blocks for depth calculation
|
|
322
|
+
return blockStack.filter(block => !block.inline).length;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
getBlockStartColumn(line) {
|
|
326
|
+
const match = line.match(/^\s*/);
|
|
327
|
+
return match ? match[0].length + 1 : 1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
isTestFile(filePath) {
|
|
331
|
+
const testPatterns = [
|
|
332
|
+
/\.test\.(js|ts|jsx|tsx)$/,
|
|
333
|
+
/\.spec\.(js|ts|jsx|tsx)$/,
|
|
334
|
+
/\/__tests__\//,
|
|
335
|
+
/\/tests?\//,
|
|
336
|
+
/\.e2e\./,
|
|
337
|
+
/test\.config\./,
|
|
338
|
+
/jest\.config\./,
|
|
339
|
+
/vitest\.config\./,
|
|
340
|
+
/cypress\//
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
return testPatterns.some(pattern => pattern.test(filePath));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
createViolation(filePath, lineNumber, column, sourceLine, depth, blockStack) {
|
|
347
|
+
return {
|
|
348
|
+
ruleId: this.ruleId,
|
|
349
|
+
severity: this.severity,
|
|
350
|
+
message: `🔄 [REGEX] Block nesting depth ${depth} exceeds maximum of ${this.maxDepth}. Consider refactoring to reduce complexity.`,
|
|
351
|
+
filePath: filePath,
|
|
352
|
+
line: lineNumber,
|
|
353
|
+
column: column,
|
|
354
|
+
source: sourceLine.trim(),
|
|
355
|
+
suggestion: this.getSuggestion(depth),
|
|
356
|
+
nestingStack: blockStack.map(b => ({
|
|
357
|
+
type: b.type,
|
|
358
|
+
line: b.line,
|
|
359
|
+
inline: b.inline || false
|
|
360
|
+
}))
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getSuggestion(currentDepth) {
|
|
365
|
+
const suggestions = [
|
|
366
|
+
"Extract nested logic into separate functions",
|
|
367
|
+
"Use early returns to reduce nesting",
|
|
368
|
+
"Consider using guard clauses",
|
|
369
|
+
"Break complex conditions into meaningful variables",
|
|
370
|
+
"Use strategy pattern for complex conditional logic",
|
|
371
|
+
"Consider using a state machine for complex flow control"
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
const index = Math.min(currentDepth - this.maxDepth - 1, suggestions.length - 1);
|
|
375
|
+
return suggestions[Math.max(0, index)];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = C010RegexBasedAnalyzer;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol-based analyzer for C010 - Block Nesting Detection using AST
|
|
3
|
+
* Purpose: Use AST traversal to accurately detect nested block statements
|
|
4
|
+
* Advantage: More accurate than regex, handles complex syntax naturally
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { SyntaxKind } = require('ts-morph');
|
|
8
|
+
|
|
9
|
+
class C010SymbolBasedAnalyzer {
|
|
10
|
+
constructor(semanticEngine = null) {
|
|
11
|
+
this.ruleId = 'C010';
|
|
12
|
+
this.ruleName = 'Limit Block Nesting (Symbol-Based)';
|
|
13
|
+
this.semanticEngine = semanticEngine;
|
|
14
|
+
this.verbose = false;
|
|
15
|
+
|
|
16
|
+
// Configuration
|
|
17
|
+
this.maxNestingLevel = 3;
|
|
18
|
+
|
|
19
|
+
// Block statement kinds that count toward nesting (use ts-morph SyntaxKind)
|
|
20
|
+
this.blockStatementKinds = new Set([
|
|
21
|
+
SyntaxKind.IfStatement,
|
|
22
|
+
SyntaxKind.ForStatement,
|
|
23
|
+
SyntaxKind.ForInStatement,
|
|
24
|
+
SyntaxKind.ForOfStatement,
|
|
25
|
+
SyntaxKind.WhileStatement,
|
|
26
|
+
SyntaxKind.DoStatement,
|
|
27
|
+
SyntaxKind.SwitchStatement,
|
|
28
|
+
SyntaxKind.TryStatement,
|
|
29
|
+
SyntaxKind.CatchClause,
|
|
30
|
+
SyntaxKind.Block
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// Statements that DON'T count toward nesting
|
|
34
|
+
this.nonNestingKinds = new Set([
|
|
35
|
+
SyntaxKind.FunctionDeclaration,
|
|
36
|
+
SyntaxKind.MethodDeclaration,
|
|
37
|
+
SyntaxKind.ArrowFunction,
|
|
38
|
+
SyntaxKind.FunctionExpression,
|
|
39
|
+
SyntaxKind.Constructor,
|
|
40
|
+
SyntaxKind.GetAccessor,
|
|
41
|
+
SyntaxKind.SetAccessor,
|
|
42
|
+
SyntaxKind.ClassDeclaration,
|
|
43
|
+
SyntaxKind.InterfaceDeclaration,
|
|
44
|
+
SyntaxKind.ObjectLiteralExpression,
|
|
45
|
+
SyntaxKind.ArrayLiteralExpression
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async initialize(semanticEngine = null) {
|
|
50
|
+
if (semanticEngine) {
|
|
51
|
+
this.semanticEngine = semanticEngine;
|
|
52
|
+
}
|
|
53
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
54
|
+
|
|
55
|
+
if (this.verbose) {
|
|
56
|
+
console.log(`[DEBUG] 🔧 C010 Symbol-Based: Analyzer initialized with max nesting level: ${this.maxNestingLevel}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async analyze(files, language, options = {}) {
|
|
61
|
+
const violations = [];
|
|
62
|
+
|
|
63
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
64
|
+
console.log(`[C010 Symbol-Based] Starting analysis for ${files.length} files`);
|
|
65
|
+
console.log(`[C010 Symbol-Based] Semantic engine available: ${!!this.semanticEngine}`);
|
|
66
|
+
console.log(`[C010 Symbol-Based] Project available: ${!!this.semanticEngine?.project}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!this.semanticEngine?.project) {
|
|
70
|
+
if (this.verbose || process.env.SUNLINT_DEBUG) {
|
|
71
|
+
console.warn('[C010 Symbol-Based] No semantic engine available, skipping analysis');
|
|
72
|
+
}
|
|
73
|
+
return violations;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const filePath of files) {
|
|
77
|
+
try {
|
|
78
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
79
|
+
console.log(`[C010 Symbol-Based] Analyzing file: ${filePath}`);
|
|
80
|
+
}
|
|
81
|
+
const fileViolations = await this.analyzeFileWithSymbols(filePath, options);
|
|
82
|
+
violations.push(...fileViolations);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (this.verbose || process.env.SUNLINT_DEBUG) {
|
|
85
|
+
console.warn(`[C010 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
86
|
+
console.warn(`[C010 Symbol-Based] Error stack:`, error.stack);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
92
|
+
console.log(`[C010 Symbol-Based] Analysis complete: ${violations.length} violations found`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return violations;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
99
|
+
const violations = [];
|
|
100
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath); if (!sourceFile) {
|
|
101
|
+
if (this.verbose) {
|
|
102
|
+
console.warn(`[C010 Symbol-Based] Source file not found: ${filePath}`);
|
|
103
|
+
}
|
|
104
|
+
return violations;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Traverse AST and track nesting depth
|
|
108
|
+
this.traverseNode(sourceFile, 0, violations, filePath);
|
|
109
|
+
|
|
110
|
+
if (this.verbose) {
|
|
111
|
+
console.log(`[C010 Symbol-Based] Found ${violations.length} violations in ${filePath}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return violations;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Recursively traverse AST nodes and track block nesting depth
|
|
119
|
+
*/
|
|
120
|
+
traverseNode(node, currentDepth, violations, filePath) {
|
|
121
|
+
const nodeKind = node.getKind();
|
|
122
|
+
|
|
123
|
+
// Check if this node starts a new nesting level
|
|
124
|
+
let newDepth = currentDepth;
|
|
125
|
+
let isBlockStatement = false;
|
|
126
|
+
|
|
127
|
+
if (this.isNestingStatement(node)) {
|
|
128
|
+
newDepth = currentDepth + 1;
|
|
129
|
+
isBlockStatement = true;
|
|
130
|
+
|
|
131
|
+
// Check if nesting exceeds maximum allowed
|
|
132
|
+
if (newDepth > this.maxNestingLevel) {
|
|
133
|
+
const startPos = node.getStart();
|
|
134
|
+
const sourceFile = node.getSourceFile();
|
|
135
|
+
const lineAndChar = sourceFile.getLineAndColumnAtPos(startPos);
|
|
136
|
+
|
|
137
|
+
violations.push({
|
|
138
|
+
ruleId: this.ruleId,
|
|
139
|
+
severity: 'warning',
|
|
140
|
+
message: `Block nesting is too deep (level ${newDepth}). Maximum allowed is ${this.maxNestingLevel} levels.`,
|
|
141
|
+
filePath: filePath,
|
|
142
|
+
line: lineAndChar.line + 1,
|
|
143
|
+
column: lineAndChar.column + 1,
|
|
144
|
+
context: this.getNodeContext(node)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Recursively analyze child nodes
|
|
150
|
+
node.forEachChild(child => {
|
|
151
|
+
// Don't increase depth for function boundaries
|
|
152
|
+
if (this.isFunctionBoundary(child)) {
|
|
153
|
+
// Reset depth for function/method/class boundaries
|
|
154
|
+
this.traverseNode(child, 0, violations, filePath);
|
|
155
|
+
} else {
|
|
156
|
+
this.traverseNode(child, newDepth, violations, filePath);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if a node represents a statement that increases nesting depth
|
|
163
|
+
*/
|
|
164
|
+
isNestingStatement(node) {
|
|
165
|
+
const kind = node.getKind();
|
|
166
|
+
|
|
167
|
+
// Basic block statements
|
|
168
|
+
if (this.blockStatementKinds.has(kind)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Special case: Block statements inside other constructs
|
|
173
|
+
if (kind === SyntaxKind.Block) {
|
|
174
|
+
const parent = node.getParent();
|
|
175
|
+
if (parent && this.blockStatementKinds.has(parent.getKind())) {
|
|
176
|
+
return false; // Block is part of the parent statement
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a node represents a function boundary (resets nesting depth)
|
|
186
|
+
*/
|
|
187
|
+
isFunctionBoundary(node) {
|
|
188
|
+
return this.nonNestingKinds.has(node.getKind());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get context information for a violation
|
|
193
|
+
*/
|
|
194
|
+
getNodeContext(node) {
|
|
195
|
+
const text = node.getText();
|
|
196
|
+
const lines = text.split('\n');
|
|
197
|
+
const firstLine = lines[0].trim();
|
|
198
|
+
|
|
199
|
+
// Return first line or statement type
|
|
200
|
+
if (firstLine.length > 0) {
|
|
201
|
+
return firstLine.length > 50 ? firstLine.substring(0, 47) + '...' : firstLine;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fallback to node kind
|
|
205
|
+
const kind = node.getKind();
|
|
206
|
+
return SyntaxKind[kind] || 'Unknown';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get detailed information about nesting violation
|
|
211
|
+
*/
|
|
212
|
+
getNestingPath(node) {
|
|
213
|
+
const path = [];
|
|
214
|
+
let current = node.getParent();
|
|
215
|
+
|
|
216
|
+
while (current && path.length < 10) { // Limit to prevent infinite loops
|
|
217
|
+
if (this.isNestingStatement(current)) {
|
|
218
|
+
const kind = current.getKind();
|
|
219
|
+
const kindName = SyntaxKind[kind] || 'Unknown';
|
|
220
|
+
path.unshift(kindName);
|
|
221
|
+
} else if (this.isFunctionBoundary(current)) {
|
|
222
|
+
break; // Stop at function boundary
|
|
223
|
+
}
|
|
224
|
+
current = current.getParent();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return path;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = C010SymbolBasedAnalyzer;
|