@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,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol-based analyzer for C013 - No Dead Code
|
|
3
|
+
* Purpose: Detect commented out code, unused variables/functions, and unreachable code using AST
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { SyntaxKind } = require('ts-morph');
|
|
7
|
+
|
|
8
|
+
class C013SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.ruleId = 'C013';
|
|
11
|
+
this.ruleName = 'No Dead Code (Symbol-Based)';
|
|
12
|
+
this.semanticEngine = semanticEngine;
|
|
13
|
+
this.verbose = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
initialize(options = {}) {
|
|
17
|
+
if (options.semanticEngine) {
|
|
18
|
+
this.semanticEngine = options.semanticEngine;
|
|
19
|
+
}
|
|
20
|
+
this.verbose = options.verbose || false;
|
|
21
|
+
|
|
22
|
+
if (this.verbose) {
|
|
23
|
+
console.log(`[DEBUG] 🔧 C013 Symbol-Based: Analyzer initialized`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async analyze(files, language, options = {}) {
|
|
28
|
+
const violations = [];
|
|
29
|
+
|
|
30
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
31
|
+
console.log(`[C013 Symbol-Based] Starting analysis for ${files.length} files`);
|
|
32
|
+
console.log(`[C013 Symbol-Based] Semantic engine available: ${!!this.semanticEngine}`);
|
|
33
|
+
console.log(`[C013 Symbol-Based] Options semantic engine: ${!!options.semanticEngine}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Use semantic engine from options if not already set
|
|
37
|
+
if (!this.semanticEngine && options.semanticEngine) {
|
|
38
|
+
this.semanticEngine = options.semanticEngine;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!this.semanticEngine?.project) {
|
|
42
|
+
if (this.verbose || process.env.SUNLINT_DEBUG) {
|
|
43
|
+
console.warn('[C013 Symbol-Based] No semantic engine available, skipping analysis');
|
|
44
|
+
}
|
|
45
|
+
return violations;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const filePath of files) {
|
|
49
|
+
try {
|
|
50
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
51
|
+
console.log(`[C013 Symbol-Based] Analyzing file: ${filePath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
55
|
+
|
|
56
|
+
if (!sourceFile) {
|
|
57
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
58
|
+
console.warn(`[C013 Symbol-Based] Could not load source file: ${filePath}`);
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 1. Check for commented out code
|
|
64
|
+
const commentedCodeViolations = this.detectCommentedOutCode(sourceFile, filePath);
|
|
65
|
+
violations.push(...commentedCodeViolations);
|
|
66
|
+
|
|
67
|
+
// 2. Check for unused variables
|
|
68
|
+
const unusedVariableViolations = this.detectUnusedVariables(sourceFile, filePath);
|
|
69
|
+
violations.push(...unusedVariableViolations);
|
|
70
|
+
|
|
71
|
+
// 3. Check for unused functions
|
|
72
|
+
const unusedFunctionViolations = this.detectUnusedFunctions(sourceFile, filePath);
|
|
73
|
+
violations.push(...unusedFunctionViolations);
|
|
74
|
+
|
|
75
|
+
// 4. Check for unreachable code
|
|
76
|
+
const unreachableCodeViolations = this.detectUnreachableCode(sourceFile, filePath);
|
|
77
|
+
violations.push(...unreachableCodeViolations);
|
|
78
|
+
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
81
|
+
console.error(`[C013 Symbol-Based] Error analyzing ${filePath}:`, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return violations;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
detectCommentedOutCode(sourceFile, filePath) {
|
|
90
|
+
const violations = [];
|
|
91
|
+
const text = sourceFile.getFullText();
|
|
92
|
+
const lines = text.split('\n');
|
|
93
|
+
|
|
94
|
+
const codePatterns = [
|
|
95
|
+
/function\s+\w+/,
|
|
96
|
+
/const\s+\w+\s*=/,
|
|
97
|
+
/let\s+\w+\s*=/,
|
|
98
|
+
/var\s+\w+\s*=/,
|
|
99
|
+
/if\s*\(/,
|
|
100
|
+
/for\s*\(/,
|
|
101
|
+
/while\s*\(/,
|
|
102
|
+
/return\s+/,
|
|
103
|
+
/console\./,
|
|
104
|
+
/import\s+/,
|
|
105
|
+
/export\s+/,
|
|
106
|
+
/class\s+\w+/,
|
|
107
|
+
/interface\s+\w+/,
|
|
108
|
+
/type\s+\w+\s*=/
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const processedLines = new Set(); // Track lines we've already processed
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
// Skip if this line was already processed as part of a block
|
|
115
|
+
if (processedLines.has(i)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const line = lines[i];
|
|
120
|
+
const trimmedLine = line.trim();
|
|
121
|
+
|
|
122
|
+
// Check single line comments and group consecutive ones
|
|
123
|
+
if (trimmedLine.startsWith('//')) {
|
|
124
|
+
const startLine = i;
|
|
125
|
+
let endLine = i;
|
|
126
|
+
let blockLines = [];
|
|
127
|
+
|
|
128
|
+
// Collect consecutive comment lines with their content, including those separated by empty lines
|
|
129
|
+
for (let j = i; j < lines.length; j++) {
|
|
130
|
+
const currentLine = lines[j].trim();
|
|
131
|
+
|
|
132
|
+
// Stop if we hit a non-comment, non-empty line
|
|
133
|
+
if (!currentLine.startsWith('//') && currentLine !== '') {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Skip empty lines but continue processing
|
|
138
|
+
if (currentLine === '') {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const content = currentLine.substring(2).trim();
|
|
143
|
+
const isLikelyCode = content.length >= 10 && this.looksLikeCode(content, codePatterns) && !this.isDocumentationComment(content);
|
|
144
|
+
const isShortCodeLine = content.length >= 3 && (
|
|
145
|
+
/^[A-Z_]+:\s*['"][^'"]*['"],?$/.test(content) || // Object property like: JASPA: '0',
|
|
146
|
+
/^[})\]];?$/.test(content) || // Closing braces/brackets
|
|
147
|
+
/^[{(\[]$/.test(content) || // Opening braces/brackets
|
|
148
|
+
/^return\s+.*;?$/.test(content) || // return statements
|
|
149
|
+
/^default:\s*$/.test(content) || // switch default
|
|
150
|
+
/^case\s+.*:$/.test(content) // switch cases
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
blockLines.push({
|
|
154
|
+
lineIndex: j,
|
|
155
|
+
content: content,
|
|
156
|
+
fullLine: lines[j],
|
|
157
|
+
isCode: isLikelyCode,
|
|
158
|
+
isShortCodeLine: isShortCodeLine
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Debug log for mappingWorkPartsRow.ts
|
|
162
|
+
if (filePath.includes('mappingWorkPartsRow.ts') && process.env.SUNLINT_DEBUG) {
|
|
163
|
+
console.log(`Line ${j+1}: "${content}" -> isCode: ${content.length >= 10 && this.looksLikeCode(content, codePatterns) && !this.isDocumentationComment(content)}`);
|
|
164
|
+
}
|
|
165
|
+
endLine = j;
|
|
166
|
+
processedLines.add(j); // Mark as processed
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Find consecutive code sections within the block
|
|
170
|
+
// For function-like blocks, group the entire block together
|
|
171
|
+
const blockContent = blockLines.map(line => line.content).join('\n');
|
|
172
|
+
const hasFunction = /\bfunction\s+\w+\s*\(/.test(blockContent) ||
|
|
173
|
+
/\bconst\s+\w+.*=>\s*\{/s.test(blockContent) ||
|
|
174
|
+
/\blet\s+\w+.*=>\s*\{/s.test(blockContent) ||
|
|
175
|
+
/\bvar\s+\w+.*=>\s*\{/s.test(blockContent) ||
|
|
176
|
+
/\bclass\s+\w+/.test(blockContent) ||
|
|
177
|
+
/\bit\s*\(\s*['"`].*['"`]\s*,\s*async\s*\(\s*\)\s*=>/s.test(blockContent) || // Jest it() async
|
|
178
|
+
/\bit\s*\(\s*['"`].*['"`]\s*,\s*\(\s*\)\s*=>/s.test(blockContent) || // Jest it() sync
|
|
179
|
+
/\bdescribe\s*\(\s*['"`].*['"`]\s*,\s*\(\s*\)\s*=>/s.test(blockContent) || // Jest describe()
|
|
180
|
+
/\btest\s*\(\s*['"`].*['"`]\s*,\s*async\s*\(\s*\)\s*=>/s.test(blockContent) || // Jest test() async
|
|
181
|
+
/\btest\s*\(\s*['"`].*['"`]\s*,\s*\(\s*\)\s*=>/s.test(blockContent); // Jest test() sync
|
|
182
|
+
|
|
183
|
+
const hasAnyCode = blockLines.some(line => line.isCode);
|
|
184
|
+
|
|
185
|
+
if (filePath.includes('BillingList.test.tsx') && process.env.SUNLINT_DEBUG) {
|
|
186
|
+
console.log(`[DEBUG] Block analysis: hasFunction=${hasFunction}, hasAnyCode=${hasAnyCode}, blockSize=${blockLines.length}`);
|
|
187
|
+
console.log(`[DEBUG] Block content snippet: "${blockContent.substring(0, 100)}..."`);
|
|
188
|
+
console.log(`[DEBUG] Jest patterns test:
|
|
189
|
+
- it async: ${/\bit\s*\(\s*['"`].*['"`]\s*,\s*async\s*\(\s*\)\s*=>/s.test(blockContent)}
|
|
190
|
+
- it sync: ${/\bit\s*\(\s*['"`].*['"`]\s*,\s*\(\s*\)\s*=>/s.test(blockContent)}
|
|
191
|
+
- test async: ${/\btest\s*\(\s*['"`].*['"`]\s*,\s*async\s*\(\s*\)\s*=>/s.test(blockContent)}
|
|
192
|
+
- describe: ${/\bdescribe\s*\(\s*['"`].*['"`]\s*,\s*\(\s*\)\s*=>/s.test(blockContent)}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (hasFunction && hasAnyCode) {
|
|
196
|
+
// For function blocks, group everything together
|
|
197
|
+
const firstCodeLineIndex = blockLines.findIndex(line => line.isCode);
|
|
198
|
+
const lastCodeLineIndex = blockLines.map((line, idx) => line.isCode ? idx : -1)
|
|
199
|
+
.filter(idx => idx !== -1)
|
|
200
|
+
.pop();
|
|
201
|
+
|
|
202
|
+
if (firstCodeLineIndex !== -1 && lastCodeLineIndex !== -1) {
|
|
203
|
+
// Use the very first line of the block instead of first code line for better accuracy
|
|
204
|
+
const startLineIndex = blockLines[0].lineIndex; // Start from beginning of comment block
|
|
205
|
+
const totalLines = lastCodeLineIndex - firstCodeLineIndex + 1;
|
|
206
|
+
|
|
207
|
+
if (filePath.includes('BillingList.test.tsx') && process.env.SUNLINT_DEBUG) {
|
|
208
|
+
console.log(`[DEBUG] Function block grouped: startLine=${startLineIndex + 1}, firstCodeLine=${blockLines[firstCodeLineIndex].lineIndex + 1}, totalLines=${totalLines}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
violations.push(this.createViolation(
|
|
212
|
+
filePath,
|
|
213
|
+
startLineIndex + 1, // Use start of comment block, not first code line
|
|
214
|
+
blockLines[0].fullLine.indexOf('//') + 1, // Column of first comment line
|
|
215
|
+
`Commented out code block detected (${totalLines} lines). Remove dead code or use Git for version history.`,
|
|
216
|
+
'commented-code'
|
|
217
|
+
));
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// Original logic for non-function blocks
|
|
221
|
+
let currentCodeStart = -1;
|
|
222
|
+
let currentCodeEnd = -1;
|
|
223
|
+
let hasCodeInBlock = false;
|
|
224
|
+
|
|
225
|
+
for (let k = 0; k < blockLines.length; k++) {
|
|
226
|
+
const lineInfo = blockLines[k];
|
|
227
|
+
|
|
228
|
+
if (lineInfo.isCode) {
|
|
229
|
+
hasCodeInBlock = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// A line is considered part of code if it's actual code OR short code line in a code context
|
|
233
|
+
const isPartOfCode = lineInfo.isCode || (lineInfo.isShortCodeLine && hasCodeInBlock);
|
|
234
|
+
|
|
235
|
+
if (isPartOfCode) {
|
|
236
|
+
if (currentCodeStart === -1) {
|
|
237
|
+
// Start new code section
|
|
238
|
+
currentCodeStart = k;
|
|
239
|
+
currentCodeEnd = k;
|
|
240
|
+
} else {
|
|
241
|
+
// Extend current code section
|
|
242
|
+
currentCodeEnd = k;
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// Non-code line - if we have a code section, report it
|
|
246
|
+
if (currentCodeStart !== -1) {
|
|
247
|
+
const codeStartLine = blockLines[currentCodeStart].lineIndex;
|
|
248
|
+
const codeCount = currentCodeEnd - currentCodeStart + 1;
|
|
249
|
+
|
|
250
|
+
const message = codeCount > 1
|
|
251
|
+
? `Commented out code block detected (${codeCount} lines). Remove dead code or use Git for version history.`
|
|
252
|
+
: `Commented out code detected. Remove dead code or use Git for version history.`;
|
|
253
|
+
|
|
254
|
+
violations.push(this.createViolation(
|
|
255
|
+
filePath,
|
|
256
|
+
codeStartLine + 1, // Convert to 1-based line number
|
|
257
|
+
blockLines[currentCodeStart].fullLine.indexOf('//') + 1,
|
|
258
|
+
message,
|
|
259
|
+
'commented-code'
|
|
260
|
+
));
|
|
261
|
+
|
|
262
|
+
// Reset for next code section
|
|
263
|
+
currentCodeStart = -1;
|
|
264
|
+
currentCodeEnd = -1;
|
|
265
|
+
hasCodeInBlock = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Report any remaining code section
|
|
271
|
+
if (currentCodeStart !== -1) {
|
|
272
|
+
const codeStartLine = blockLines[currentCodeStart].lineIndex;
|
|
273
|
+
const codeCount = currentCodeEnd - currentCodeStart + 1;
|
|
274
|
+
|
|
275
|
+
const message = codeCount > 1
|
|
276
|
+
? `Commented out code block detected (${codeCount} lines). Remove dead code or use Git for version history.`
|
|
277
|
+
: `Commented out code detected. Remove dead code or use Git for version history.`;
|
|
278
|
+
|
|
279
|
+
violations.push(this.createViolation(
|
|
280
|
+
filePath,
|
|
281
|
+
codeStartLine + 1, // Convert to 1-based line number
|
|
282
|
+
blockLines[currentCodeStart].fullLine.indexOf('//') + 1,
|
|
283
|
+
message,
|
|
284
|
+
'commented-code'
|
|
285
|
+
));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Don't forget the last code section from the original logic (this should already be handled above)
|
|
290
|
+
// This section can be removed as it's redundant
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check multi-line comments (but skip JSDoc)
|
|
294
|
+
if (trimmedLine.startsWith('/*') && !trimmedLine.startsWith('/**') && !trimmedLine.includes('*/')) {
|
|
295
|
+
let commentBlock = '';
|
|
296
|
+
let endLine = i;
|
|
297
|
+
|
|
298
|
+
// Collect the full comment block
|
|
299
|
+
for (let j = i; j < lines.length; j++) {
|
|
300
|
+
commentBlock += lines[j] + '\n';
|
|
301
|
+
processedLines.add(j); // Mark as processed
|
|
302
|
+
if (lines[j].includes('*/')) {
|
|
303
|
+
endLine = j;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Clean the comment block
|
|
309
|
+
const cleanedComment = commentBlock
|
|
310
|
+
.replace(/\/\*|\*\/|\*/g, '')
|
|
311
|
+
.trim();
|
|
312
|
+
|
|
313
|
+
// Skip if it's documentation
|
|
314
|
+
if (this.isDocumentationComment(cleanedComment)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (cleanedComment.length >= 20 && this.looksLikeCode(cleanedComment, codePatterns)) {
|
|
319
|
+
violations.push(this.createViolation(
|
|
320
|
+
filePath,
|
|
321
|
+
i + 1,
|
|
322
|
+
line.indexOf('/*') + 1,
|
|
323
|
+
`Commented out code block detected. Remove dead code or use Git for version history.`,
|
|
324
|
+
'commented-code-block'
|
|
325
|
+
));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return violations;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
isDocumentationComment(text) {
|
|
334
|
+
// Check for JSDoc tags and documentation patterns
|
|
335
|
+
const docPatterns = [
|
|
336
|
+
/@param\b/,
|
|
337
|
+
/@returns?\b/,
|
|
338
|
+
/@example\b/,
|
|
339
|
+
/@description\b/,
|
|
340
|
+
/@see\b/,
|
|
341
|
+
/@throws?\b/,
|
|
342
|
+
/@since\b/,
|
|
343
|
+
/@author\b/,
|
|
344
|
+
/@version\b/,
|
|
345
|
+
/\* Sort an array/,
|
|
346
|
+
/\* Items are sorted/,
|
|
347
|
+
/\* @/,
|
|
348
|
+
/Result:/,
|
|
349
|
+
/Note:/
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
// If it contains documentation patterns, it's likely documentation
|
|
353
|
+
if (docPatterns.some(pattern => pattern.test(text))) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Check for common explanatory phrases
|
|
358
|
+
const explanatoryPhrases = [
|
|
359
|
+
'explanation',
|
|
360
|
+
'description',
|
|
361
|
+
'example',
|
|
362
|
+
'usage',
|
|
363
|
+
'note that',
|
|
364
|
+
'this function',
|
|
365
|
+
'this method',
|
|
366
|
+
'basic usage',
|
|
367
|
+
'with duplicate',
|
|
368
|
+
'items not found'
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
const lowerText = text.toLowerCase();
|
|
372
|
+
return explanatoryPhrases.some(phrase => lowerText.includes(phrase));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
looksLikeCode(text, patterns) {
|
|
376
|
+
// Check if text matches code patterns
|
|
377
|
+
const matchCount = patterns.filter(pattern => pattern.test(text)).length;
|
|
378
|
+
|
|
379
|
+
// If it matches multiple patterns or contains typical code structure
|
|
380
|
+
if (matchCount >= 1) {
|
|
381
|
+
// Additional checks for code-like characteristics
|
|
382
|
+
const hasCodeStructure = (
|
|
383
|
+
text.includes('{') ||
|
|
384
|
+
text.includes(';') ||
|
|
385
|
+
text.includes('()') ||
|
|
386
|
+
text.includes('[]') ||
|
|
387
|
+
/\w+\s*=\s*\w+/.test(text) ||
|
|
388
|
+
/\w+\.\w+/.test(text)
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return hasCodeStructure;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
detectUnusedVariables(sourceFile, filePath) {
|
|
398
|
+
const violations = [];
|
|
399
|
+
|
|
400
|
+
// Get all variable declarations
|
|
401
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
|
|
402
|
+
|
|
403
|
+
for (const declaration of variableDeclarations) {
|
|
404
|
+
const name = declaration.getName();
|
|
405
|
+
|
|
406
|
+
// Skip variables with underscore prefix (conventional ignore)
|
|
407
|
+
if (name.startsWith('_') || name.startsWith('$')) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Skip destructured variables for now (complex analysis)
|
|
412
|
+
if (declaration.getNameNode().getKind() !== SyntaxKind.Identifier) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Skip exported variables (they might be used externally)
|
|
417
|
+
const variableStatement = declaration.getParent()?.getParent();
|
|
418
|
+
if (variableStatement && variableStatement.getKind() === SyntaxKind.VariableStatement) {
|
|
419
|
+
if (variableStatement.isExported()) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if variable is used
|
|
425
|
+
const usages = declaration.getNameNode().findReferences();
|
|
426
|
+
const isUsed = usages.some(ref =>
|
|
427
|
+
ref.getReferences().length > 1 // More than just the declaration itself
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
if (!isUsed) {
|
|
431
|
+
const line = sourceFile.getLineAndColumnAtPos(declaration.getStart()).line;
|
|
432
|
+
const column = sourceFile.getLineAndColumnAtPos(declaration.getStart()).column;
|
|
433
|
+
|
|
434
|
+
violations.push(this.createViolation(
|
|
435
|
+
filePath,
|
|
436
|
+
line,
|
|
437
|
+
column,
|
|
438
|
+
`Unused variable '${name}'. Remove dead code to keep codebase clean.`,
|
|
439
|
+
'unused-variable'
|
|
440
|
+
));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return violations;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
detectUnusedFunctions(sourceFile, filePath) {
|
|
448
|
+
const violations = [];
|
|
449
|
+
|
|
450
|
+
// Get all function declarations
|
|
451
|
+
const functionDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration);
|
|
452
|
+
|
|
453
|
+
for (const func of functionDeclarations) {
|
|
454
|
+
const name = func.getName();
|
|
455
|
+
|
|
456
|
+
if (!name) continue; // Anonymous functions
|
|
457
|
+
|
|
458
|
+
// Skip functions with underscore prefix
|
|
459
|
+
if (name.startsWith('_')) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Skip exported functions (they might be used externally)
|
|
464
|
+
if (func.isExported()) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Check if function is used
|
|
469
|
+
const nameNode = func.getNameNode();
|
|
470
|
+
if (nameNode) {
|
|
471
|
+
const usages = nameNode.findReferences();
|
|
472
|
+
const isUsed = usages.some(ref =>
|
|
473
|
+
ref.getReferences().length > 1 // More than just the declaration itself
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
if (!isUsed) {
|
|
477
|
+
const line = sourceFile.getLineAndColumnAtPos(func.getStart()).line;
|
|
478
|
+
const column = sourceFile.getLineAndColumnAtPos(func.getStart()).column;
|
|
479
|
+
|
|
480
|
+
violations.push(this.createViolation(
|
|
481
|
+
filePath,
|
|
482
|
+
line,
|
|
483
|
+
column,
|
|
484
|
+
`Unused function '${name}'. Remove dead code to keep codebase clean.`,
|
|
485
|
+
'unused-function'
|
|
486
|
+
));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return violations;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
detectUnreachableCode(sourceFile, filePath) {
|
|
495
|
+
const violations = [];
|
|
496
|
+
|
|
497
|
+
// Find all return, throw, break, continue statements
|
|
498
|
+
const terminatingStatements = [
|
|
499
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.ReturnStatement),
|
|
500
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.ThrowStatement),
|
|
501
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.BreakStatement),
|
|
502
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.ContinueStatement)
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
for (const statement of terminatingStatements) {
|
|
506
|
+
// Find the statement that contains this terminating statement
|
|
507
|
+
let containingStatement = statement;
|
|
508
|
+
let parent = statement.getParent();
|
|
509
|
+
|
|
510
|
+
// Walk up to find the statement that's directly in a block
|
|
511
|
+
while (parent && parent.getKind() !== SyntaxKind.Block && parent.getKind() !== SyntaxKind.SourceFile) {
|
|
512
|
+
containingStatement = parent;
|
|
513
|
+
parent = parent.getParent();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Get the parent block
|
|
517
|
+
const parentBlock = parent;
|
|
518
|
+
|
|
519
|
+
if (!parentBlock || parentBlock.getKind() === SyntaxKind.SourceFile) continue;
|
|
520
|
+
|
|
521
|
+
// Find all statements in the same block after this terminating statement
|
|
522
|
+
const allStatements = parentBlock.getStatements();
|
|
523
|
+
const currentIndex = allStatements.indexOf(containingStatement);
|
|
524
|
+
|
|
525
|
+
if (currentIndex >= 0 && currentIndex < allStatements.length - 1) {
|
|
526
|
+
// Check statements after the terminating statement
|
|
527
|
+
for (let i = currentIndex + 1; i < allStatements.length; i++) {
|
|
528
|
+
const nextStatement = allStatements[i];
|
|
529
|
+
|
|
530
|
+
// Skip comments and empty statements
|
|
531
|
+
if (nextStatement.getKind() === SyntaxKind.EmptyStatement) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Don't flag catch/finally blocks as unreachable
|
|
536
|
+
if (this.isInTryCatchFinally(nextStatement)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Skip if this is within a conditional (if/else) or loop that might not execute
|
|
541
|
+
if (this.isConditionallyReachable(containingStatement, nextStatement)) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const line = sourceFile.getLineAndColumnAtPos(nextStatement.getStart()).line;
|
|
546
|
+
const column = sourceFile.getLineAndColumnAtPos(nextStatement.getStart()).column;
|
|
547
|
+
|
|
548
|
+
violations.push(this.createViolation(
|
|
549
|
+
filePath,
|
|
550
|
+
line,
|
|
551
|
+
column,
|
|
552
|
+
`Unreachable code detected after ${statement.getKindName().toLowerCase()}. Remove dead code.`,
|
|
553
|
+
'unreachable-code'
|
|
554
|
+
));
|
|
555
|
+
|
|
556
|
+
break; // Only flag the first unreachable statement to avoid spam
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return violations;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
isInTryCatchFinally(node) {
|
|
565
|
+
// Check if the node is inside a try-catch-finally block
|
|
566
|
+
let parent = node.getParent();
|
|
567
|
+
while (parent) {
|
|
568
|
+
if (parent.getKind() === SyntaxKind.TryStatement) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
if (parent.getKind() === SyntaxKind.CatchClause) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
parent = parent.getParent();
|
|
575
|
+
}
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
isConditionallyReachable(terminatingStatement, nextStatement) {
|
|
580
|
+
// Check if the terminating statement is within an arrow function expression
|
|
581
|
+
// or other expression that shouldn't be considered as blocking execution
|
|
582
|
+
let current = terminatingStatement;
|
|
583
|
+
|
|
584
|
+
while (current) {
|
|
585
|
+
const kind = current.getKind();
|
|
586
|
+
|
|
587
|
+
// If the terminating statement is in an arrow function or function expression,
|
|
588
|
+
// it doesn't block the execution of subsequent statements in the parent scope
|
|
589
|
+
if (kind === SyntaxKind.ArrowFunction ||
|
|
590
|
+
kind === SyntaxKind.FunctionExpression ||
|
|
591
|
+
kind === SyntaxKind.ConditionalExpression) {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// If we're in an if statement, the return might be conditional
|
|
596
|
+
if (kind === SyntaxKind.IfStatement) {
|
|
597
|
+
// Check if this is a complete if-else that covers all paths
|
|
598
|
+
const ifStatement = current;
|
|
599
|
+
const elseStatement = ifStatement.getElseStatement();
|
|
600
|
+
|
|
601
|
+
// If there's no else, or else doesn't have a return, then subsequent code is reachable
|
|
602
|
+
if (!elseStatement || !this.hasUnconditionalReturn(elseStatement)) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
current = current.getParent();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
hasUnconditionalReturn(node) {
|
|
614
|
+
// Check if a node has an unconditional return statement
|
|
615
|
+
if (node.getKind() === SyntaxKind.ReturnStatement) {
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (node.getKind() === SyntaxKind.Block) {
|
|
620
|
+
const statements = node.getStatements();
|
|
621
|
+
return statements.some(stmt => this.hasUnconditionalReturn(stmt));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
createViolation(filePath, line, column, message, type) {
|
|
628
|
+
return {
|
|
629
|
+
file: filePath,
|
|
630
|
+
line: line,
|
|
631
|
+
column: column,
|
|
632
|
+
message: message,
|
|
633
|
+
severity: 'warning',
|
|
634
|
+
ruleId: this.ruleId,
|
|
635
|
+
type: type
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
module.exports = C013SymbolBasedAnalyzer;
|