@sun-asterisk/sunlint 1.3.46 → 1.3.48

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 (102) hide show
  1. package/config/rules/rules-registry-generated.json +1717 -282
  2. package/core/adapters/sunlint-rule-adapter.js +16 -0
  3. package/core/architecture-integration.js +57 -15
  4. package/core/cli-action-handler.js +51 -36
  5. package/core/config-manager.js +6 -0
  6. package/core/config-merger.js +33 -0
  7. package/core/config-validator.js +37 -2
  8. package/core/output-service.js +12 -3
  9. package/core/rule-selection-service.js +24 -3
  10. package/core/scoring-service.js +12 -6
  11. package/core/summary-report-service.js +9 -4
  12. package/engines/heuristic-engine.js +6 -1
  13. package/engines/impact/cli.js +54 -39
  14. package/engines/impact/config/default-config.js +105 -5
  15. package/engines/impact/core/impact-analyzer.js +12 -15
  16. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  17. package/engines/impact/core/utils/method-call-graph.js +272 -87
  18. package/origin-rules/dart-en.md +1 -1
  19. package/origin-rules/go-en.md +231 -0
  20. package/origin-rules/php-en.md +107 -0
  21. package/origin-rules/python-en.md +113 -0
  22. package/origin-rules/ruby-en.md +607 -0
  23. package/package.json +2 -2
  24. package/scripts/copy-arch-detect.js +5 -1
  25. package/scripts/copy-impact-analyzer.js +5 -1
  26. package/scripts/generate-rules-registry.js +30 -14
  27. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  28. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  29. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  30. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  31. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  32. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  33. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  34. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  35. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  36. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  37. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
  38. package/skill-assets/sunlint-code-quality/rules/ruby/C006-verb-noun-functions.md +63 -0
  39. package/skill-assets/sunlint-code-quality/rules/ruby/C013-no-dead-code.md +48 -0
  40. package/skill-assets/sunlint-code-quality/rules/ruby/C014-dependency-injection.md +42 -0
  41. package/skill-assets/sunlint-code-quality/rules/ruby/C017-no-constructor-logic.md +42 -0
  42. package/skill-assets/sunlint-code-quality/rules/ruby/C018-generic-errors.md +41 -0
  43. package/skill-assets/sunlint-code-quality/rules/ruby/C019-error-log-level.md +41 -0
  44. package/skill-assets/sunlint-code-quality/rules/ruby/C020-no-unused-imports.md +36 -0
  45. package/skill-assets/sunlint-code-quality/rules/ruby/C022-no-unused-variables.md +31 -0
  46. package/skill-assets/sunlint-code-quality/rules/ruby/C023-no-duplicate-names.md +39 -0
  47. package/skill-assets/sunlint-code-quality/rules/ruby/C024-centralize-constants.md +35 -0
  48. package/skill-assets/sunlint-code-quality/rules/ruby/C029-catch-log-root-cause.md +34 -0
  49. package/skill-assets/sunlint-code-quality/rules/ruby/C030-custom-error-classes.md +32 -0
  50. package/skill-assets/sunlint-code-quality/rules/ruby/C033-separate-data-access.md +52 -0
  51. package/skill-assets/sunlint-code-quality/rules/ruby/C035-error-context-logging.md +34 -0
  52. package/skill-assets/sunlint-code-quality/rules/ruby/C041-no-hardcoded-secrets.md +29 -0
  53. package/skill-assets/sunlint-code-quality/rules/ruby/C042-boolean-naming.md +38 -0
  54. package/skill-assets/sunlint-code-quality/rules/ruby/C052-controller-parsing.md +37 -0
  55. package/skill-assets/sunlint-code-quality/rules/ruby/C060-superclass-logic.md +38 -0
  56. package/skill-assets/sunlint-code-quality/rules/ruby/C067-no-hardcoded-config.md +37 -0
  57. package/skill-assets/sunlint-code-quality/rules/ruby/S003-open-redirect.md +58 -0
  58. package/skill-assets/sunlint-code-quality/rules/ruby/S004-no-log-credentials.md +38 -0
  59. package/skill-assets/sunlint-code-quality/rules/ruby/S005-server-authorization.md +37 -0
  60. package/skill-assets/sunlint-code-quality/rules/ruby/S006-default-credentials.md +29 -0
  61. package/skill-assets/sunlint-code-quality/rules/ruby/S007-output-encoding.md +31 -0
  62. package/skill-assets/sunlint-code-quality/rules/ruby/S009-approved-crypto.md +31 -0
  63. package/skill-assets/sunlint-code-quality/rules/ruby/S010-csprng.md +30 -0
  64. package/skill-assets/sunlint-code-quality/rules/ruby/S011-encrypted-client-hello.md +27 -0
  65. package/skill-assets/sunlint-code-quality/rules/ruby/S012-secrets-management.md +28 -0
  66. package/skill-assets/sunlint-code-quality/rules/ruby/S013-tls-connections.md +30 -0
  67. package/skill-assets/sunlint-code-quality/rules/ruby/S016-no-sensitive-query-string.md +37 -0
  68. package/skill-assets/sunlint-code-quality/rules/ruby/S017-parameterized-queries.md +33 -0
  69. package/skill-assets/sunlint-code-quality/rules/ruby/S019-email-input-sanitization.md +31 -0
  70. package/skill-assets/sunlint-code-quality/rules/ruby/S020-eval-code-execution.md +36 -0
  71. package/skill-assets/sunlint-code-quality/rules/ruby/S022-context-escaping.md +36 -0
  72. package/skill-assets/sunlint-code-quality/rules/ruby/S023-dynamic-js-encoding.md +33 -0
  73. package/skill-assets/sunlint-code-quality/rules/ruby/S025-server-validation.md +30 -0
  74. package/skill-assets/sunlint-code-quality/rules/ruby/S026-tls-encryption.md +30 -0
  75. package/skill-assets/sunlint-code-quality/rules/ruby/S027-mtls-validation.md +26 -0
  76. package/skill-assets/sunlint-code-quality/rules/ruby/S028-upload-limits.md +33 -0
  77. package/skill-assets/sunlint-code-quality/rules/ruby/S029-csrf-protection.md +32 -0
  78. package/skill-assets/sunlint-code-quality/rules/ruby/S030-directory-browsing.md +30 -0
  79. package/skill-assets/sunlint-code-quality/rules/ruby/S031-secure-cookie-flag.md +27 -0
  80. package/skill-assets/sunlint-code-quality/rules/ruby/S032-httponly-cookie.md +26 -0
  81. package/skill-assets/sunlint-code-quality/rules/ruby/S033-samesite-cookie.md +29 -0
  82. package/skill-assets/sunlint-code-quality/rules/ruby/S034-host-prefix-cookie.md +30 -0
  83. package/skill-assets/sunlint-code-quality/rules/ruby/S035-app-hostnames.md +28 -0
  84. package/skill-assets/sunlint-code-quality/rules/ruby/S036-internal-file-paths.md +37 -0
  85. package/skill-assets/sunlint-code-quality/rules/ruby/S037-anti-cache-headers.md +31 -0
  86. package/skill-assets/sunlint-code-quality/rules/ruby/S039-tls-certificate-validation.md +29 -0
  87. package/skill-assets/sunlint-code-quality/rules/ruby/S041-logout-invalidation.md +31 -0
  88. package/skill-assets/sunlint-code-quality/rules/ruby/S042-long-lived-sessions.md +27 -0
  89. package/skill-assets/sunlint-code-quality/rules/ruby/S044-critical-changes-reauth.md +34 -0
  90. package/skill-assets/sunlint-code-quality/rules/ruby/S045-brute-force-protection.md +33 -0
  91. package/skill-assets/sunlint-code-quality/rules/ruby/S047-oauth-csrf-protection.md +33 -0
  92. package/skill-assets/sunlint-code-quality/rules/ruby/S048-oauth-redirect-validation.md +29 -0
  93. package/skill-assets/sunlint-code-quality/rules/ruby/S049-auth-code-expiry.md +31 -0
  94. package/skill-assets/sunlint-code-quality/rules/ruby/S050-token-entropy.md +26 -0
  95. package/skill-assets/sunlint-code-quality/rules/ruby/S051-password-length.md +38 -0
  96. package/skill-assets/sunlint-code-quality/rules/ruby/S052-otp-entropy.md +25 -0
  97. package/skill-assets/sunlint-code-quality/rules/ruby/S053-generic-error-messages.md +33 -0
  98. package/skill-assets/sunlint-code-quality/rules/ruby/S054-no-default-admin.md +29 -0
  99. package/skill-assets/sunlint-code-quality/rules/ruby/S055-content-type-validation.md +24 -0
  100. package/skill-assets/sunlint-code-quality/rules/ruby/S056-log-injection.md +28 -0
  101. package/skill-assets/sunlint-code-quality/rules/ruby/S057-synchronized-time.md +18 -0
  102. package/skill-assets/sunlint-code-quality/rules/ruby/S058-ssrf-protection.md +39 -0
@@ -22,9 +22,10 @@ class ScoringService {
22
22
  //
23
23
  this.weights = {
24
24
  // Penalty per violation type (per KLOC)
25
- // Errors are 3x more severe than warnings
25
+ // Severity ratio: error(6) : warning(2) : info(0.5) = 12:4:1
26
26
  errorPenaltyPerKLOC: 6, // Each error per KLOC reduces score by 6 points
27
27
  warningPenaltyPerKLOC: 2, // Each warning per KLOC reduces score by 2 points
28
+ infoPenaltyPerKLOC: 0.5, // Each info per KLOC reduces score by 0.5 points
28
29
 
29
30
  // Absolute penalty thresholds (regardless of LOC)
30
31
  // Large projects should still be penalized for raw violation counts
@@ -57,11 +58,12 @@ class ScoringService {
57
58
  * @param {Object} params
58
59
  * @param {number} params.errorCount - Number of errors found
59
60
  * @param {number} params.warningCount - Number of warnings found
61
+ * @param {number} params.infoCount - Number of info violations found
60
62
  * @param {number} params.rulesChecked - Number of rules checked
61
63
  * @param {number} params.loc - Total lines of code
62
64
  * @returns {number} Score between 0-100
63
65
  */
64
- calculateScore({ errorCount = 0, warningCount = 0, rulesChecked = 0, loc = 0 }) {
66
+ calculateScore({ errorCount = 0, warningCount = 0, infoCount = 0, rulesChecked = 0, loc = 0 }) {
65
67
  // Base score starts at 100
66
68
  let score = 100;
67
69
 
@@ -72,12 +74,13 @@ class ScoringService {
72
74
  // Calculate violations per KLOC
73
75
  const errorsPerKLOC = errorCount / kloc;
74
76
  const warningsPerKLOC = warningCount / kloc;
75
- const totalViolationsPerKLOC = errorsPerKLOC + warningsPerKLOC;
77
+ const infosPerKLOC = infoCount / kloc;
76
78
 
77
79
  // 1. Density-based penalty (main scoring factor)
78
80
  // This penalizes based on how "dense" the violations are
79
81
  const densityPenalty = (errorsPerKLOC * this.weights.errorPenaltyPerKLOC) +
80
- (warningsPerKLOC * this.weights.warningPenaltyPerKLOC);
82
+ (warningsPerKLOC * this.weights.warningPenaltyPerKLOC) +
83
+ (infosPerKLOC * this.weights.infoPenaltyPerKLOC);
81
84
  score -= densityPenalty;
82
85
 
83
86
  // 2. Absolute penalty for projects with too many errors
@@ -195,6 +198,8 @@ class ScoringService {
195
198
  generateScoringSummary(params) {
196
199
  const score = this.calculateScore(params);
197
200
  const grade = this.getGrade(score);
201
+ const infoCount = params.infoCount || 0;
202
+ const totalViolations = params.errorCount + params.warningCount + infoCount;
198
203
 
199
204
  return {
200
205
  score,
@@ -202,10 +207,11 @@ class ScoringService {
202
207
  metrics: {
203
208
  errors: params.errorCount,
204
209
  warnings: params.warningCount,
210
+ infos: infoCount,
205
211
  rulesChecked: params.rulesChecked,
206
212
  linesOfCode: params.loc,
207
- violationsPerKLOC: params.loc > 0
208
- ? Math.round(((params.errorCount + params.warningCount) / params.loc * 1000) * 10) / 10
213
+ violationsPerKLOC: params.loc > 0
214
+ ? Math.round((totalViolations / params.loc * 1000) * 10) / 10
209
215
  : 0
210
216
  }
211
217
  };
@@ -247,11 +247,12 @@ class SummaryReportService {
247
247
  total_violations: violations.length,
248
248
  error_count: scoringSummary.metrics.errors,
249
249
  warning_count: scoringSummary.metrics.warnings,
250
- info_count: 0, // Reserved for future use
250
+ info_count: scoringSummary.metrics.infos || 0,
251
251
  lines_of_code: scoringSummary.metrics.linesOfCode,
252
252
  files_analyzed: options.filesAnalyzed || 0,
253
253
  sunlint_version: options.version || this.version,
254
254
  analysis_duration_ms: options.duration || 0,
255
+ scoring_mode: options.scoringMode || 'project',
255
256
  violations: violationsSummary,
256
257
 
257
258
  // Additional metadata for backwards compatibility
@@ -259,7 +260,8 @@ class SummaryReportService {
259
260
  generated_at: new Date().toISOString(),
260
261
  tool: 'SunLint',
261
262
  version: options.version || this.version,
262
- analysis_duration_ms: options.duration || 0
263
+ analysis_duration_ms: options.duration || 0,
264
+ scoring_mode: options.scoringMode || 'project'
263
265
  },
264
266
  quality: {
265
267
  score: scoringSummary.score,
@@ -384,15 +386,18 @@ class SummaryReportService {
384
386
  const totalViolations = summaryReport.total_violations || 0;
385
387
  const errorCount = summaryReport.error_count || 0;
386
388
  const warningCount = summaryReport.warning_count || 0;
389
+ const infoCount = summaryReport.info_count || 0;
387
390
  const violationsByRule = summaryReport.violations || [];
388
391
  const violationsPerKLOC = summaryReport.quality?.metrics?.violationsPerKLOC || 0;
392
+ const scoringMode = summaryReport.scoring_mode || 'project';
389
393
 
390
394
  let output = '\n📊 Quality Summary Report\n';
391
395
  output += '━'.repeat(50) + '\n';
392
- output += `📈 Quality Score: ${score} (Grade: ${grade})\n`;
396
+ output += `📈 Quality Score: ${score} (Grade: ${grade})`;
397
+ output += scoringMode === 'pr' ? ' [PR mode]\n' : '\n';
393
398
  output += `📁 Files Analyzed: ${filesAnalyzed}\n`;
394
399
  output += `📏 Lines of Code: ${linesOfCode.toLocaleString()}\n`;
395
- output += `⚠️ Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings)\n`;
400
+ output += `⚠️ Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings, ${infoCount} info)\n`;
396
401
  output += `📊 Violations per KLOC: ${violationsPerKLOC}\n`;
397
402
 
398
403
  if (violationsByRule.length > 0) {
@@ -1002,7 +1002,12 @@ class HeuristicEngine extends AnalysisEngineInterface {
1002
1002
  fileResult = { file: filePath, violations: [] };
1003
1003
  results.results.push(fileResult);
1004
1004
  }
1005
- fileResult.violations.push(...violations);
1005
+ // Apply rule severity to each violation (from config)
1006
+ const violationsWithSeverity = violations.map(v => ({
1007
+ ...v,
1008
+ severity: rule.severity || v.severity || 'warning'
1009
+ }));
1010
+ fileResult.violations.push(...violationsWithSeverity);
1006
1011
  }
1007
1012
  }
1008
1013
 
@@ -50,57 +50,72 @@ DESCRIPTION:
50
50
  components in TypeScript/JavaScript projects.
51
51
 
52
52
  OPTIONS:
53
- --input=<path> Source directory to analyze
54
- Default: src
55
53
 
56
- --base=<ref> Base git reference for comparison
57
- Default: HEAD~1 (previous commit)
58
- Examples: origin/main, HEAD~5, abc123
54
+ Input & Source:
55
+ --input=<path> Source directory to analyze (default: src)
56
+
57
+ Git References:
58
+ --base=<ref> Base git reference (default: HEAD~1)
59
+ --head=<ref> Head git reference (default: current working dir)
60
+
61
+ Exclusions:
62
+ --exclude=<paths> Comma-separated paths to exclude
63
+ Example: --exclude=test,examples,mocks
64
+ --use-gitignore Use .gitignore patterns for exclusions (recommended)
65
+ This automatically excludes files ignored by git
66
+
67
+ Analysis Options:
68
+ --max-depth=<n> Maximum call chain depth (default: 3)
69
+ --include-tests Include test files in analysis
70
+ --verbose Show detailed analysis output
71
+
72
+ Output:
73
+ --format=<type> Output format: markdown, json, csv (default: markdown)
74
+ --output=<file> Output file path (default: impact-report.md)
75
+
76
+ Other:
77
+ --help Show this help message
59
78
 
60
- --head=<ref> Head git reference (optional)
61
- Default: (working directory)
62
-
63
- --exclude=<paths> Comma-separated paths to exclude
64
- Default: node_modules,dist,build,specs,coverage
65
-
66
- --output=<file> Output markdown report file
67
- Default: impact-report.md
68
-
69
- --json=<file> Output JSON report file (optional)
70
- Example: --json=impact-report.json
71
-
72
- --max-depth=<n> Maximum call graph depth
73
- Default: 3
74
-
75
- --include-tests Include test files in analysis
79
+ EXAMPLES:
76
80
 
77
- --verbose Show verbose output and stack traces
81
+ Basic usage (analyze changes from last commit):
82
+ $ impact-analyzer --input=src --verbose
78
83
 
79
- --no-fail Don't exit with error on critical impact
84
+ Use .gitignore patterns (recommended for large projects):
85
+ $ impact-analyzer --input=src --use-gitignore --verbose
80
86
 
81
- --help, -h Show this help message
87
+ Analyze specific git range:
88
+ $ impact-analyzer --base=main --head=feature-branch
82
89
 
83
- EXAMPLES:
84
- # Analyze changes in last commit (default)
85
- impact-analyzer
90
+ Custom exclusions:
91
+ $ impact-analyzer --exclude=test,examples,fixtures
86
92
 
87
- # Analyze changes between main branch and current
88
- impact-analyzer --base=origin/main
93
+ Include tests in analysis:
94
+ $ impact-analyzer --include-tests
89
95
 
90
- # Analyze specific directory with JSON output
91
- impact-analyzer --input=backend/src --json=report.json
96
+ Generate JSON report:
97
+ $ impact-analyzer --format=json --output=impact.json
92
98
 
93
- # Analyze last 3 commits with verbose output
94
- impact-analyzer --base=HEAD~3 --verbose
99
+ PERFORMANCE TIPS:
95
100
 
96
- # Compare two specific commits
97
- impact-analyzer --base=abc123 --head=def456
101
+ Use --use-gitignore for automatic exclusion of unnecessary files
102
+ This can reduce analysis time by 60-90% on large projects!
103
+
104
+ ⚡ Exclude test/example directories if not needed:
105
+ --exclude=test,tests,examples,samples,mocks
106
+
107
+ ⚡ For large codebases, limit max-depth:
108
+ --max-depth=2
98
109
 
99
- REFERENCE:
100
- Architecture: .specify/plans/architecture.md
101
- Coding Rules: .github/copilot-instructions.md
110
+ DEFAULT EXCLUSIONS (when not using --use-gitignore):
102
111
 
103
- For more information, visit: https://github.com/sun-asterisk/impact-analyzer
112
+ node_modules, dist, build, out
113
+ • test files: *.test.ts, *.spec.ts
114
+ • examples, samples, fixtures, mocks
115
+ • coverage, reports
116
+ • .cache, .temp, .history
117
+ • IDE: .idea, .vscode
118
+ • Type definitions: *.d.ts
104
119
  `);
105
120
  }
106
121
  }
@@ -3,15 +3,49 @@
3
3
  * Loads and validates configuration from CLI args and defaults
4
4
  */
5
5
 
6
+ import { GitignoreParser, gitignoreToExcludePaths } from '../core/utils/gitignore-parser.js';
7
+ import path from 'path';
8
+
6
9
  export function loadConfig(cli) {
10
+ const sourceDir = cli.getArg('input', 'src');
11
+ const rootDir = path.resolve(sourceDir, '..');
12
+
13
+ // Load patterns from .gitignore
14
+ const gitignorePatterns = cli.hasArg('use-gitignore')
15
+ ? gitignoreToExcludePaths(rootDir)
16
+ : [];
17
+
18
+ // Custom exclude paths from CLI
19
+ const customExcludes = cli.getArg('exclude', '')
20
+ .split(',')
21
+ .map(p => p.trim())
22
+ .filter(p => p);
23
+
24
+ // Merge: gitignore + custom + defaults
25
+ const allExcludes = [
26
+ ...new Set([
27
+ ...gitignorePatterns,
28
+ ...customExcludes,
29
+ ...DEFAULT_EXCLUDE_PATHS,
30
+ ])
31
+ ];
32
+
7
33
  const config = {
8
34
  // Source directory to analyze
9
- sourceDir: cli.getArg('input', 'src'),
35
+ sourceDir: sourceDir,
36
+ rootDir: rootDir,
10
37
 
11
38
  // Paths to exclude from analysis
12
- excludePaths: cli.getArg('exclude', 'node_modules,dist,build,specs,coverage')
13
- .split(',')
14
- .map(p => p.trim()),
39
+ excludePaths: allExcludes,
40
+
41
+ // Gitignore parser for advanced pattern matching
42
+ gitignoreParser: cli.hasArg('use-gitignore')
43
+ ? (() => {
44
+ const parser = new GitignoreParser(rootDir);
45
+ parser.loadGitignore();
46
+ return parser;
47
+ })()
48
+ : null,
15
49
 
16
50
  // Git references - defaults to HEAD~1 (previous commit)
17
51
  baseRef: cli.getArg('base', 'HEAD~1'),
@@ -41,9 +75,75 @@ function validateConfig(config) {
41
75
  // baseRef and headRef are optional, have sensible defaults
42
76
  }
43
77
 
78
+ // Default exclude paths - comprehensive list
79
+ const DEFAULT_EXCLUDE_PATHS = [
80
+ // Build & Dependencies
81
+ 'node_modules',
82
+ 'dist',
83
+ 'build',
84
+ 'out',
85
+ '.turbo',
86
+ '.next',
87
+
88
+ // Tests (unless --include-tests)
89
+ '__tests__',
90
+ 'test',
91
+ 'tests',
92
+ '.test.ts',
93
+ '.spec.ts',
94
+ '.test.js',
95
+ '.spec.js',
96
+ '.test.tsx',
97
+ '.spec.tsx',
98
+
99
+ // Examples & Samples
100
+ 'examples',
101
+ 'samples',
102
+ 'fixtures',
103
+ 'mocks',
104
+ '__mocks__',
105
+ 'project-samples',
106
+
107
+ // Coverage & Reports
108
+ 'coverage',
109
+ 'reports',
110
+ '.nyc_output',
111
+
112
+ // Cache & Temp
113
+ '.cache',
114
+ '.temp',
115
+ '.tmp',
116
+ '.history',
117
+
118
+ // Documentation Sites
119
+ 'pages/_site',
120
+ '_site',
121
+ '.jekyll-cache',
122
+ '.jekyll-metadata',
123
+ 'vendor',
124
+ '.bundle',
125
+
126
+ // IDE & Tools
127
+ '.idea',
128
+ '.vscode',
129
+ '.github',
130
+ '.agent',
131
+ '.claude',
132
+ '.serena',
133
+
134
+ // Config-specific
135
+ 'specs',
136
+ '.sunlint-cache',
137
+ 'origin-rules',
138
+ 'arch-detect',
139
+
140
+ // Type definitions (no runtime code to analyze)
141
+ '.d.ts',
142
+ ];
143
+
44
144
  export const DEFAULT_CONFIG = {
45
145
  sourceDir: 'src',
46
- excludePaths: ['node_modules', 'dist', 'build', 'specs', 'coverage'],
146
+ excludePaths: DEFAULT_EXCLUDE_PATHS,
47
147
  baseRef: 'HEAD~1', // Previous commit
48
148
  headRef: 'HEAD', // Current commit
49
149
  maxDepth: 3,
@@ -20,7 +20,8 @@ export class ImpactAnalyzer {
20
20
  await this.methodCallGraph.initialize(
21
21
  this.absoluteSourceDir,
22
22
  this.config.excludePaths,
23
- this.config.verbose
23
+ this.config.verbose,
24
+ this.config.gitignoreParser || null
24
25
  );
25
26
 
26
27
  this.endpointDetector = new EndpointDetector(this.methodCallGraph, this.config);
@@ -69,28 +70,24 @@ export class ImpactAnalyzer {
69
70
 
70
71
  detectLogicImpact(changedSymbols) {
71
72
  const directCallers = new Set();
72
- const indirectCallers = new Set();
73
+ const allCallers = new Set();
73
74
 
74
75
  for (const symbol of changedSymbols) {
75
76
  const methodName = `${symbol.className || 'global'}.${symbol.name}`;
76
- const callers = this.methodCallGraph.findAllCallers(methodName);
77
77
 
78
- callers.forEach(caller => {
79
- directCallers.add(caller);
80
-
81
- const indirectCallersOfCaller = this.methodCallGraph.findAllCallers(caller);
82
- indirectCallersOfCaller.forEach(ic => {
83
- if (!callers.includes(ic)) {
84
- indirectCallers.add(ic);
85
- }
86
- });
87
- });
78
+ const immediateCallers = this.methodCallGraph.getCallers(methodName);
79
+ immediateCallers.forEach(caller => directCallers.add(caller));
80
+
81
+ const recursiveCallers = this.methodCallGraph.findAllCallers(methodName);
82
+ recursiveCallers.forEach(caller => allCallers.add(caller));
88
83
  }
89
84
 
85
+ const indirectCallers = Array.from(allCallers).filter(caller => !directCallers.has(caller));
86
+
90
87
  return {
91
88
  directCallers: Array.from(directCallers),
92
- indirectCallers: Array.from(indirectCallers),
93
- riskLevel: this.calculateRiskLevel(directCallers.size, indirectCallers.size),
89
+ indirectCallers: indirectCallers,
90
+ riskLevel: this.calculateRiskLevel(directCallers.size, indirectCallers.length),
94
91
  };
95
92
  }
96
93
 
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Gitignore Parser
3
+ * Reads .gitignore files and converts patterns to exclude paths
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { minimatch } from 'minimatch';
9
+
10
+ export class GitignoreParser {
11
+ constructor(rootDir) {
12
+ this.rootDir = rootDir;
13
+ this.patterns = [];
14
+ this.negationPatterns = [];
15
+ }
16
+
17
+ /**
18
+ * Load patterns from .gitignore file
19
+ */
20
+ loadGitignore(gitignorePath = null) {
21
+ const ignoreFile = gitignorePath || path.join(this.rootDir, '.gitignore');
22
+
23
+ if (!fs.existsSync(ignoreFile)) {
24
+ return [];
25
+ }
26
+
27
+ const content = fs.readFileSync(ignoreFile, 'utf8');
28
+ const lines = content.split('\n');
29
+
30
+ const patterns = [];
31
+
32
+ for (const line of lines) {
33
+ const trimmed = line.trim();
34
+
35
+ // Skip empty lines and comments
36
+ if (!trimmed || trimmed.startsWith('#')) continue;
37
+
38
+ // Handle negation patterns (!)
39
+ if (trimmed.startsWith('!')) {
40
+ this.negationPatterns.push(trimmed.substring(1));
41
+ continue;
42
+ }
43
+
44
+ patterns.push(trimmed);
45
+ }
46
+
47
+ this.patterns = patterns;
48
+ return patterns;
49
+ }
50
+
51
+ /**
52
+ * Check if a file path should be excluded based on gitignore patterns
53
+ */
54
+ shouldExclude(filePath) {
55
+ // Normalize path separators
56
+ const normalizedPath = filePath.replace(/\\/g, '/');
57
+
58
+ // Check if path matches any pattern
59
+ for (const pattern of this.patterns) {
60
+ if (this.matchesPattern(normalizedPath, pattern)) {
61
+ // Check if there's a negation pattern that overrides
62
+ const isNegated = this.negationPatterns.some(negPattern =>
63
+ this.matchesPattern(normalizedPath, negPattern)
64
+ );
65
+
66
+ if (!isNegated) {
67
+ return true;
68
+ }
69
+ }
70
+ }
71
+
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * Match a file path against a gitignore pattern
77
+ */
78
+ matchesPattern(filePath, pattern) {
79
+ // Handle directory-only patterns (ending with /)
80
+ if (pattern.endsWith('/')) {
81
+ const dirPattern = pattern.slice(0, -1);
82
+ return filePath.includes(`/${dirPattern}/`) || filePath.includes(`${dirPattern}/`);
83
+ }
84
+
85
+ // Handle patterns starting with /
86
+ if (pattern.startsWith('/')) {
87
+ return minimatch(filePath, pattern.substring(1), { matchBase: true });
88
+ }
89
+
90
+ // Handle wildcards and glob patterns
91
+ if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) {
92
+ return minimatch(filePath, pattern, { matchBase: true, dot: true });
93
+ }
94
+
95
+ // Simple substring match for paths
96
+ return filePath.includes(pattern) ||
97
+ filePath.includes(`/${pattern}/`) ||
98
+ filePath.endsWith(`/${pattern}`);
99
+ }
100
+
101
+ /**
102
+ * Get all patterns
103
+ */
104
+ getPatterns() {
105
+ return this.patterns;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Convert gitignore patterns to simple exclude path list
111
+ * for backward compatibility
112
+ */
113
+ export function gitignoreToExcludePaths(rootDir) {
114
+ const parser = new GitignoreParser(rootDir);
115
+ parser.loadGitignore();
116
+
117
+ // Extract simple path patterns (non-glob)
118
+ const simplePaths = parser.getPatterns().filter(p =>
119
+ !p.includes('*') && !p.includes('?') && !p.includes('[')
120
+ );
121
+
122
+ return simplePaths.map(p => p.replace(/^\//, '').replace(/\/$/, ''));
123
+ }