@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/CONTRIBUTING.md +249 -605
  3. package/README.md +3 -4
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/large-project.json +143 -0
  7. package/config/presets/all.json +0 -1
  8. package/config/release.json +70 -0
  9. package/config/rule-analysis-strategies.js +38 -3
  10. package/config/rules/enhanced-rules-registry.json +474 -1179
  11. package/config/rules/rules-registry-generated.json +3 -3
  12. package/core/cli-action-handler.js +24 -30
  13. package/core/cli-program.js +11 -3
  14. package/core/config-merger.js +29 -2
  15. package/core/enhanced-rules-registry.js +3 -2
  16. package/core/semantic-engine.js +129 -19
  17. package/core/semantic-rule-base.js +4 -2
  18. package/core/unified-rule-registry.js +1 -1
  19. package/docs/COMMAND-EXAMPLES.md +134 -0
  20. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  21. package/engines/heuristic-engine.js +135 -16
  22. package/integrations/eslint/plugin/index.js +0 -2
  23. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
  24. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
  25. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
  26. package/origin-rules/common-en.md +19 -15
  27. package/package.json +1 -1
  28. package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
  29. package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
  30. package/rules/common/C006_function_naming/analyzer.js +29 -3
  31. package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
  32. package/rules/common/C010_limit_block_nesting/config.json +64 -0
  33. package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
  34. package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
  35. package/rules/common/C013_no_dead_code/analyzer.js +75 -177
  36. package/rules/common/C013_no_dead_code/config.json +61 -0
  37. package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
  38. package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
  39. package/rules/common/C014_dependency_injection/analyzer.js +48 -313
  40. package/rules/common/C014_dependency_injection/config.json +26 -0
  41. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
  42. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  43. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  44. package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
  45. package/rules/common/C018_no_throw_generic_error/config.json +50 -0
  46. package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
  47. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
  48. package/rules/common/C019_log_level_usage/analyzer.js +110 -317
  49. package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
  50. package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
  51. package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
  52. package/rules/common/C023_no_duplicate_variable/config.json +50 -0
  53. package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
  54. package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
  55. package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
  56. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
  57. package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
  58. package/rules/common/C033_separate_service_repository/README.md +78 -0
  59. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  60. package/rules/common/C033_separate_service_repository/config.json +50 -0
  61. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  62. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  63. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  64. package/rules/common/C035_error_logging_context/analyzer.js +232 -0
  65. package/rules/common/C035_error_logging_context/config.json +54 -0
  66. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  67. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  68. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  69. package/rules/common/C040_centralized_validation/config.json +46 -0
  70. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  71. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  72. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  73. package/rules/common/C076_explicit_function_types/README.md +30 -0
  74. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  75. package/rules/common/C076_explicit_function_types/config.json +15 -0
  76. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  77. package/rules/index.js +6 -1
  78. package/rules/parser/rule-parser.js +13 -2
  79. package/rules/security/S005_no_origin_auth/README.md +226 -0
  80. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  81. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  82. package/rules/security/S005_no_origin_auth/config.json +85 -0
  83. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  84. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  85. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  86. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  87. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  88. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  89. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  90. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  91. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  92. package/rules/security/S009_no_insecure_encryption/README.md +158 -0
  93. package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
  94. package/rules/security/S009_no_insecure_encryption/config.json +55 -0
  95. package/rules/security/S010_no_insecure_encryption/README.md +224 -0
  96. package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
  97. package/rules/security/S010_no_insecure_encryption/config.json +48 -0
  98. package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
  99. package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
  100. package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
  101. package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
  102. package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
  103. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  104. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  105. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  106. package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
  107. package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
  108. package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
  109. package/rules/security/S055_content_type_validation/README.md +176 -0
  110. package/rules/security/S055_content_type_validation/analyzer.js +312 -0
  111. package/rules/security/S055_content_type_validation/config.json +48 -0
  112. package/rules/utils/rule-helpers.js +140 -1
  113. package/scripts/consolidate-config.js +116 -0
  114. package/scripts/prepare-release.sh +1 -1
  115. package/config/rules/rules-registry.json +0 -765
  116. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  117. package/docs/FUTURE_PACKAGES.md +0 -83
  118. package/docs/HEURISTIC_VS_AI.md +0 -113
  119. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  120. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  121. package/docs/RELEASE_GUIDE.md +0 -230
  122. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  123. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  124. 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;