@sun-asterisk/sunlint 1.2.1 → 1.2.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 (77) hide show
  1. package/config/rule-analysis-strategies.js +18 -2
  2. package/engines/eslint-engine.js +9 -11
  3. package/engines/heuristic-engine.js +55 -31
  4. package/package.json +2 -1
  5. package/rules/README.md +252 -0
  6. package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
  7. package/rules/common/C002_no_duplicate_code/config.json +23 -0
  8. package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
  9. package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
  10. package/rules/common/C006_function_naming/analyzer.js +504 -0
  11. package/rules/common/C006_function_naming/config.json +86 -0
  12. package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
  13. package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
  14. package/rules/common/C012_command_query_separation/analyzer.js +481 -0
  15. package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
  16. package/rules/common/C013_no_dead_code/analyzer.js +206 -0
  17. package/rules/common/C014_dependency_injection/analyzer.js +338 -0
  18. package/rules/common/C017_constructor_logic/analyzer.js +314 -0
  19. package/rules/common/C019_log_level_usage/analyzer.js +362 -0
  20. package/rules/common/C019_log_level_usage/config.json +121 -0
  21. package/rules/common/C029_catch_block_logging/analyzer-backup.js +426 -0
  22. package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
  23. package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
  24. package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
  25. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
  26. package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
  27. package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
  28. package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
  29. package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
  30. package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
  31. package/rules/common/C029_catch_block_logging/config.json +59 -0
  32. package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
  33. package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
  34. package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
  35. package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -0
  36. package/rules/common/C031_validation_separation/analyzer.js +186 -0
  37. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
  38. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
  39. package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
  40. package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
  41. package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
  42. package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
  43. package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
  44. package/rules/docs/C002_no_duplicate_code.md +57 -0
  45. package/rules/docs/C031_validation_separation.md +72 -0
  46. package/rules/index.js +155 -0
  47. package/rules/migration/converter.js +385 -0
  48. package/rules/migration/mapping.json +164 -0
  49. package/rules/parser/constants.js +31 -0
  50. package/rules/parser/file-config.js +80 -0
  51. package/rules/parser/rule-parser-simple.js +305 -0
  52. package/rules/parser/rule-parser.js +527 -0
  53. package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
  54. package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
  55. package/rules/security/S023_no_json_injection/analyzer.js +278 -0
  56. package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
  57. package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
  58. package/rules/security/S026_json_schema_validation/config.json +27 -0
  59. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
  60. package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
  61. package/rules/security/S029_csrf_protection/analyzer.js +330 -0
  62. package/rules/tests/C002_no_duplicate_code.test.js +50 -0
  63. package/rules/universal/C010/generic.js +0 -0
  64. package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
  65. package/rules/utils/ast-utils.js +191 -0
  66. package/rules/utils/base-analyzer.js +98 -0
  67. package/rules/utils/pattern-matchers.js +239 -0
  68. package/rules/utils/rule-helpers.js +264 -0
  69. package/rules/utils/severity-constants.js +93 -0
  70. package/scripts/generate_insights.js +188 -0
  71. package/scripts/merge-reports.js +0 -424
  72. package/scripts/test-scripts/README.md +0 -22
  73. package/scripts/test-scripts/test-c041-comparison.js +0 -114
  74. package/scripts/test-scripts/test-c041-eslint.js +0 -67
  75. package/scripts/test-scripts/test-eslint-rules.js +0 -146
  76. package/scripts/test-scripts/test-real-world.js +0 -44
  77. package/scripts/test-scripts/test-rules-on-real-projects.js +0 -86
@@ -0,0 +1,495 @@
1
+ /**
2
+ * C012 AST Analyzer - Command Query Separation
3
+ *
4
+ * Uses AST parsing to detect violations of the Command Query Separation principle:
5
+ * - Commands (modify state) should not return values
6
+ * - Queries (return data) should not have side effects
7
+ * - Functions that both modify state and return meaningful values violate CQS
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ class C012ASTAnalyzer {
14
+ constructor() {
15
+ this.ruleId = 'C012';
16
+ this.ruleName = 'Command Query Separation';
17
+ this.description = 'Separate commands (modify state) from queries (return data)';
18
+ this.severity = 'warning';
19
+ }
20
+
21
+ async analyze(files, language, config = {}) {
22
+ const violations = [];
23
+
24
+ for (const filePath of files) {
25
+ try {
26
+ const content = fs.readFileSync(filePath, 'utf8');
27
+ const fileViolations = await this.analyzeFile(filePath, content, language, config);
28
+ violations.push(...fileViolations);
29
+ } catch (error) {
30
+ console.warn(`C012 AST analysis failed for ${filePath}:`, error.message);
31
+ }
32
+ }
33
+
34
+ return violations;
35
+ }
36
+
37
+ async analyzeFile(filePath, content, language, config) {
38
+ const violations = [];
39
+
40
+ try {
41
+ let ast;
42
+
43
+ if (language === 'typescript' || filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
44
+ ast = await this.parseTypeScript(content);
45
+ } else if (language === 'javascript' || filePath.endsWith('.js') || filePath.endsWith('.jsx')) {
46
+ ast = await this.parseJavaScript(content);
47
+ } else {
48
+ // Fallback to regex analysis
49
+ return [];
50
+ }
51
+
52
+ if (ast) {
53
+ this.traverseAST(ast, (node) => {
54
+ const violation = this.checkCQSViolation(node, content, filePath);
55
+ if (violation) {
56
+ violations.push(violation);
57
+ }
58
+ });
59
+ }
60
+ } catch (error) {
61
+ console.warn(`C012 AST parsing failed for ${filePath}:`, error.message);
62
+ }
63
+
64
+ return violations;
65
+ }
66
+
67
+ async parseTypeScript(content) {
68
+ try {
69
+ const babel = require('@babel/parser');
70
+ return babel.parse(content, {
71
+ sourceType: 'module',
72
+ allowImportExportEverywhere: true,
73
+ allowReturnOutsideFunction: true,
74
+ plugins: [
75
+ 'typescript',
76
+ 'jsx',
77
+ 'decorators-legacy',
78
+ 'classProperties',
79
+ 'asyncGenerators',
80
+ 'functionBind',
81
+ 'exportDefaultFrom',
82
+ 'exportNamespaceFrom',
83
+ 'dynamicImport',
84
+ 'nullishCoalescingOperator',
85
+ 'optionalChaining'
86
+ ]
87
+ });
88
+ } catch (error) {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ async parseJavaScript(content) {
94
+ try {
95
+ const babel = require('@babel/parser');
96
+ return babel.parse(content, {
97
+ sourceType: 'module',
98
+ allowImportExportEverywhere: true,
99
+ allowReturnOutsideFunction: true,
100
+ plugins: [
101
+ 'jsx',
102
+ 'decorators-legacy',
103
+ 'classProperties',
104
+ 'asyncGenerators',
105
+ 'functionBind',
106
+ 'exportDefaultFrom',
107
+ 'exportNamespaceFrom',
108
+ 'dynamicImport',
109
+ 'nullishCoalescingOperator',
110
+ 'optionalChaining'
111
+ ]
112
+ });
113
+ } catch (error) {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ traverseAST(node, callback) {
119
+ if (!node || typeof node !== 'object') return;
120
+
121
+ callback(node);
122
+
123
+ for (const key in node) {
124
+ if (key === 'parent' || key === 'loc' || key === 'range') continue;
125
+
126
+ const child = node[key];
127
+ if (Array.isArray(child)) {
128
+ child.forEach(item => this.traverseAST(item, callback));
129
+ } else if (child && typeof child === 'object') {
130
+ this.traverseAST(child, callback);
131
+ }
132
+ }
133
+ }
134
+
135
+ checkCQSViolation(node, content, filePath) {
136
+ // Check function declarations, methods, and arrow functions
137
+ if (!this.isFunctionNode(node)) {
138
+ return null;
139
+ }
140
+
141
+ const functionName = this.getFunctionName(node);
142
+ if (!functionName || this.isAllowedFunction(functionName)) {
143
+ return null;
144
+ }
145
+
146
+ const hasStateModification = this.hasStateModification(node);
147
+ const hasReturnValue = this.hasReturnValue(node);
148
+
149
+ // CQS violation: function both modifies state AND returns meaningful value
150
+ if (hasStateModification && hasReturnValue) {
151
+ // NEW: Check if this is an acceptable pattern
152
+ if (this.isAcceptablePattern(node, functionName)) {
153
+ return null; // Allow acceptable patterns
154
+ }
155
+
156
+ const line = node.loc ? node.loc.start.line : 1;
157
+ const column = node.loc ? node.loc.start.column + 1 : 1;
158
+
159
+ return {
160
+ ruleId: this.ruleId,
161
+ file: filePath,
162
+ line,
163
+ column,
164
+ message: `Function '${functionName}' violates Command Query Separation: both modifies state and returns value`,
165
+ severity: this.severity,
166
+ code: this.getNodeCode(node, content),
167
+ type: 'cqs_violation',
168
+ confidence: this.calculateConfidence(hasStateModification, hasReturnValue),
169
+ suggestion: this.getSuggestion(functionName, hasStateModification, hasReturnValue)
170
+ };
171
+ }
172
+
173
+ return null;
174
+ }
175
+
176
+ isFunctionNode(node) {
177
+ return [
178
+ 'FunctionDeclaration',
179
+ 'FunctionExpression',
180
+ 'ArrowFunctionExpression',
181
+ 'MethodDefinition',
182
+ 'ObjectMethod',
183
+ 'ClassMethod'
184
+ ].includes(node.type);
185
+ }
186
+
187
+ getFunctionName(node) {
188
+ if (node.key && node.key.name) {
189
+ return node.key.name; // Method
190
+ }
191
+ if (node.id && node.id.name) {
192
+ return node.id.name; // Function declaration
193
+ }
194
+ if (node.type === 'ArrowFunctionExpression' && node.parent) {
195
+ // Arrow function assigned to variable
196
+ if (node.parent.type === 'VariableDeclarator' && node.parent.id) {
197
+ return node.parent.id.name;
198
+ }
199
+ }
200
+ return 'anonymous';
201
+ }
202
+
203
+ isAllowedFunction(functionName) {
204
+ // Allowed patterns that don't violate CQS
205
+ const allowedPatterns = [
206
+ // Constructor and lifecycle
207
+ /^constructor$/,
208
+ /^componentDidMount$/,
209
+ /^componentWillUnmount$/,
210
+ /^useEffect$/,
211
+
212
+ // Test functions
213
+ /^test_/,
214
+ /^it$/,
215
+ /^describe$/,
216
+ /^beforeEach$/,
217
+ /^afterEach$/,
218
+
219
+ // Getters/setters (have special semantics)
220
+ /^get\w+$/,
221
+ /^set\w+$/,
222
+
223
+ // Factory/builder patterns (expected to create and return)
224
+ /^create\w+$/,
225
+ /^build\w+$/,
226
+ /^make\w+$/,
227
+ /^new\w+$/,
228
+
229
+ // Initialization (setup state and return success)
230
+ /^init\w+$/,
231
+ /^setup\w+$/,
232
+ /^configure\w+$/,
233
+
234
+ // Toggle operations (modify state and return new state)
235
+ /^toggle\w+$/,
236
+ /^switch\w+$/,
237
+
238
+ // Array operations that modify and return
239
+ /^push$/,
240
+ /^pop$/,
241
+ /^shift$/,
242
+ /^unshift$/,
243
+ /^splice$/,
244
+
245
+ // Built-in operations
246
+ /^toString$/,
247
+ /^valueOf$/,
248
+ /^render$/
249
+ ];
250
+
251
+ return allowedPatterns.some(pattern => pattern.test(functionName));
252
+ }
253
+
254
+ hasStateModification(node) {
255
+ let hasModification = false;
256
+
257
+ this.traverseAST(node.body, (innerNode) => {
258
+ if (hasModification) return;
259
+
260
+ // Assignment operations
261
+ if (innerNode.type === 'AssignmentExpression') {
262
+ // Check if assigning to object property or variable
263
+ if (innerNode.left.type === 'MemberExpression' ||
264
+ innerNode.left.type === 'Identifier') {
265
+ hasModification = true;
266
+ }
267
+ }
268
+
269
+ // Update expressions (++, --)
270
+ if (innerNode.type === 'UpdateExpression') {
271
+ hasModification = true;
272
+ }
273
+
274
+ // Method calls that likely modify state
275
+ if (innerNode.type === 'CallExpression' && innerNode.callee) {
276
+ const callName = this.getCallName(innerNode.callee);
277
+ if (this.isStateModifyingCall(callName)) {
278
+ hasModification = true;
279
+ }
280
+ }
281
+
282
+ // Property mutations
283
+ if (innerNode.type === 'MemberExpression' &&
284
+ innerNode.parent &&
285
+ innerNode.parent.type === 'AssignmentExpression' &&
286
+ innerNode.parent.left === innerNode) {
287
+ hasModification = true;
288
+ }
289
+ });
290
+
291
+ return hasModification;
292
+ }
293
+
294
+ hasReturnValue(node) {
295
+ let hasReturn = false;
296
+
297
+ this.traverseAST(node.body, (innerNode) => {
298
+ if (hasReturn) return;
299
+
300
+ // Return statements with value
301
+ if (innerNode.type === 'ReturnStatement' && innerNode.argument) {
302
+ // Ignore simple boolean returns (success/failure indicators)
303
+ if (!this.isSimpleBooleanReturn(innerNode.argument)) {
304
+ hasReturn = true;
305
+ }
306
+ }
307
+
308
+ // Arrow function with expression body
309
+ if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') {
310
+ if (!this.isSimpleBooleanReturn(node.body)) {
311
+ hasReturn = true;
312
+ }
313
+ }
314
+ });
315
+
316
+ return hasReturn;
317
+ }
318
+
319
+ isSimpleBooleanReturn(argument) {
320
+ // Simple boolean literals
321
+ if (argument.type === 'BooleanLiteral' ||
322
+ (argument.type === 'Literal' && typeof argument.value === 'boolean')) {
323
+ return true;
324
+ }
325
+
326
+ // Simple boolean expressions
327
+ if (argument.type === 'UnaryExpression' && argument.operator === '!') {
328
+ return true;
329
+ }
330
+
331
+ // Comparison operations (often return success/failure)
332
+ if (argument.type === 'BinaryExpression' &&
333
+ ['==', '===', '!=', '!==', '<', '>', '<=', '>='].includes(argument.operator)) {
334
+ return true;
335
+ }
336
+
337
+ return false;
338
+ }
339
+
340
+ getCallName(callee) {
341
+ if (callee.type === 'Identifier') {
342
+ return callee.name;
343
+ }
344
+ if (callee.type === 'MemberExpression' && callee.property) {
345
+ return callee.property.name;
346
+ }
347
+ return '';
348
+ }
349
+
350
+ isStateModifyingCall(callName) {
351
+ const modifyingCalls = [
352
+ 'push', 'pop', 'shift', 'unshift', 'splice',
353
+ 'sort', 'reverse', 'fill',
354
+ 'set', 'delete', 'clear',
355
+ 'add', 'remove', 'update',
356
+ 'save', 'store', 'persist',
357
+ 'increment', 'decrement',
358
+ 'modify', 'change', 'alter',
359
+ 'append', 'prepend', 'insert',
360
+ 'setState', 'dispatch'
361
+ ];
362
+
363
+ return modifyingCalls.includes(callName) ||
364
+ /^set[A-Z]/.test(callName) ||
365
+ /^update[A-Z]/.test(callName) ||
366
+ /^modify[A-Z]/.test(callName);
367
+ }
368
+
369
+ calculateConfidence(hasStateModification, hasReturnValue) {
370
+ let confidence = 0.7;
371
+
372
+ // Higher confidence if both conditions are clearly present
373
+ if (hasStateModification && hasReturnValue) {
374
+ confidence = 0.9;
375
+ }
376
+
377
+ return confidence;
378
+ }
379
+
380
+ isAcceptablePattern(node, functionName) {
381
+ // NEW: Practical CQS - Allow acceptable patterns per strategy
382
+
383
+ // 1. CRUD Operations (single operation + return)
384
+ const crudPatterns = [
385
+ /^(create|insert|add|save|store)\w*$/i,
386
+ /^(update|modify|edit|change)\w*$/i,
387
+ /^(upsert|merge)\w*$/i,
388
+ /^(delete|remove|destroy)\w*$/i
389
+ ];
390
+
391
+ if (crudPatterns.some(pattern => pattern.test(functionName))) {
392
+ // Check if it's a simple CRUD - single operation
393
+ const queryCount = this.countDatabaseOperations(node);
394
+ if (queryCount <= 1) {
395
+ return true; // Single query + return is acceptable
396
+ }
397
+ }
398
+
399
+ // 2. Transaction-based Operations
400
+ if (this.isTransactionBased(node)) {
401
+ return true; // Multiple operations in transaction are atomic
402
+ }
403
+
404
+ // 3. ORM Standard Patterns
405
+ const ormPatterns = [
406
+ /^findOrCreate\w*$/i,
407
+ /^findAndUpdate\w*$/i,
408
+ /^findAndModify\w*$/i,
409
+ /^saveAndReturn\w*$/i,
410
+ /^selectForUpdate\w*$/i
411
+ ];
412
+
413
+ if (ormPatterns.some(pattern => pattern.test(functionName))) {
414
+ return true; // Standard ORM patterns including selectForUpdate
415
+ }
416
+
417
+ // 4. Factory patterns (create and return by design)
418
+ const factoryPatterns = [
419
+ /^(build|construct|generate|produce)\w*$/i,
420
+ /^(transform|convert|map)\w*$/i
421
+ ];
422
+
423
+ if (factoryPatterns.some(pattern => pattern.test(functionName))) {
424
+ return true; // Factory patterns expected to create and return
425
+ }
426
+
427
+ return false; // Not an acceptable pattern - flag as violation
428
+ }
429
+
430
+ countDatabaseOperations(node) {
431
+ let count = 0;
432
+
433
+ this.traverseAST(node.body, (innerNode) => {
434
+ if (innerNode.type === 'CallExpression' && innerNode.callee) {
435
+ const callName = this.getCallName(innerNode.callee);
436
+
437
+ // Database operation patterns
438
+ const dbOperations = [
439
+ 'save', 'insert', 'create', 'update', 'delete', 'remove',
440
+ 'find', 'findOne', 'findBy', 'query', 'execute',
441
+ 'upsert', 'merge', 'replace'
442
+ ];
443
+
444
+ if (dbOperations.some(op => callName.toLowerCase().includes(op))) {
445
+ count++;
446
+ }
447
+ }
448
+ });
449
+
450
+ return count;
451
+ }
452
+
453
+ isTransactionBased(node) {
454
+ let isTransaction = false;
455
+
456
+ this.traverseAST(node.body, (innerNode) => {
457
+ if (innerNode.type === 'CallExpression' && innerNode.callee) {
458
+ const callName = this.getCallName(innerNode.callee);
459
+
460
+ // Transaction indicators
461
+ const transactionPatterns = [
462
+ 'transaction', 'withTransaction', 'runInTransaction',
463
+ 'beginTransaction', 'commit', 'rollback',
464
+ 'manager.transaction', 'queryRunner.startTransaction'
465
+ ];
466
+
467
+ if (transactionPatterns.some(pattern =>
468
+ callName.toLowerCase().includes(pattern.toLowerCase()))) {
469
+ isTransaction = true;
470
+ }
471
+ }
472
+ });
473
+
474
+ return isTransaction;
475
+ }
476
+
477
+ getSuggestion(functionName, hasStateModification, hasReturnValue) {
478
+ if (hasStateModification && hasReturnValue) {
479
+ return `Split '${functionName}' into separate command (modify state) and query (return data) functions`;
480
+ }
481
+ return `Follow Command Query Separation principle for '${functionName}'`;
482
+ }
483
+
484
+ getNodeCode(node, content) {
485
+ if (node.loc) {
486
+ const lines = content.split('\n');
487
+ const startLine = node.loc.start.line - 1;
488
+ const endLine = Math.min(node.loc.end.line - 1, startLine + 2); // Limit to 3 lines
489
+ return lines.slice(startLine, endLine + 1).join('\n').trim();
490
+ }
491
+ return 'Unknown code';
492
+ }
493
+ }
494
+
495
+ module.exports = C012ASTAnalyzer;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Heuristic analyzer for: C013 – Do not leave dead code
3
+ * Purpose: Detect unreachable code after return, throw, break, continue statements
4
+ */
5
+
6
+ class C013Analyzer {
7
+ constructor() {
8
+ this.ruleId = 'C013';
9
+ this.ruleName = 'No Dead Code';
10
+ this.description = 'Avoid unreachable code after return, throw, break, continue statements';
11
+ }
12
+
13
+ async analyze(files, language, options = {}) {
14
+ const violations = [];
15
+
16
+ for (const filePath of files) {
17
+ if (options.verbose) {
18
+ console.log(`🔍 Running C013 analysis on ${require('path').basename(filePath)}`);
19
+ }
20
+
21
+ try {
22
+ const content = require('fs').readFileSync(filePath, 'utf8');
23
+ const fileViolations = this.analyzeFile(content, filePath);
24
+ violations.push(...fileViolations);
25
+ } catch (error) {
26
+ console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
27
+ }
28
+ }
29
+
30
+ return violations;
31
+ }
32
+
33
+ analyzeFile(content, filePath) {
34
+ const violations = [];
35
+ const lines = content.split('\n');
36
+
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i].trim();
39
+
40
+ // Skip empty lines and comments
41
+ if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
42
+ continue;
43
+ }
44
+
45
+ // Only look for return statements (not throw in catch blocks)
46
+ if (this.isSimpleReturn(line)) {
47
+ // Check if there are non-comment, non-empty lines after this return
48
+ // within the same function scope
49
+ const unreachableLines = this.findUnreachableCodeSimple(lines, i);
50
+
51
+ for (const unreachableLine of unreachableLines) {
52
+ violations.push({
53
+ file: filePath,
54
+ line: unreachableLine + 1,
55
+ column: 1,
56
+ message: `Unreachable code detected after return statement. Remove dead code or restructure logic.`,
57
+ severity: 'warning',
58
+ ruleId: this.ruleId
59
+ });
60
+ }
61
+ }
62
+ }
63
+
64
+ return violations;
65
+ }
66
+
67
+ isSimpleReturn(line) {
68
+ // Only detect simple return statements that actually complete, not multiline returns
69
+ const cleanLine = line.replace(/;?\s*$/, '');
70
+
71
+ return (
72
+ // Complete return statements with semicolon or at end of line
73
+ /^return\s*;?\s*$/.test(cleanLine) ||
74
+ /^return\s+[^{}\[\(]+;?\s*$/.test(cleanLine) ||
75
+ // Single-line returns with simple values
76
+ /^return\s+(true|false|null|undefined|\d+|"[^"]*"|'[^']*')\s*;?\s*$/.test(cleanLine) ||
77
+ // Handle single-line conditional returns
78
+ /}\s*else\s*return\s+[^{}\[\(]+;?\s*$/.test(line) ||
79
+ /}\s*return\s+[^{}\[\(]+;?\s*$/.test(line)
80
+ );
81
+ }
82
+
83
+ findUnreachableCodeSimple(lines, terminatingLineIndex) {
84
+ const unreachableLines = [];
85
+
86
+ // Look for code after the return statement until we hit a closing brace or new function
87
+ for (let i = terminatingLineIndex + 1; i < lines.length; i++) {
88
+ const line = lines[i].trim();
89
+
90
+ // Skip empty lines and comments
91
+ if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
92
+ continue;
93
+ }
94
+
95
+ // Stop if we hit a closing brace (end of function/block)
96
+ if (line === '}' || line === '};' || line.startsWith('} ')) {
97
+ break;
98
+ }
99
+
100
+ // Stop if we hit catch/finally (these are reachable)
101
+ if (line.includes('catch') || line.includes('finally')) {
102
+ break;
103
+ }
104
+
105
+ // Stop if we hit a new function or method definition
106
+ if (line.includes('function') || line.includes('=>') || line.match(/^\w+\s*\(/)) {
107
+ break;
108
+ }
109
+
110
+ // This looks like unreachable code
111
+ if (this.isExecutableCode(line)) {
112
+ unreachableLines.push(i);
113
+ }
114
+ }
115
+
116
+ return unreachableLines;
117
+ }
118
+
119
+ findUnreachableCode(lines, terminatingLineIndex, content) {
120
+ const unreachableLines = [];
121
+ let braceDepth = this.getCurrentBraceDepth(lines, terminatingLineIndex, content);
122
+ let currentDepth = braceDepth;
123
+ let inTryCatchFinally = false;
124
+
125
+ // Check if we're inside a try-catch-finally context
126
+ const contextBefore = lines.slice(0, terminatingLineIndex).join(' ');
127
+ if (contextBefore.includes('try') || contextBefore.includes('catch') || contextBefore.includes('finally')) {
128
+ // Need more sophisticated detection of try-catch-finally blocks
129
+ inTryCatchFinally = true;
130
+ }
131
+
132
+ // Look for unreachable code after the terminating statement
133
+ for (let i = terminatingLineIndex + 1; i < lines.length; i++) {
134
+ const line = lines[i].trim();
135
+
136
+ // Skip empty lines and comments
137
+ if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
138
+ continue;
139
+ }
140
+
141
+ // Special handling for try-catch-finally: finally blocks are always reachable
142
+ if (line.includes('finally') || (inTryCatchFinally && line === '}')) {
143
+ // Don't mark finally blocks or their closing braces as unreachable
144
+ if (line.includes('finally')) {
145
+ inTryCatchFinally = false; // Reset after seeing finally
146
+ }
147
+ continue;
148
+ }
149
+
150
+ // Update brace depth
151
+ const openBraces = (line.match(/{/g) || []).length;
152
+ const closeBraces = (line.match(/}/g) || []).length;
153
+ currentDepth += openBraces - closeBraces;
154
+
155
+ // If we've exited the current block scope, stop looking
156
+ if (currentDepth < braceDepth) {
157
+ break;
158
+ }
159
+
160
+ // If we're still in the same block scope, this is potentially unreachable
161
+ if (currentDepth === braceDepth) {
162
+ // Check if this line contains executable code (not just closing braces)
163
+ if (this.isExecutableCode(line)) {
164
+ unreachableLines.push(i);
165
+ }
166
+ }
167
+ }
168
+
169
+ return unreachableLines;
170
+ }
171
+
172
+ getCurrentBraceDepth(lines, lineIndex, content) {
173
+ // Calculate the brace depth at the given line
174
+ let depth = 0;
175
+ const textUpToLine = lines.slice(0, lineIndex + 1).join('\n');
176
+
177
+ for (let char of textUpToLine) {
178
+ if (char === '{') depth++;
179
+ if (char === '}') depth--;
180
+ }
181
+
182
+ return depth;
183
+ }
184
+
185
+ isExecutableCode(line) {
186
+ // Exclude lines that are just structural (closing braces, etc.)
187
+ if (line === '}' || line === '};' || line === '},' || line.match(/^\s*}\s*$/)) {
188
+ return false;
189
+ }
190
+
191
+ // Exclude catch/finally blocks (they are reachable)
192
+ if (line.includes('catch') || line.includes('finally')) {
193
+ return false;
194
+ }
195
+
196
+ // Exclude case/default statements (they might be reachable)
197
+ if (line.startsWith('case ') || line.startsWith('default:')) {
198
+ return false;
199
+ }
200
+
201
+ // This looks like executable code
202
+ return true;
203
+ }
204
+ }
205
+
206
+ module.exports = C013Analyzer;