@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.
- package/config/rules/rules-registry-generated.json +1717 -282
- package/core/adapters/sunlint-rule-adapter.js +16 -0
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/output-service.js +12 -3
- package/core/rule-selection-service.js +24 -3
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- package/engines/heuristic-engine.js +6 -1
- package/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +2 -2
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C006-verb-noun-functions.md +63 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C013-no-dead-code.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C014-dependency-injection.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C017-no-constructor-logic.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C018-generic-errors.md +41 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C019-error-log-level.md +41 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C020-no-unused-imports.md +36 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C022-no-unused-variables.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C023-no-duplicate-names.md +39 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C024-centralize-constants.md +35 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C029-catch-log-root-cause.md +34 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C030-custom-error-classes.md +32 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C033-separate-data-access.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C035-error-context-logging.md +34 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C041-no-hardcoded-secrets.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C042-boolean-naming.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C052-controller-parsing.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C060-superclass-logic.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C067-no-hardcoded-config.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S003-open-redirect.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S004-no-log-credentials.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S005-server-authorization.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S006-default-credentials.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S007-output-encoding.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S009-approved-crypto.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S010-csprng.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S011-encrypted-client-hello.md +27 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S012-secrets-management.md +28 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S013-tls-connections.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S016-no-sensitive-query-string.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S017-parameterized-queries.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S019-email-input-sanitization.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S020-eval-code-execution.md +36 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S022-context-escaping.md +36 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S023-dynamic-js-encoding.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S025-server-validation.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S026-tls-encryption.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S027-mtls-validation.md +26 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S028-upload-limits.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S029-csrf-protection.md +32 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S030-directory-browsing.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S031-secure-cookie-flag.md +27 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S032-httponly-cookie.md +26 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S033-samesite-cookie.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S034-host-prefix-cookie.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S035-app-hostnames.md +28 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S036-internal-file-paths.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S037-anti-cache-headers.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S039-tls-certificate-validation.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S041-logout-invalidation.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S042-long-lived-sessions.md +27 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S044-critical-changes-reauth.md +34 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S045-brute-force-protection.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S047-oauth-csrf-protection.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S048-oauth-redirect-validation.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S049-auth-code-expiry.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S050-token-entropy.md +26 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S051-password-length.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S052-otp-entropy.md +25 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S053-generic-error-messages.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S054-no-default-admin.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S055-content-type-validation.md +24 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S056-log-injection.md +28 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S057-synchronized-time.md +18 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S058-ssrf-protection.md +39 -0
package/core/scoring-service.js
CHANGED
|
@@ -22,9 +22,10 @@ class ScoringService {
|
|
|
22
22
|
//
|
|
23
23
|
this.weights = {
|
|
24
24
|
// Penalty per violation type (per KLOC)
|
|
25
|
-
//
|
|
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
|
|
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((
|
|
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,
|
|
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})
|
|
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
|
-
|
|
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
|
|
package/engines/impact/cli.js
CHANGED
|
@@ -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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
Basic usage (analyze changes from last commit):
|
|
82
|
+
$ impact-analyzer --input=src --verbose
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
Use .gitignore patterns (recommended for large projects):
|
|
85
|
+
$ impact-analyzer --input=src --use-gitignore --verbose
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
Analyze specific git range:
|
|
88
|
+
$ impact-analyzer --base=main --head=feature-branch
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
impact-analyzer
|
|
90
|
+
Custom exclusions:
|
|
91
|
+
$ impact-analyzer --exclude=test,examples,fixtures
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
Include tests in analysis:
|
|
94
|
+
$ impact-analyzer --include-tests
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
Generate JSON report:
|
|
97
|
+
$ impact-analyzer --format=json --output=impact.json
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
impact-analyzer --base=HEAD~3 --verbose
|
|
99
|
+
PERFORMANCE TIPS:
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
Architecture: .specify/plans/architecture.md
|
|
101
|
-
Coding Rules: .github/copilot-instructions.md
|
|
110
|
+
DEFAULT EXCLUSIONS (when not using --use-gitignore):
|
|
102
111
|
|
|
103
|
-
|
|
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:
|
|
35
|
+
sourceDir: sourceDir,
|
|
36
|
+
rootDir: rootDir,
|
|
10
37
|
|
|
11
38
|
// Paths to exclude from analysis
|
|
12
|
-
excludePaths:
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
93
|
-
riskLevel: this.calculateRiskLevel(directCallers.size, indirectCallers.
|
|
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
|
+
}
|