@sun-asterisk/sunlint 1.3.11 → 1.3.12
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/CHANGELOG.md +12 -4
- package/core/output-service.js +1 -1
- package/core/summary-report-service.js +152 -22
- package/docs/API_FORMAT_GUIDE.md +370 -0
- package/docs/QUALITY_SCORING_GUIDE.md +19 -7
- package/package.json +1 -1
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +21 -22
- package/docs/AI.md +0 -163
package/CHANGELOG.md
CHANGED
|
@@ -18,12 +18,20 @@
|
|
|
18
18
|
- **Bonus System**: Up to 10 points bonus for comprehensive rule coverage
|
|
19
19
|
|
|
20
20
|
#### **Summary Report Generation** 📊
|
|
21
|
-
- **NEW**: `--output-summary <file>` option for JSON summary reports
|
|
22
|
-
- **Format**:
|
|
23
|
-
- **Git Integration**: Auto-detects repository
|
|
24
|
-
-
|
|
21
|
+
- **NEW**: `--output-summary <file>` option for API-compatible JSON summary reports
|
|
22
|
+
- **Format**: Direct dashboard API compatibility - **no transformation needed!**
|
|
23
|
+
- **Git Integration**: Auto-detects complete repository and commit information
|
|
24
|
+
- Repository: URL, name, project path (mono-repo support)
|
|
25
|
+
- Commit: hash, message, author name, author email
|
|
26
|
+
- PR tracking: Automatic PR number extraction from commit messages/branch names
|
|
27
|
+
- **Environment Variables**: Full GitHub Actions support
|
|
28
|
+
- `GITHUB_REPOSITORY`, `GITHUB_REF_NAME`, `GITHUB_SHA`
|
|
29
|
+
- `GITHUB_EVENT_PATH` for commit details
|
|
30
|
+
- Automatic fallback to git commands if env vars unavailable
|
|
31
|
+
- **Mono-Repo Support**: Automatic `project_path` detection for multi-project repositories
|
|
25
32
|
- **Violation Summary**: Aggregated violations by rule with count and severity
|
|
26
33
|
- **Metrics**: LOC, files analyzed, violations per KLOC, errors/warnings breakdown
|
|
34
|
+
- **API-Ready**: Flat structure format ready for direct POST to dashboard APIs
|
|
27
35
|
|
|
28
36
|
#### **New Services** 🛠️
|
|
29
37
|
- **ScoringService**: Calculate quality scores with customizable weights
|
package/core/output-service.js
CHANGED
|
@@ -14,13 +14,19 @@ class SummaryReportService {
|
|
|
14
14
|
/**
|
|
15
15
|
* Get Git repository information
|
|
16
16
|
* @param {string} cwd - Working directory
|
|
17
|
-
* @returns {Object} Git info including repository URL, branch, and commit
|
|
17
|
+
* @returns {Object} Git info including repository URL, branch, commit hash, and commit details
|
|
18
18
|
*/
|
|
19
19
|
getGitInfo(cwd = process.cwd()) {
|
|
20
20
|
const gitInfo = {
|
|
21
21
|
repository_url: null,
|
|
22
|
+
repository_name: null,
|
|
23
|
+
project_path: null,
|
|
22
24
|
branch: null,
|
|
23
|
-
commit_hash: null
|
|
25
|
+
commit_hash: null,
|
|
26
|
+
commit_message: null,
|
|
27
|
+
author_email: null,
|
|
28
|
+
author_name: null,
|
|
29
|
+
pr_number: null
|
|
24
30
|
};
|
|
25
31
|
|
|
26
32
|
try {
|
|
@@ -39,6 +45,12 @@ class SummaryReportService {
|
|
|
39
45
|
} else {
|
|
40
46
|
gitInfo.repository_url = remoteUrl.replace('.git', '');
|
|
41
47
|
}
|
|
48
|
+
|
|
49
|
+
// Extract repository name from URL
|
|
50
|
+
const urlMatch = gitInfo.repository_url.match(/\/([^\/]+)$/);
|
|
51
|
+
if (urlMatch) {
|
|
52
|
+
gitInfo.repository_name = urlMatch[1];
|
|
53
|
+
}
|
|
42
54
|
} catch (error) {
|
|
43
55
|
// No remote configured
|
|
44
56
|
}
|
|
@@ -56,6 +68,55 @@ class SummaryReportService {
|
|
|
56
68
|
} catch (error) {
|
|
57
69
|
// Can't get commit hash
|
|
58
70
|
}
|
|
71
|
+
|
|
72
|
+
// Get commit message
|
|
73
|
+
try {
|
|
74
|
+
gitInfo.commit_message = execSync('git log -1 --pretty=%B', { cwd, encoding: 'utf8' }).trim();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Can't get commit message
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get author email
|
|
80
|
+
try {
|
|
81
|
+
gitInfo.author_email = execSync('git log -1 --pretty=%ae', { cwd, encoding: 'utf8' }).trim();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Can't get author email
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get author name
|
|
87
|
+
try {
|
|
88
|
+
gitInfo.author_name = execSync('git log -1 --pretty=%an', { cwd, encoding: 'utf8' }).trim();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Can't get author name
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get project path (relative to repo root for mono-repo)
|
|
94
|
+
try {
|
|
95
|
+
const repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
|
|
96
|
+
if (cwd !== repoRoot) {
|
|
97
|
+
gitInfo.project_path = path.relative(repoRoot, cwd);
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Can't determine project path
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Try to extract PR number from commit message or branch name
|
|
104
|
+
try {
|
|
105
|
+
// Check commit message for PR patterns like "#123" or "pull request #123"
|
|
106
|
+
const commitMsg = gitInfo.commit_message || '';
|
|
107
|
+
const prMatch = commitMsg.match(/#(\d+)/);
|
|
108
|
+
if (prMatch) {
|
|
109
|
+
gitInfo.pr_number = parseInt(prMatch[1]);
|
|
110
|
+
} else {
|
|
111
|
+
// Check branch name for PR patterns
|
|
112
|
+
const branchMatch = gitInfo.branch.match(/(?:pr|pull)[/-]?(\d+)/i);
|
|
113
|
+
if (branchMatch) {
|
|
114
|
+
gitInfo.pr_number = parseInt(branchMatch[1]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// Can't extract PR number
|
|
119
|
+
}
|
|
59
120
|
} catch (error) {
|
|
60
121
|
// Not a git repository - return nulls
|
|
61
122
|
}
|
|
@@ -79,8 +140,42 @@ class SummaryReportService {
|
|
|
79
140
|
? `https://github.com/${process.env.GITHUB_REPOSITORY}`
|
|
80
141
|
: gitInfo.repository_url;
|
|
81
142
|
|
|
143
|
+
const repository_name = process.env.GITHUB_REPOSITORY
|
|
144
|
+
? process.env.GITHUB_REPOSITORY.split('/')[1]
|
|
145
|
+
: (gitInfo.repository_name || null);
|
|
146
|
+
|
|
82
147
|
const branch = process.env.GITHUB_REF_NAME || gitInfo.branch;
|
|
83
148
|
const commit_hash = process.env.GITHUB_SHA || gitInfo.commit_hash;
|
|
149
|
+
|
|
150
|
+
// Get commit details from GitHub context or git
|
|
151
|
+
const commit_message = process.env.GITHUB_EVENT_HEAD_COMMIT_MESSAGE
|
|
152
|
+
|| (process.env.GITHUB_EVENT_PATH
|
|
153
|
+
? this._getGitHubEventData('head_commit.message')
|
|
154
|
+
: null)
|
|
155
|
+
|| gitInfo.commit_message;
|
|
156
|
+
|
|
157
|
+
const author_email = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_EMAIL
|
|
158
|
+
|| (process.env.GITHUB_EVENT_PATH
|
|
159
|
+
? this._getGitHubEventData('head_commit.author.email')
|
|
160
|
+
: null)
|
|
161
|
+
|| gitInfo.author_email;
|
|
162
|
+
|
|
163
|
+
const author_name = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_NAME
|
|
164
|
+
|| (process.env.GITHUB_EVENT_PATH
|
|
165
|
+
? this._getGitHubEventData('head_commit.author.name')
|
|
166
|
+
: null)
|
|
167
|
+
|| gitInfo.author_name;
|
|
168
|
+
|
|
169
|
+
// Get PR number from GitHub event or git
|
|
170
|
+
let pr_number = null;
|
|
171
|
+
if (process.env.GITHUB_EVENT_PATH) {
|
|
172
|
+
pr_number = this._getGitHubEventData('pull_request.number')
|
|
173
|
+
|| this._getGitHubEventData('number');
|
|
174
|
+
}
|
|
175
|
+
pr_number = pr_number || gitInfo.pr_number;
|
|
176
|
+
|
|
177
|
+
// Get project path (for mono-repo support)
|
|
178
|
+
const project_path = gitInfo.project_path;
|
|
84
179
|
|
|
85
180
|
// Count violations by rule
|
|
86
181
|
const violationsByRule = {};
|
|
@@ -100,42 +195,77 @@ class SummaryReportService {
|
|
|
100
195
|
const violationsSummary = Object.values(violationsByRule)
|
|
101
196
|
.sort((a, b) => b.count - a.count);
|
|
102
197
|
|
|
103
|
-
// Build the summary report
|
|
198
|
+
// Build the summary report compatible with coding-standards-report API
|
|
104
199
|
const summaryReport = {
|
|
200
|
+
// API-compatible fields (flat structure)
|
|
201
|
+
repository_url,
|
|
202
|
+
repository_name,
|
|
203
|
+
project_path,
|
|
204
|
+
branch,
|
|
205
|
+
commit_hash,
|
|
206
|
+
commit_message,
|
|
207
|
+
author_email,
|
|
208
|
+
author_name,
|
|
209
|
+
pr_number,
|
|
210
|
+
score: scoringSummary.score,
|
|
211
|
+
total_violations: violations.length,
|
|
212
|
+
error_count: scoringSummary.metrics.errors,
|
|
213
|
+
warning_count: scoringSummary.metrics.warnings,
|
|
214
|
+
info_count: 0, // Reserved for future use
|
|
215
|
+
lines_of_code: scoringSummary.metrics.linesOfCode,
|
|
216
|
+
files_analyzed: options.filesAnalyzed || 0,
|
|
217
|
+
sunlint_version: options.version || '1.3.12',
|
|
218
|
+
analysis_duration_ms: options.duration || 0,
|
|
219
|
+
violations: violationsSummary,
|
|
220
|
+
|
|
221
|
+
// Additional metadata for backwards compatibility
|
|
105
222
|
metadata: {
|
|
106
223
|
generated_at: new Date().toISOString(),
|
|
107
224
|
tool: 'SunLint',
|
|
108
|
-
version: options.version || '1.3.
|
|
225
|
+
version: options.version || '1.3.12',
|
|
109
226
|
analysis_duration_ms: options.duration || 0
|
|
110
227
|
},
|
|
111
|
-
repository: {
|
|
112
|
-
repository_url,
|
|
113
|
-
branch,
|
|
114
|
-
commit_hash
|
|
115
|
-
},
|
|
116
228
|
quality: {
|
|
117
229
|
score: scoringSummary.score,
|
|
118
230
|
grade: scoringSummary.grade,
|
|
119
231
|
metrics: scoringSummary.metrics
|
|
120
|
-
},
|
|
121
|
-
violations: {
|
|
122
|
-
total: violations.length,
|
|
123
|
-
by_severity: {
|
|
124
|
-
errors: scoringSummary.metrics.errors,
|
|
125
|
-
warnings: scoringSummary.metrics.warnings
|
|
126
|
-
},
|
|
127
|
-
by_rule: violationsSummary
|
|
128
|
-
},
|
|
129
|
-
analysis: {
|
|
130
|
-
files_analyzed: options.filesAnalyzed || 0,
|
|
131
|
-
rules_checked: scoringSummary.metrics.rulesChecked,
|
|
132
|
-
lines_of_code: scoringSummary.metrics.linesOfCode
|
|
133
232
|
}
|
|
134
233
|
};
|
|
135
234
|
|
|
136
235
|
return summaryReport;
|
|
137
236
|
}
|
|
138
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Helper method to extract data from GitHub event JSON
|
|
240
|
+
* @param {string} path - Dot-notation path to the data (e.g., 'head_commit.message')
|
|
241
|
+
* @returns {any} Extracted value or null
|
|
242
|
+
* @private
|
|
243
|
+
*/
|
|
244
|
+
_getGitHubEventData(path) {
|
|
245
|
+
try {
|
|
246
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
247
|
+
if (!eventPath || !fs.existsSync(eventPath)) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
|
252
|
+
const keys = path.split('.');
|
|
253
|
+
let value = eventData;
|
|
254
|
+
|
|
255
|
+
for (const key of keys) {
|
|
256
|
+
if (value && typeof value === 'object' && key in value) {
|
|
257
|
+
value = value[key];
|
|
258
|
+
} else {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return value;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
139
269
|
/**
|
|
140
270
|
* Generate summary report and save to file
|
|
141
271
|
* @param {Array} violations - Array of violations
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# API-Compatible Summary Report Format
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
SunLint v1.4.0+ generates summary reports in a format that's **directly compatible** with dashboard APIs, eliminating the need for manual transformation in CI/CD pipelines.
|
|
6
|
+
|
|
7
|
+
## Output Format
|
|
8
|
+
|
|
9
|
+
When using `--output-summary=<file>`, SunLint generates a JSON file with the following structure:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"repository_url": "https://github.com/sun-asterisk/engineer-excellence",
|
|
14
|
+
"repository_name": "engineer-excellence",
|
|
15
|
+
"project_path": "coding-quality/extensions/sunlint",
|
|
16
|
+
"branch": "main",
|
|
17
|
+
"commit_hash": "abc123def456",
|
|
18
|
+
"commit_message": "Merge pull request #123\n\nFix: Update quality scoring",
|
|
19
|
+
"author_email": "user@example.com",
|
|
20
|
+
"author_name": "John Doe",
|
|
21
|
+
"pr_number": 123,
|
|
22
|
+
"score": 92.6,
|
|
23
|
+
"total_violations": 39,
|
|
24
|
+
"error_count": 0,
|
|
25
|
+
"warning_count": 39,
|
|
26
|
+
"info_count": 0,
|
|
27
|
+
"lines_of_code": 4954,
|
|
28
|
+
"files_analyzed": 22,
|
|
29
|
+
"sunlint_version": "1.3.9",
|
|
30
|
+
"analysis_duration_ms": 1464,
|
|
31
|
+
"violations": [
|
|
32
|
+
{
|
|
33
|
+
"rule_code": "C065",
|
|
34
|
+
"count": 39,
|
|
35
|
+
"severity": "warning"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"metadata": {
|
|
39
|
+
"generated_at": "2025-10-09T06:59:57.680Z",
|
|
40
|
+
"tool": "SunLint",
|
|
41
|
+
"version": "1.3.9",
|
|
42
|
+
"analysis_duration_ms": 1464
|
|
43
|
+
},
|
|
44
|
+
"quality": {
|
|
45
|
+
"score": 92.6,
|
|
46
|
+
"grade": "A",
|
|
47
|
+
"metrics": {
|
|
48
|
+
"errors": 0,
|
|
49
|
+
"warnings": 39,
|
|
50
|
+
"rulesChecked": 1,
|
|
51
|
+
"linesOfCode": 4954,
|
|
52
|
+
"violationsPerKLOC": 7.9
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Field Descriptions
|
|
59
|
+
|
|
60
|
+
### Repository Information
|
|
61
|
+
|
|
62
|
+
| Field | Type | Description | Source |
|
|
63
|
+
|-------|------|-------------|--------|
|
|
64
|
+
| `repository_url` | string | Full HTTPS URL to repository | Git remote or `GITHUB_REPOSITORY` env var |
|
|
65
|
+
| `repository_name` | string | Repository name (without owner) | Extracted from URL or repo name |
|
|
66
|
+
| `project_path` | string\|null | Relative path from repo root (for mono-repo) | Calculated from git root |
|
|
67
|
+
| `branch` | string | Current branch name | Git or `GITHUB_REF_NAME` |
|
|
68
|
+
| `commit_hash` | string | Full commit SHA | Git or `GITHUB_SHA` |
|
|
69
|
+
| `commit_message` | string | Commit message (may include newlines) | Git log or GitHub event |
|
|
70
|
+
| `author_email` | string | Commit author email | Git log or GitHub event |
|
|
71
|
+
| `author_name` | string | Commit author name | Git log or GitHub event |
|
|
72
|
+
| `pr_number` | number\|null | Pull request number (if applicable) | Extracted from commit message or branch name |
|
|
73
|
+
|
|
74
|
+
### Quality Metrics
|
|
75
|
+
|
|
76
|
+
| Field | Type | Description |
|
|
77
|
+
|-------|------|-------------|
|
|
78
|
+
| `score` | number | Quality score (0-100) |
|
|
79
|
+
| `total_violations` | number | Total number of violations found |
|
|
80
|
+
| `error_count` | number | Number of error-level violations |
|
|
81
|
+
| `warning_count` | number | Number of warning-level violations |
|
|
82
|
+
| `info_count` | number | Number of info-level violations (reserved) |
|
|
83
|
+
| `lines_of_code` | number | Total lines of code analyzed |
|
|
84
|
+
| `files_analyzed` | number | Number of files analyzed |
|
|
85
|
+
| `sunlint_version` | string | Version of SunLint used |
|
|
86
|
+
| `analysis_duration_ms` | number | Time taken for analysis in milliseconds |
|
|
87
|
+
|
|
88
|
+
### Violations Array
|
|
89
|
+
|
|
90
|
+
Each violation object contains:
|
|
91
|
+
|
|
92
|
+
| Field | Type | Description |
|
|
93
|
+
|-------|------|-------------|
|
|
94
|
+
| `rule_code` | string | Rule identifier (e.g., "C065") |
|
|
95
|
+
| `count` | number | Number of times this rule was violated |
|
|
96
|
+
| `severity` | string | Severity level ("error" or "warning") |
|
|
97
|
+
|
|
98
|
+
## Auto-Detection Features
|
|
99
|
+
|
|
100
|
+
### 1. Mono-Repo Support
|
|
101
|
+
|
|
102
|
+
**Automatic `project_path` Detection:**
|
|
103
|
+
- If running from repo root → `project_path: null`
|
|
104
|
+
- If running from subdirectory → `project_path: "relative/path/from/root"`
|
|
105
|
+
|
|
106
|
+
**Example Mono-Repo Structure:**
|
|
107
|
+
```
|
|
108
|
+
my-monorepo/
|
|
109
|
+
├── project-a/
|
|
110
|
+
│ └── src/
|
|
111
|
+
├── project-b/
|
|
112
|
+
│ └── src/
|
|
113
|
+
└── shared/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Running from `my-monorepo/project-a/`:
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"project_path": "project-a",
|
|
120
|
+
"repository_name": "my-monorepo"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2. PR Number Extraction
|
|
125
|
+
|
|
126
|
+
SunLint automatically extracts PR numbers from:
|
|
127
|
+
|
|
128
|
+
**Commit Messages:**
|
|
129
|
+
- `"Merge pull request #123"` → `pr_number: 123`
|
|
130
|
+
- `"Fix #456: Update scoring"` → `pr_number: 456`
|
|
131
|
+
- `"Closes #789"` → `pr_number: 789`
|
|
132
|
+
|
|
133
|
+
**Branch Names:**
|
|
134
|
+
- `"pr-123"` → `pr_number: 123`
|
|
135
|
+
- `"pull/456"` → `pr_number: 456`
|
|
136
|
+
- `"PR-789-feature"` → `pr_number: 789`
|
|
137
|
+
|
|
138
|
+
### 3. Environment Variable Priority
|
|
139
|
+
|
|
140
|
+
CI/CD environment variables take precedence over git commands:
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
// Priority order:
|
|
144
|
+
1. GITHUB_REPOSITORY → repository_url
|
|
145
|
+
2. GITHUB_REF_NAME → branch
|
|
146
|
+
3. GITHUB_SHA → commit_hash
|
|
147
|
+
4. GITHUB_EVENT_PATH (JSON) → commit details
|
|
148
|
+
5. Git commands → fallback for all fields
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Usage in CI/CD
|
|
152
|
+
|
|
153
|
+
### Before v1.4.0 (Manual Transformation Required)
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
- name: Generate Report
|
|
157
|
+
run: sunlint --all --output-summary=report.json
|
|
158
|
+
|
|
159
|
+
- name: Transform for API
|
|
160
|
+
run: |
|
|
161
|
+
jq -n \
|
|
162
|
+
--arg repo_url "${{ github.repository }}" \
|
|
163
|
+
--arg commit "${{ github.sha }}" \
|
|
164
|
+
--argjson report "$(cat report.json)" \
|
|
165
|
+
'{
|
|
166
|
+
repository_url: $repo_url,
|
|
167
|
+
commit_hash: $commit,
|
|
168
|
+
score: $report.quality.score,
|
|
169
|
+
total_violations: $report.violations.total,
|
|
170
|
+
# ... manual mapping for 15+ fields
|
|
171
|
+
}' > api-payload.json
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### v1.4.0+ (Direct Use - No Transformation!)
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
- name: Generate Report
|
|
178
|
+
run: sunlint --all --output-summary=report.json
|
|
179
|
+
|
|
180
|
+
- name: Upload to Dashboard
|
|
181
|
+
run: |
|
|
182
|
+
curl -X POST https://dashboard.example.com/api/reports \
|
|
183
|
+
-H "Content-Type: application/json" \
|
|
184
|
+
-H "Authorization: Bearer ${{ secrets.API_TOKEN }}" \
|
|
185
|
+
-d @report.json
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**That's it!** No `jq` transformation needed. 🎉
|
|
189
|
+
|
|
190
|
+
## GitHub Actions Integration
|
|
191
|
+
|
|
192
|
+
### Full Example
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
name: Code Quality Check
|
|
196
|
+
|
|
197
|
+
on:
|
|
198
|
+
push:
|
|
199
|
+
branches: [ main ]
|
|
200
|
+
pull_request:
|
|
201
|
+
branches: [ main ]
|
|
202
|
+
|
|
203
|
+
jobs:
|
|
204
|
+
quality:
|
|
205
|
+
runs-on: ubuntu-latest
|
|
206
|
+
steps:
|
|
207
|
+
- uses: actions/checkout@v3
|
|
208
|
+
with:
|
|
209
|
+
fetch-depth: 0 # Full history for git info
|
|
210
|
+
|
|
211
|
+
- name: Run SunLint
|
|
212
|
+
run: |
|
|
213
|
+
npx @sun-asterisk/sunlint \
|
|
214
|
+
--input=src \
|
|
215
|
+
--all \
|
|
216
|
+
--output-summary=quality-report.json
|
|
217
|
+
# Environment variables are automatically available
|
|
218
|
+
# No need to set them manually!
|
|
219
|
+
|
|
220
|
+
- name: Upload to Dashboard
|
|
221
|
+
run: |
|
|
222
|
+
curl -X POST ${{ secrets.DASHBOARD_URL }}/api/reports \
|
|
223
|
+
-H "Content-Type: application/json" \
|
|
224
|
+
-H "Authorization: Bearer ${{ secrets.DASHBOARD_TOKEN }}" \
|
|
225
|
+
-d @quality-report.json
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Available GitHub Actions Environment Variables
|
|
229
|
+
|
|
230
|
+
These are **automatically** available and used by SunLint:
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
# Automatically set by GitHub Actions:
|
|
234
|
+
GITHUB_REPOSITORY: "owner/repo"
|
|
235
|
+
GITHUB_REF_NAME: "main"
|
|
236
|
+
GITHUB_SHA: "abc123..."
|
|
237
|
+
GITHUB_EVENT_PATH: "/github/workflow/event.json"
|
|
238
|
+
|
|
239
|
+
# From event.json:
|
|
240
|
+
- head_commit.message
|
|
241
|
+
- head_commit.author.email
|
|
242
|
+
- head_commit.author.name
|
|
243
|
+
- pull_request.number (for PR events)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Local Development
|
|
247
|
+
|
|
248
|
+
When running locally (not in CI/CD), SunLint uses git commands:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# Local run - auto-detects from git
|
|
252
|
+
sunlint --input=src --all --output-summary=local-report.json
|
|
253
|
+
|
|
254
|
+
# Result uses local git information:
|
|
255
|
+
{
|
|
256
|
+
"repository_url": "https://github.com/owner/repo", # from git remote
|
|
257
|
+
"branch": "feature/my-branch", # from git branch
|
|
258
|
+
"commit_hash": "def456...", # from git log
|
|
259
|
+
"commit_message": "WIP: My feature", # from git log
|
|
260
|
+
"author_email": "me@example.com", # from git config
|
|
261
|
+
"author_name": "My Name", # from git config
|
|
262
|
+
"pr_number": null # no PR locally
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Compatibility
|
|
267
|
+
|
|
268
|
+
### Dashboard API Requirements
|
|
269
|
+
|
|
270
|
+
The output format is compatible with APIs expecting:
|
|
271
|
+
|
|
272
|
+
✅ Flat structure (no nested objects for main fields)
|
|
273
|
+
✅ Repository metadata (URL, name, path, branch, commit)
|
|
274
|
+
✅ Commit details (message, author email, author name)
|
|
275
|
+
✅ PR tracking (PR number)
|
|
276
|
+
✅ Quality metrics (score, violations, LOC)
|
|
277
|
+
✅ Violations summary (grouped by rule)
|
|
278
|
+
|
|
279
|
+
### Backwards Compatibility
|
|
280
|
+
|
|
281
|
+
Additional nested objects are included for backwards compatibility:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
// API-compatible flat fields
|
|
286
|
+
"score": 92.6,
|
|
287
|
+
"total_violations": 39,
|
|
288
|
+
|
|
289
|
+
// Legacy nested structure (for backwards compatibility)
|
|
290
|
+
"metadata": { ... },
|
|
291
|
+
"quality": { ... }
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Troubleshooting
|
|
296
|
+
|
|
297
|
+
### Missing Git Information
|
|
298
|
+
|
|
299
|
+
**Problem**: Some fields are `null` in the output
|
|
300
|
+
|
|
301
|
+
**Solution**: Ensure you're running from a git repository with commits
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# Check git status
|
|
305
|
+
git status
|
|
306
|
+
|
|
307
|
+
# Verify remote is configured
|
|
308
|
+
git remote -v
|
|
309
|
+
|
|
310
|
+
# Ensure commits exist
|
|
311
|
+
git log -1
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Incorrect project_path
|
|
315
|
+
|
|
316
|
+
**Problem**: `project_path` is not null when it should be
|
|
317
|
+
|
|
318
|
+
**Solution**: Run SunLint from the repository root:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
cd /path/to/repo-root
|
|
322
|
+
sunlint --input=. --all --output-summary=report.json
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### PR Number Not Detected
|
|
326
|
+
|
|
327
|
+
**Problem**: `pr_number` is `null` in PR builds
|
|
328
|
+
|
|
329
|
+
**Solution**:
|
|
330
|
+
|
|
331
|
+
1. For GitHub Actions PR events, use `pull_request` trigger
|
|
332
|
+
2. Ensure commit message includes PR reference (e.g., "#123")
|
|
333
|
+
3. Use PR branch naming convention (e.g., "pr-123" or "pull/123")
|
|
334
|
+
|
|
335
|
+
```yaml
|
|
336
|
+
on:
|
|
337
|
+
pull_request: # This sets pull_request.number in event.json
|
|
338
|
+
branches: [ main ]
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Migration Guide
|
|
342
|
+
|
|
343
|
+
### From Manual Transformation to Direct API Upload
|
|
344
|
+
|
|
345
|
+
**Before:**
|
|
346
|
+
```yaml
|
|
347
|
+
- run: sunlint --all --output-summary=sunlint-report.json
|
|
348
|
+
- run: |
|
|
349
|
+
# 30+ lines of jq transformation
|
|
350
|
+
jq -n --arg repo ... > api-payload.json
|
|
351
|
+
- run: curl -X POST ... -d @api-payload.json
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**After:**
|
|
355
|
+
```yaml
|
|
356
|
+
- run: sunlint --all --output-summary=sunlint-report.json
|
|
357
|
+
- run: curl -X POST ... -d @sunlint-report.json
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Benefits:**
|
|
361
|
+
- ✅ **Simpler**: 3 lines instead of 30+
|
|
362
|
+
- ✅ **Faster**: No jq processing overhead
|
|
363
|
+
- ✅ **More reliable**: No transformation errors
|
|
364
|
+
- ✅ **Maintainable**: SunLint handles format updates
|
|
365
|
+
|
|
366
|
+
## See Also
|
|
367
|
+
|
|
368
|
+
- [Quality Scoring Guide](QUALITY_SCORING_GUIDE.md)
|
|
369
|
+
- [CI/CD Integration Guide](CI-CD-GUIDE.md)
|
|
370
|
+
- [Configuration Guide](CONFIGURATION.md)
|
|
@@ -152,13 +152,25 @@ jobs:
|
|
|
152
152
|
|
|
153
153
|
The summary report automatically detects and uses Git information from environment variables (commonly available in CI/CD):
|
|
154
154
|
|
|
155
|
-
| Variable | Description | Source |
|
|
156
|
-
|
|
157
|
-
| `GITHUB_REPOSITORY` | Repository name (e.g., `owner/repo`) | GitHub Actions |
|
|
158
|
-
| `GITHUB_REF_NAME` | Branch or tag name | GitHub Actions |
|
|
159
|
-
| `GITHUB_SHA` | Commit hash | GitHub Actions |
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
| Variable | Description | Source | Fallback |
|
|
156
|
+
|----------|-------------|--------|----------|
|
|
157
|
+
| `GITHUB_REPOSITORY` | Repository name (e.g., `owner/repo`) | GitHub Actions | Git remote URL |
|
|
158
|
+
| `GITHUB_REF_NAME` | Branch or tag name | GitHub Actions | `git rev-parse --abbrev-ref HEAD` |
|
|
159
|
+
| `GITHUB_SHA` | Commit hash | GitHub Actions | `git rev-parse HEAD` |
|
|
160
|
+
| `GITHUB_EVENT_PATH` | Path to event payload JSON | GitHub Actions | - |
|
|
161
|
+
| `GITHUB_EVENT_HEAD_COMMIT_MESSAGE` | Commit message | GitHub Actions | `git log -1 --pretty=%B` |
|
|
162
|
+
| `GITHUB_EVENT_HEAD_COMMIT_AUTHOR_EMAIL` | Author email | GitHub Actions | `git log -1 --pretty=%ae` |
|
|
163
|
+
| `GITHUB_EVENT_HEAD_COMMIT_AUTHOR_NAME` | Author name | GitHub Actions | `git log -1 --pretty=%an` |
|
|
164
|
+
|
|
165
|
+
**Additional Fields Auto-Detected:**
|
|
166
|
+
- **`repository_name`**: Extracted from repository URL
|
|
167
|
+
- **`project_path`**: Relative path from repo root (for mono-repo support)
|
|
168
|
+
- **`commit_message`**: From git log or GitHub event
|
|
169
|
+
- **`author_email`**: From git log or GitHub event
|
|
170
|
+
- **`author_name`**: From git log or GitHub event
|
|
171
|
+
- **`pr_number`**: Extracted from commit message (e.g., "#123") or branch name (e.g., "pr-123")
|
|
172
|
+
|
|
173
|
+
If environment variables are not available, the tool will automatically detect all Git information from the local repository.
|
|
162
174
|
|
|
163
175
|
## CLI Options
|
|
164
176
|
|
package/package.json
CHANGED
|
@@ -66,8 +66,8 @@ class C023SymbolBasedAnalyzer {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
checkScope(node, violations) {
|
|
70
|
-
const seen = new Map();
|
|
69
|
+
checkScope(node, violations, parentSeen = new Map()) {
|
|
70
|
+
const seen = new Map(parentSeen);
|
|
71
71
|
|
|
72
72
|
node.forEachChild((child) => {
|
|
73
73
|
switch (child.getKind()) {
|
|
@@ -83,56 +83,55 @@ class C023SymbolBasedAnalyzer {
|
|
|
83
83
|
case SyntaxKind.ArrowFunction:
|
|
84
84
|
case SyntaxKind.MethodDeclaration:
|
|
85
85
|
case SyntaxKind.Constructor: { // ✅ also cover constructors
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.checkDuplicate(p, seen, violations)
|
|
86
|
+
const funcSeen = new Map();
|
|
87
|
+
child.getParameters().forEach((p) =>
|
|
88
|
+
this.checkDuplicate(p, funcSeen, violations)
|
|
90
89
|
);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (body) this.checkScope(body, violations);
|
|
90
|
+
const body = child.getBody && child.getBody();
|
|
91
|
+
if (body) this.checkScope(body, violations, funcSeen);
|
|
94
92
|
break;
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
case SyntaxKind.Block: {
|
|
98
|
-
this.checkScope(child, violations);
|
|
96
|
+
this.checkScope(child, violations, new Map(seen));
|
|
99
97
|
break;
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
case SyntaxKind.CatchClause: {
|
|
101
|
+
const catchSeen = new Map(seen);
|
|
103
102
|
const catchVar = child.getVariableDeclaration();
|
|
104
103
|
if (catchVar) {
|
|
105
|
-
|
|
106
|
-
this.checkDuplicate(catchVar, seen, violations);
|
|
104
|
+
this.checkDuplicate(catchVar, catchSeen, violations);
|
|
107
105
|
}
|
|
108
|
-
|
|
109
|
-
this.checkScope(child.getBlock(), violations);
|
|
106
|
+
this.checkScope(child.getBlock(), violations, catchSeen);
|
|
110
107
|
break;
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
case SyntaxKind.ForStatement:
|
|
114
111
|
case SyntaxKind.ForOfStatement:
|
|
115
112
|
case SyntaxKind.ForInStatement: {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
const loopSeen = new Map(seen);
|
|
114
|
+
const initializer = child.getInitializer && child.getInitializer();
|
|
115
|
+
if (initializer && initializer.getDeclarations) {
|
|
116
|
+
initializer.getDeclarations().forEach((decl) =>
|
|
117
|
+
this.checkDuplicate(decl, loopSeen, violations)
|
|
120
118
|
);
|
|
121
119
|
}
|
|
122
|
-
const statement = child.getStatement
|
|
123
|
-
if (statement) this.checkScope(statement, violations);
|
|
120
|
+
const statement = child.getStatement && child.getStatement();
|
|
121
|
+
if (statement) this.checkScope(statement, violations, loopSeen);
|
|
124
122
|
break;
|
|
125
123
|
}
|
|
126
124
|
|
|
127
125
|
default: {
|
|
128
|
-
this.checkScope(child, violations);
|
|
126
|
+
this.checkScope(child, violations, seen);
|
|
129
127
|
}
|
|
130
128
|
}
|
|
131
129
|
});
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
checkDuplicate(node, seen, violations) {
|
|
135
|
-
const
|
|
133
|
+
const nameNode = node.getNameNode();
|
|
134
|
+
const name = nameNode.getText();
|
|
136
135
|
if (!name) return;
|
|
137
136
|
const filePath = node.getSourceFile().getFilePath();
|
|
138
137
|
|
package/docs/AI.md
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# 🤖 AI-Powered Analysis
|
|
2
|
-
|
|
3
|
-
Sunlint supports AI-powered code analysis alongside traditional pattern-based analysis for more intelligent and context-aware rule checking.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
- **🎯 Smart Analysis**: Uses AI to understand code context and intent
|
|
8
|
-
- **🔄 Fallback Strategy**: Automatically falls back to pattern analysis if AI fails
|
|
9
|
-
- **⚡ Performance**: AI analysis runs per-file with caching support
|
|
10
|
-
- **🔧 Configurable**: Multiple AI providers and models supported
|
|
11
|
-
|
|
12
|
-
## Configuration
|
|
13
|
-
|
|
14
|
-
### In `.sunlint.json`:
|
|
15
|
-
|
|
16
|
-
```json
|
|
17
|
-
{
|
|
18
|
-
"ai": {
|
|
19
|
-
"enabled": true,
|
|
20
|
-
"provider": "openai",
|
|
21
|
-
"model": "gpt-4o-mini",
|
|
22
|
-
"apiKey": "${OPENAI_API_KEY}",
|
|
23
|
-
"fallbackToPattern": true
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Environment Variables:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
export OPENAI_API_KEY="your-openai-api-key"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Supported Providers
|
|
35
|
-
|
|
36
|
-
### OpenAI
|
|
37
|
-
- **Models**: `gpt-4`, `gpt-4o-mini`, `gpt-3.5-turbo`
|
|
38
|
-
- **API Key**: Required via `OPENAI_API_KEY` environment variable
|
|
39
|
-
- **Cost**: Pay-per-use based on OpenAI pricing
|
|
40
|
-
|
|
41
|
-
### GitHub Copilot (Planned)
|
|
42
|
-
- **Integration**: VS Code extension integration
|
|
43
|
-
- **Models**: GitHub Copilot models
|
|
44
|
-
- **Authentication**: VS Code Copilot session
|
|
45
|
-
|
|
46
|
-
## AI-Enhanced Rules
|
|
47
|
-
|
|
48
|
-
### C019 - Log Level Usage
|
|
49
|
-
✅ **AI-Enabled**: Understands code context to determine appropriate log levels
|
|
50
|
-
|
|
51
|
-
**AI Analysis Features:**
|
|
52
|
-
- **Context Understanding**: Analyzes surrounding code to determine error criticality
|
|
53
|
-
- **Intent Recognition**: Understands whether errors are expected or exceptional
|
|
54
|
-
- **Semantic Analysis**: Goes beyond pattern matching to understand meaning
|
|
55
|
-
|
|
56
|
-
**Example:**
|
|
57
|
-
```typescript
|
|
58
|
-
// AI understands this is a validation error, suggests warn level
|
|
59
|
-
if (!user.email) {
|
|
60
|
-
console.error('Missing email'); // AI: Should use console.warn()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// AI understands this is a critical system error, keeps error level
|
|
64
|
-
try {
|
|
65
|
-
await database.connect();
|
|
66
|
-
} catch (error) {
|
|
67
|
-
console.error('Database connection failed:', error); // AI: Appropriate error level
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Usage
|
|
72
|
-
|
|
73
|
-
### CLI Commands
|
|
74
|
-
|
|
75
|
-
**Enable AI for specific rule:**
|
|
76
|
-
```bash
|
|
77
|
-
sunlint --rule=C019 --input=src --ai
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**Enable AI for all rules:**
|
|
81
|
-
```bash
|
|
82
|
-
sunlint --quality --input=src --ai
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**Debug AI analysis:**
|
|
86
|
-
```bash
|
|
87
|
-
sunlint --rule=C019 --input=src --ai --verbose
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### VS Code Integration
|
|
91
|
-
|
|
92
|
-
1. **Debug Configuration**: Use "Debug Sunlint - AI Analysis"
|
|
93
|
-
2. **Task**: Run "Sunlint: AI Analysis Test"
|
|
94
|
-
3. **Set API Key**: Configure `OPENAI_API_KEY` in environment
|
|
95
|
-
|
|
96
|
-
## Output Differences
|
|
97
|
-
|
|
98
|
-
### Pattern Analysis Output:
|
|
99
|
-
```
|
|
100
|
-
WARNING: Error log level used for non-critical issue - should use warn/info level
|
|
101
|
-
at src/user.ts:15:5 (C019)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### AI Analysis Output:
|
|
105
|
-
```
|
|
106
|
-
WARNING: Email validation error should use console.warn() - this is user input validation, not a system error
|
|
107
|
-
at src/user.ts:15:5 (C019)
|
|
108
|
-
Suggestion: Change to console.warn('User email validation failed:', email)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Performance Considerations
|
|
112
|
-
|
|
113
|
-
- **Caching**: AI responses are cached per file content hash
|
|
114
|
-
- **Concurrency**: AI calls are made concurrently with rate limiting
|
|
115
|
-
- **Timeout**: 30-second timeout per AI request
|
|
116
|
-
- **Cost**: Monitor API usage in OpenAI dashboard
|
|
117
|
-
|
|
118
|
-
## Troubleshooting
|
|
119
|
-
|
|
120
|
-
### Common Issues
|
|
121
|
-
|
|
122
|
-
**API Key Not Found:**
|
|
123
|
-
```bash
|
|
124
|
-
⚠️ AI API key not found, falling back to pattern analysis
|
|
125
|
-
```
|
|
126
|
-
Solution: Set `OPENAI_API_KEY` environment variable
|
|
127
|
-
|
|
128
|
-
**API Rate Limit:**
|
|
129
|
-
```bash
|
|
130
|
-
AI Analysis failed: OpenAI API error: 429 Too Many Requests
|
|
131
|
-
```
|
|
132
|
-
Solution: Reduce `maxConcurrentRules` in config or wait
|
|
133
|
-
|
|
134
|
-
**Network Issues:**
|
|
135
|
-
```bash
|
|
136
|
-
AI Analysis failed: OpenAI API error: Network timeout
|
|
137
|
-
```
|
|
138
|
-
Solution: Check internet connection, increase `timeoutMs`
|
|
139
|
-
|
|
140
|
-
### Debug AI Issues
|
|
141
|
-
|
|
142
|
-
1. **Enable verbose mode**: `--verbose`
|
|
143
|
-
2. **Check API key**: `echo $OPENAI_API_KEY`
|
|
144
|
-
3. **Test connection**: Use debug configuration
|
|
145
|
-
4. **Check API quota**: Visit OpenAI dashboard
|
|
146
|
-
|
|
147
|
-
## Future Enhancements
|
|
148
|
-
|
|
149
|
-
- **🔄 GitHub Copilot Integration**: Direct integration with VS Code Copilot
|
|
150
|
-
- **📊 Custom Models**: Support for fine-tuned models
|
|
151
|
-
- **🎯 Rule-Specific Prompts**: Specialized prompts per rule type
|
|
152
|
-
- **💾 Smart Caching**: Semantic caching across similar code patterns
|
|
153
|
-
- **📈 Analytics**: AI vs Pattern analysis effectiveness metrics
|
|
154
|
-
|
|
155
|
-
## Cost Estimation
|
|
156
|
-
|
|
157
|
-
**OpenAI API Costs** (approximate):
|
|
158
|
-
- **gpt-4o-mini**: ~$0.001 per 1K tokens
|
|
159
|
-
- **gpt-4**: ~$0.03 per 1K tokens
|
|
160
|
-
- **Average file**: ~500 tokens
|
|
161
|
-
- **1000 files with gpt-4o-mini**: ~$0.50
|
|
162
|
-
|
|
163
|
-
**Recommendation**: Start with `gpt-4o-mini` for cost-effectiveness.
|