@sun-asterisk/sunlint 1.3.23 → 1.3.25

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.
@@ -31,6 +31,38 @@
31
31
  "heuristic": ["rules/common/C019_log_level_usage/analyzer.js"]
32
32
  }
33
33
  },
34
+ "C020": {
35
+ "name": "Unused Imports",
36
+ "description": "Không import các module hoặc symbol không sử dụng",
37
+ "category": "code-quality",
38
+ "severity": "warning",
39
+ "languages": ["typescript", "javascript"],
40
+ "analyzer": "./rules/common/C020_unused_imports/analyzer.js",
41
+ "config": "./rules/common/C020_unused_imports/config.json",
42
+ "version": "1.0.0",
43
+ "status": "stable",
44
+ "tags": ["imports", "cleanup", "unused-code"],
45
+ "engineMappings": {
46
+ "eslint": ["no-unused-vars", "@typescript-eslint/no-unused-vars"],
47
+ "heuristic": ["rules/common/C020_unused_imports/analyzer.js"]
48
+ }
49
+ },
50
+ "C021": {
51
+ "name": "Import Organization",
52
+ "description": "Tổ chức và sắp xếp imports theo nhóm và thứ tự alphabet",
53
+ "category": "code-quality",
54
+ "severity": "info",
55
+ "languages": ["typescript", "javascript"],
56
+ "analyzer": "./rules/common/C021_import_organization/analyzer.js",
57
+ "config": "./rules/common/C021_import_organization/config.json",
58
+ "version": "1.0.0",
59
+ "status": "stable",
60
+ "tags": ["imports", "organization", "readability"],
61
+ "engineMappings": {
62
+ "eslint": ["import/order", "sort-imports"],
63
+ "heuristic": ["rules/common/C021_import_organization/analyzer.js"]
64
+ }
65
+ },
34
66
  "C006": {
35
67
  "name": "Function Naming Convention",
36
68
  "description": "Tên hàm phải là động từ/verb-noun pattern",
@@ -380,9 +380,6 @@ async function createReviewsInBatches(octokit, owner, repoName, prNumber, headSh
380
380
  const batch = batches[i];
381
381
  const isLastBatch = i === batches.length - 1;
382
382
 
383
- // Only REQUEST_CHANGES on last batch if there are errors
384
- const eventType = isLastBatch && hasError ? 'REQUEST_CHANGES' : 'COMMENT';
385
-
386
383
  try {
387
384
  const reviewRes = await withRetry(async () => {
388
385
  return await octokit.pulls.createReview({
@@ -390,7 +387,7 @@ async function createReviewsInBatches(octokit, owner, repoName, prNumber, headSh
390
387
  repo: repoName,
391
388
  pull_number: prNumber,
392
389
  commit_id: headSha,
393
- event: eventType,
390
+ event: 'COMMENT',
394
391
  body: isLastBatch && batches.length > 1
395
392
  ? `SunLint found ${comments.length} issue(s) across multiple reviews.`
396
393
  : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.23",
3
+ "version": "1.3.25",
4
4
  "description": "☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -17,6 +17,8 @@ class C010SymbolBasedAnalyzer {
17
17
  this.maxNestingLevel = 3;
18
18
 
19
19
  // Block statement kinds that count toward nesting (use ts-morph SyntaxKind)
20
+ // IMPORTANT: Only control flow statements that represent LOGICAL complexity
21
+ // Try/catch are error handling wrappers and should NOT count toward nesting
20
22
  this.blockStatementKinds = new Set([
21
23
  SyntaxKind.IfStatement,
22
24
  SyntaxKind.ForStatement,
@@ -24,10 +26,9 @@ class C010SymbolBasedAnalyzer {
24
26
  SyntaxKind.ForOfStatement,
25
27
  SyntaxKind.WhileStatement,
26
28
  SyntaxKind.DoStatement,
27
- SyntaxKind.SwitchStatement,
28
- SyntaxKind.TryStatement,
29
- SyntaxKind.CatchClause,
30
- SyntaxKind.Block
29
+ SyntaxKind.SwitchStatement
30
+ // Removed: TryStatement, CatchClause, Block
31
+ // Rationale: Error handling wrappers don't add logical complexity
31
32
  ]);
32
33
 
33
34
  // Statements that DON'T count toward nesting
@@ -44,6 +45,13 @@ class C010SymbolBasedAnalyzer {
44
45
  SyntaxKind.ObjectLiteralExpression,
45
46
  SyntaxKind.ArrayLiteralExpression
46
47
  ]);
48
+
49
+ // Error handling constructs that should be transparent (don't add or reset nesting)
50
+ this.errorHandlingKinds = new Set([
51
+ SyntaxKind.TryStatement,
52
+ SyntaxKind.CatchClause,
53
+ SyntaxKind.FinallyKeyword
54
+ ]);
47
55
  }
48
56
 
49
57
  async initialize(semanticEngine = null) {
@@ -137,22 +145,30 @@ class C010SymbolBasedAnalyzer {
137
145
  violations.push({
138
146
  ruleId: this.ruleId,
139
147
  severity: 'warning',
140
- message: `Block nesting is too deep (level ${newDepth}). Maximum allowed is ${this.maxNestingLevel} levels.`,
148
+ message: `Block nesting depth ${newDepth} exceeds maximum of ${this.maxNestingLevel}. Consider refactoring to reduce complexity.`,
141
149
  filePath: filePath,
142
150
  line: lineAndChar.line + 1,
143
151
  column: lineAndChar.column + 1,
144
- context: this.getNodeContext(node)
152
+ context: this.getNodeContext(node),
153
+ suggestion: this.getSuggestion(newDepth)
145
154
  });
146
155
  }
147
156
  }
148
157
 
149
158
  // Recursively analyze child nodes
150
159
  node.forEachChild(child => {
151
- // Don't increase depth for function boundaries
160
+ const childKind = child.getKind();
161
+
162
+ // Reset depth for function boundaries
152
163
  if (this.isFunctionBoundary(child)) {
153
- // Reset depth for function/method/class boundaries
154
164
  this.traverseNode(child, 0, violations, filePath);
155
- } else {
165
+ }
166
+ // Keep same depth for error handling (transparent wrappers)
167
+ else if (this.errorHandlingKinds.has(childKind)) {
168
+ this.traverseNode(child, currentDepth, violations, filePath);
169
+ }
170
+ // Normal case: use new depth
171
+ else {
156
172
  this.traverseNode(child, newDepth, violations, filePath);
157
173
  }
158
174
  });
@@ -195,17 +211,30 @@ class C010SymbolBasedAnalyzer {
195
211
  const text = node.getText();
196
212
  const lines = text.split('\n');
197
213
  const firstLine = lines[0].trim();
198
-
214
+
199
215
  // Return first line or statement type
200
216
  if (firstLine.length > 0) {
201
217
  return firstLine.length > 50 ? firstLine.substring(0, 47) + '...' : firstLine;
202
218
  }
203
-
219
+
204
220
  // Fallback to node kind
205
221
  const kind = node.getKind();
206
222
  return SyntaxKind[kind] || 'Unknown';
207
223
  }
208
224
 
225
+ /**
226
+ * Get refactoring suggestion based on nesting depth
227
+ */
228
+ getSuggestion(depth) {
229
+ if (depth >= 6) {
230
+ return 'Critical: Extract nested logic into separate functions';
231
+ } else if (depth >= 5) {
232
+ return 'Use early returns, guard clauses, or extract methods';
233
+ } else {
234
+ return 'Consider using early returns or extracting to helper functions';
235
+ }
236
+ }
237
+
209
238
  /**
210
239
  * Get detailed information about nesting violation
211
240
  */
@@ -396,23 +396,30 @@ class C013SymbolBasedAnalyzer {
396
396
 
397
397
  detectUnusedVariables(sourceFile, filePath) {
398
398
  const violations = [];
399
-
399
+
400
400
  // Get all variable declarations
401
401
  const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
402
-
402
+
403
403
  for (const declaration of variableDeclarations) {
404
404
  const name = declaration.getName();
405
-
405
+
406
406
  // Skip variables with underscore prefix (conventional ignore)
407
407
  if (name.startsWith('_') || name.startsWith('$')) {
408
408
  continue;
409
409
  }
410
-
410
+
411
411
  // Skip destructured variables for now (complex analysis)
412
412
  if (declaration.getNameNode().getKind() !== SyntaxKind.Identifier) {
413
413
  continue;
414
414
  }
415
-
415
+
416
+ // Skip catch clause error variables (defensive programming pattern)
417
+ // Example: catch (error) { return false; } - error doesn't need to be used
418
+ // This aligns with C029 rule improvements
419
+ if (this.isInCatchClause(declaration)) {
420
+ continue;
421
+ }
422
+
416
423
  // Skip exported variables (they might be used externally)
417
424
  const variableStatement = declaration.getParent()?.getParent();
418
425
  if (variableStatement && variableStatement.getKind() === SyntaxKind.VariableStatement) {
@@ -420,17 +427,17 @@ class C013SymbolBasedAnalyzer {
420
427
  continue;
421
428
  }
422
429
  }
423
-
430
+
424
431
  // Check if variable is used
425
432
  const usages = declaration.getNameNode().findReferences();
426
- const isUsed = usages.some(ref =>
433
+ const isUsed = usages.some(ref =>
427
434
  ref.getReferences().length > 1 // More than just the declaration itself
428
435
  );
429
-
436
+
430
437
  if (!isUsed) {
431
438
  const line = sourceFile.getLineAndColumnAtPos(declaration.getStart()).line;
432
439
  const column = sourceFile.getLineAndColumnAtPos(declaration.getStart()).column;
433
-
440
+
434
441
  violations.push(this.createViolation(
435
442
  filePath,
436
443
  line,
@@ -440,10 +447,32 @@ class C013SymbolBasedAnalyzer {
440
447
  ));
441
448
  }
442
449
  }
443
-
450
+
444
451
  return violations;
445
452
  }
446
453
 
454
+ /**
455
+ * Check if a node is within a catch clause
456
+ * Used to skip catch clause error variables from unused variable detection
457
+ */
458
+ isInCatchClause(node) {
459
+ let current = node.getParent();
460
+ while (current) {
461
+ if (current.getKind() === SyntaxKind.CatchClause) {
462
+ return true;
463
+ }
464
+ // Stop at function boundaries
465
+ if (current.getKind() === SyntaxKind.FunctionDeclaration ||
466
+ current.getKind() === SyntaxKind.FunctionExpression ||
467
+ current.getKind() === SyntaxKind.ArrowFunction ||
468
+ current.getKind() === SyntaxKind.MethodDeclaration) {
469
+ return false;
470
+ }
471
+ current = current.getParent();
472
+ }
473
+ return false;
474
+ }
475
+
447
476
  detectUnusedFunctions(sourceFile, filePath) {
448
477
  const violations = [];
449
478
 
@@ -493,58 +522,66 @@ class C013SymbolBasedAnalyzer {
493
522
 
494
523
  detectUnreachableCode(sourceFile, filePath) {
495
524
  const violations = [];
496
-
497
- // Find all return, throw, break, continue statements
525
+
526
+ // Find all return and throw statements (truly terminating)
527
+ // Note: Break and continue are NOT included - they only exit loops/switches,
528
+ // not the containing function block
498
529
  const terminatingStatements = [
499
530
  ...sourceFile.getDescendantsOfKind(SyntaxKind.ReturnStatement),
500
- ...sourceFile.getDescendantsOfKind(SyntaxKind.ThrowStatement),
501
- ...sourceFile.getDescendantsOfKind(SyntaxKind.BreakStatement),
502
- ...sourceFile.getDescendantsOfKind(SyntaxKind.ContinueStatement)
531
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.ThrowStatement)
532
+ // Removed: BreakStatement, ContinueStatement - these don't make code unreachable
533
+ // Example: switch(x) { case A: break; } const y = 1; // ← y is reachable!
503
534
  ];
504
-
535
+
505
536
  for (const statement of terminatingStatements) {
506
537
  // Find the statement that contains this terminating statement
507
538
  let containingStatement = statement;
508
539
  let parent = statement.getParent();
509
-
540
+
510
541
  // Walk up to find the statement that's directly in a block
511
542
  while (parent && parent.getKind() !== SyntaxKind.Block && parent.getKind() !== SyntaxKind.SourceFile) {
512
543
  containingStatement = parent;
513
544
  parent = parent.getParent();
514
545
  }
515
-
546
+
516
547
  // Get the parent block
517
548
  const parentBlock = parent;
518
-
549
+
519
550
  if (!parentBlock || parentBlock.getKind() === SyntaxKind.SourceFile) continue;
520
-
551
+
552
+ // Check if this is in a conditional block (if/else/switch/loop)
553
+ // If yes, code after the conditional is still reachable
554
+ if (this.isInConditionalBlock(containingStatement)) {
555
+ continue;
556
+ }
557
+
521
558
  // Find all statements in the same block after this terminating statement
522
559
  const allStatements = parentBlock.getStatements();
523
560
  const currentIndex = allStatements.indexOf(containingStatement);
524
-
561
+
525
562
  if (currentIndex >= 0 && currentIndex < allStatements.length - 1) {
526
563
  // Check statements after the terminating statement
527
564
  for (let i = currentIndex + 1; i < allStatements.length; i++) {
528
565
  const nextStatement = allStatements[i];
529
-
566
+
530
567
  // Skip comments and empty statements
531
568
  if (nextStatement.getKind() === SyntaxKind.EmptyStatement) {
532
569
  continue;
533
570
  }
534
-
571
+
535
572
  // Don't flag catch/finally blocks as unreachable
536
573
  if (this.isInTryCatchFinally(nextStatement)) {
537
574
  continue;
538
575
  }
539
-
576
+
540
577
  // Skip if this is within a conditional (if/else) or loop that might not execute
541
578
  if (this.isConditionallyReachable(containingStatement, nextStatement)) {
542
579
  continue;
543
580
  }
544
-
581
+
545
582
  const line = sourceFile.getLineAndColumnAtPos(nextStatement.getStart()).line;
546
583
  const column = sourceFile.getLineAndColumnAtPos(nextStatement.getStart()).column;
547
-
584
+
548
585
  violations.push(this.createViolation(
549
586
  filePath,
550
587
  line,
@@ -552,15 +589,54 @@ class C013SymbolBasedAnalyzer {
552
589
  `Unreachable code detected after ${statement.getKindName().toLowerCase()}. Remove dead code.`,
553
590
  'unreachable-code'
554
591
  ));
555
-
592
+
556
593
  break; // Only flag the first unreachable statement to avoid spam
557
594
  }
558
595
  }
559
596
  }
560
-
597
+
561
598
  return violations;
562
599
  }
563
600
 
601
+ /**
602
+ * Check if a statement is within a conditional block (if/else/switch/loop)
603
+ * Code after such blocks is still reachable even if the block has return/throw
604
+ */
605
+ isInConditionalBlock(statement) {
606
+ let current = statement;
607
+
608
+ // Walk up the tree to check if we're inside conditional constructs
609
+ while (current) {
610
+ const kind = current.getKind();
611
+
612
+ // If we hit these, the code after the containing block is reachable
613
+ if (kind === SyntaxKind.IfStatement ||
614
+ kind === SyntaxKind.ElseKeyword ||
615
+ kind === SyntaxKind.SwitchStatement ||
616
+ kind === SyntaxKind.CaseClause ||
617
+ kind === SyntaxKind.DefaultClause ||
618
+ kind === SyntaxKind.ForStatement ||
619
+ kind === SyntaxKind.ForInStatement ||
620
+ kind === SyntaxKind.ForOfStatement ||
621
+ kind === SyntaxKind.WhileStatement ||
622
+ kind === SyntaxKind.DoStatement) {
623
+ return true;
624
+ }
625
+
626
+ // Stop at function boundaries
627
+ if (kind === SyntaxKind.FunctionDeclaration ||
628
+ kind === SyntaxKind.FunctionExpression ||
629
+ kind === SyntaxKind.ArrowFunction ||
630
+ kind === SyntaxKind.MethodDeclaration) {
631
+ return false;
632
+ }
633
+
634
+ current = current.getParent();
635
+ }
636
+
637
+ return false;
638
+ }
639
+
564
640
  isInTryCatchFinally(node) {
565
641
  // Check if the node is inside a try-catch-finally block
566
642
  let parent = node.getParent();
@@ -1,16 +1,16 @@
1
- const C019SystemLogAnalyzer = require('./system-log-analyzer.js');
1
+ const C019TsMorphAnalyzer = require('./ts-morph-analyzer.js');
2
2
  const C019PatternAnalyzer = require('./pattern-analyzer.js');
3
3
 
4
4
  class C019Analyzer {
5
5
  constructor(semanticEngine = null) {
6
6
  this.ruleId = 'C019';
7
7
  this.ruleName = 'Log Level Usage';
8
- this.description = 'Comprehensive logging analysis: levels, patterns, performance, and system requirements';
8
+ this.description = 'Detect inappropriate ERROR log level for business logic errors';
9
9
  this.semanticEngine = semanticEngine;
10
10
  this.verbose = false;
11
-
12
- // Initialize analyzers - consolidated architecture
13
- this.systemAnalyzer = new C019SystemLogAnalyzer(semanticEngine);
11
+
12
+ // Initialize analyzers - ts-morph primary, pattern fallback
13
+ this.tsMorphAnalyzer = new C019TsMorphAnalyzer(semanticEngine);
14
14
  this.patternAnalyzer = new C019PatternAnalyzer();
15
15
  this.aiAnalyzer = null;
16
16
  }
@@ -20,53 +20,56 @@ class C019Analyzer {
20
20
  this.semanticEngine = semanticEngine;
21
21
  }
22
22
  this.verbose = semanticEngine?.verbose || false;
23
-
24
- await this.systemAnalyzer.initialize(semanticEngine);
23
+
24
+ await this.tsMorphAnalyzer.initialize(semanticEngine);
25
25
  await this.patternAnalyzer.initialize({ verbose: this.verbose });
26
26
  }
27
27
 
28
28
  async analyzeFileBasic(filePath, options = {}) {
29
29
  const allViolations = [];
30
-
30
+
31
31
  try {
32
- // Run comprehensive system-level analysis (Primary - AST)
32
+ // Use ts-morph analysis (Primary - AST) if semantic engine is available
33
33
  if (this.semanticEngine?.isSymbolEngineReady?.() && this.semanticEngine.project) {
34
34
  if (this.verbose) {
35
- console.log(`[DEBUG] 🎯 C019: Using comprehensive system-level analysis for ${filePath.split('/').pop()}`);
35
+ console.log(`[DEBUG] 🎯 C019: Using ts-morph analysis for ${filePath.split('/').pop()}`);
36
36
  }
37
-
37
+
38
38
  try {
39
- const systemViolations = await this.systemAnalyzer.analyzeFileBasic(filePath, options);
40
- allViolations.push(...systemViolations);
41
-
39
+ const tsMorphViolations = await this.tsMorphAnalyzer.analyzeFile(filePath, options);
40
+ allViolations.push(...tsMorphViolations);
41
+
42
42
  if (this.verbose) {
43
- console.log(`[DEBUG] 🎯 C019: System analysis found ${systemViolations.length} violations`);
43
+ console.log(`[DEBUG] 🎯 C019: ts-morph analysis found ${tsMorphViolations.length} violations`);
44
44
  }
45
- } catch (systemError) {
45
+
46
+ // Return ts-morph results (even if 0) - do NOT fall back to pattern analyzer
47
+ // Pattern analyzer has no catch block detection and will produce false positives
48
+ return this.deduplicateViolations(allViolations);
49
+
50
+ } catch (tsMorphError) {
46
51
  if (this.verbose) {
47
- console.warn(`[DEBUG] ⚠️ C019: System analysis failed: ${systemError.message}`);
52
+ console.warn(`[DEBUG] ⚠️ C019: ts-morph analysis failed: ${tsMorphError.message}`);
48
53
  }
49
- }
50
-
51
- if (allViolations.length > 0) {
52
- return this.deduplicateViolations(allViolations);
54
+ // Only fall through to pattern analyzer if ts-morph failed
53
55
  }
54
56
  }
55
-
56
- // Fall back to pattern-based analysis (Secondary - Regex)
57
+
58
+ // Fall back to pattern-based analysis (Secondary - Regex)
59
+ // ONLY if semantic engine is not available or ts-morph failed
57
60
  if (this.verbose) {
58
61
  console.log(`[DEBUG] 🔄 C019: Running pattern-based analysis for ${filePath.split('/').pop()}`);
59
62
  }
60
-
63
+
61
64
  const patternViolations = await this.patternAnalyzer.analyzeFileBasic(filePath, options);
62
65
  allViolations.push(...patternViolations);
63
-
66
+
64
67
  if (this.verbose) {
65
68
  console.log(`[DEBUG] 🔄 C019: Pattern analysis found ${patternViolations.length} violations`);
66
69
  }
67
-
70
+
68
71
  return this.deduplicateViolations(allViolations);
69
-
72
+
70
73
  } catch (error) {
71
74
  if (this.verbose) {
72
75
  console.error(`[DEBUG] ❌ C019: Analysis failed: ${error.message}`);
@@ -12,7 +12,7 @@
12
12
  "errorKeywords": [
13
13
  "not found",
14
14
  "invalid",
15
- "unauthorized",
15
+ "unauthorized",
16
16
  "forbidden",
17
17
  "validation failed",
18
18
  "bad request",
@@ -23,7 +23,9 @@
23
23
  "input error",
24
24
  "validation",
25
25
  "invalid input",
26
- "missing parameter"
26
+ "missing parameter",
27
+ "exceed",
28
+ "limit"
27
29
  ],
28
30
  "legitimateErrorKeywords": [
29
31
  "exception",