@sun-asterisk/sunlint 1.3.20 → 1.3.22
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/core/analysis-orchestrator.js +34 -17
- package/core/file-targeting-service.js +66 -15
- package/core/git-utils.js +232 -8
- package/core/github-annotate-service.js +72 -19
- package/core/output-service.js +4 -3
- package/docs/GITHUB_ACTIONS_INTEGRATION.md +15 -2
- package/engines/heuristic-engine.js +45 -16
- package/package.json +1 -1
|
@@ -196,13 +196,24 @@ class AnalysisOrchestrator {
|
|
|
196
196
|
|
|
197
197
|
// Group rules by their preferred engines
|
|
198
198
|
const engineGroups = this.groupRulesByEngine(optimizedRules, config);
|
|
199
|
-
|
|
199
|
+
|
|
200
|
+
// Calculate total batches for progress tracking
|
|
201
|
+
let totalBatches = 0;
|
|
202
|
+
const engineBatchInfo = new Map();
|
|
203
|
+
for (const [engineName, rules] of engineGroups) {
|
|
204
|
+
const ruleBatches = this.performanceOptimizer.createRuleBatches(rules, config);
|
|
205
|
+
engineBatchInfo.set(engineName, ruleBatches);
|
|
206
|
+
totalBatches += ruleBatches.length;
|
|
207
|
+
}
|
|
208
|
+
|
|
200
209
|
if (!options.quiet) {
|
|
201
|
-
console.log(chalk.cyan(`🚀 Running analysis across ${engineGroups.size} engines...`));
|
|
210
|
+
console.log(chalk.cyan(`🚀 Running analysis across ${engineGroups.size} engines (${totalBatches} batches total)...`));
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
// Run analysis on each engine with batching
|
|
205
214
|
const results = [];
|
|
215
|
+
let completedBatches = 0;
|
|
216
|
+
|
|
206
217
|
for (const [engineName, rules] of engineGroups) {
|
|
207
218
|
const engine = this.engines.get(engineName);
|
|
208
219
|
if (!engine) {
|
|
@@ -210,38 +221,44 @@ class AnalysisOrchestrator {
|
|
|
210
221
|
continue;
|
|
211
222
|
}
|
|
212
223
|
|
|
213
|
-
//
|
|
214
|
-
const ruleBatches =
|
|
215
|
-
|
|
224
|
+
// Get pre-calculated batches
|
|
225
|
+
const ruleBatches = engineBatchInfo.get(engineName);
|
|
226
|
+
|
|
216
227
|
for (let i = 0; i < ruleBatches.length; i++) {
|
|
217
228
|
const batch = ruleBatches[i];
|
|
218
229
|
const batchNumber = i + 1;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
const overallProgress = Math.round((completedBatches / totalBatches) * 100);
|
|
231
|
+
|
|
232
|
+
if (!options.quiet) {
|
|
233
|
+
if (ruleBatches.length > 1) {
|
|
234
|
+
console.log(chalk.blue(`⚙️ [${overallProgress}%] ${engineName} - Batch ${batchNumber}/${ruleBatches.length}: ${batch.length} rules (${optimizedFiles.length} files)`));
|
|
235
|
+
} else {
|
|
236
|
+
console.log(chalk.blue(`⚙️ [${overallProgress}%] Running ${batch.length} rules on ${engineName} engine (${optimizedFiles.length} files)...`));
|
|
237
|
+
}
|
|
224
238
|
}
|
|
225
239
|
|
|
226
240
|
try {
|
|
227
241
|
const engineResult = await this.runEngineWithOptimizations(
|
|
228
|
-
engine,
|
|
229
|
-
optimizedFiles,
|
|
230
|
-
batch,
|
|
242
|
+
engine,
|
|
243
|
+
optimizedFiles,
|
|
244
|
+
batch,
|
|
231
245
|
options,
|
|
232
|
-
{ batchNumber, totalBatches: ruleBatches.length }
|
|
246
|
+
{ batchNumber, totalBatches: ruleBatches.length, overallProgress }
|
|
233
247
|
);
|
|
234
|
-
|
|
248
|
+
|
|
235
249
|
results.push({
|
|
236
250
|
engine: engineName,
|
|
237
251
|
batch: batchNumber,
|
|
238
252
|
rules: batch.map(r => r.id),
|
|
239
253
|
...engineResult
|
|
240
254
|
});
|
|
241
|
-
|
|
255
|
+
|
|
256
|
+
completedBatches++;
|
|
257
|
+
const newProgress = Math.round((completedBatches / totalBatches) * 100);
|
|
258
|
+
|
|
242
259
|
if (!options.quiet) {
|
|
243
260
|
const violationCount = this.countViolations(engineResult);
|
|
244
|
-
console.log(chalk.
|
|
261
|
+
console.log(chalk.green(`✅ [${newProgress}%] ${engineName} batch ${batchNumber}/${ruleBatches.length}: ${violationCount} violations found`));
|
|
245
262
|
}
|
|
246
263
|
} catch (error) {
|
|
247
264
|
// Enhanced error recovery with batch context
|
|
@@ -12,6 +12,7 @@ const { minimatch } = require('minimatch');
|
|
|
12
12
|
class FileTargetingService {
|
|
13
13
|
constructor() {
|
|
14
14
|
this.supportedLanguages = ['typescript', 'javascript', 'dart', 'kotlin', 'java', 'swift'];
|
|
15
|
+
this.GitUtils = require('./git-utils');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -22,35 +23,43 @@ class FileTargetingService {
|
|
|
22
23
|
try {
|
|
23
24
|
const startTime = Date.now();
|
|
24
25
|
const metadata = config._metadata;
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
if (cliOptions.verbose) {
|
|
27
28
|
console.log(chalk.cyan(`📁 File Targeting: ${this.getTargetingMode(metadata)}`));
|
|
28
29
|
if (metadata?.shouldBypassProjectDiscovery) {
|
|
29
30
|
console.log(chalk.blue(`🎯 Optimized targeting for ${metadata.analysisScope}`));
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
let allFiles = [];
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
allFiles = await this.
|
|
35
|
+
|
|
36
|
+
// Handle --changed-files option (git diff mode)
|
|
37
|
+
if (cliOptions.changedFiles) {
|
|
38
|
+
if (cliOptions.verbose) {
|
|
39
|
+
console.log(chalk.cyan('🔄 Using --changed-files mode (git diff)'));
|
|
40
|
+
}
|
|
41
|
+
allFiles = await this.getGitChangedFiles(cliOptions);
|
|
41
42
|
} else {
|
|
42
|
-
|
|
43
|
+
// Smart project-level optimization
|
|
44
|
+
const optimizedPaths = this.optimizeProjectPaths(inputPaths, cliOptions);
|
|
45
|
+
|
|
46
|
+
// Use enhanced targeting based on metadata
|
|
47
|
+
if (metadata?.shouldBypassProjectDiscovery) {
|
|
48
|
+
allFiles = await this.collectTargetedFiles(optimizedPaths, config, cliOptions);
|
|
49
|
+
} else {
|
|
50
|
+
allFiles = await this.collectProjectFiles(optimizedPaths, config, cliOptions);
|
|
51
|
+
}
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
// Apply filtering logic
|
|
46
55
|
const targetFiles = this.applyFiltering(allFiles, config, cliOptions);
|
|
47
|
-
|
|
56
|
+
|
|
48
57
|
const duration = Date.now() - startTime;
|
|
49
|
-
|
|
58
|
+
|
|
50
59
|
if (cliOptions.verbose) {
|
|
51
60
|
console.log(chalk.green(`✅ File targeting completed in ${duration}ms (${targetFiles.length} files)`));
|
|
52
61
|
}
|
|
53
|
-
|
|
62
|
+
|
|
54
63
|
return {
|
|
55
64
|
files: targetFiles,
|
|
56
65
|
stats: this.generateStats(targetFiles, config),
|
|
@@ -62,6 +71,41 @@ class FileTargetingService {
|
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Get files changed in git (for --changed-files option)
|
|
76
|
+
*/
|
|
77
|
+
async getGitChangedFiles(cliOptions) {
|
|
78
|
+
try {
|
|
79
|
+
const baseRef = cliOptions.diffBase || null; // null = auto-detect
|
|
80
|
+
const changedFiles = this.GitUtils.getChangedFiles(baseRef);
|
|
81
|
+
|
|
82
|
+
if (cliOptions.verbose) {
|
|
83
|
+
const detectedBase = baseRef || this.GitUtils.getSmartBaseRef();
|
|
84
|
+
console.log(chalk.blue(`ℹ️ Using base ref: ${detectedBase}`));
|
|
85
|
+
console.log(chalk.blue(`ℹ️ Found ${changedFiles.length} changed file(s)`));
|
|
86
|
+
|
|
87
|
+
if (changedFiles.length > 0 && changedFiles.length <= 10) {
|
|
88
|
+
console.log(chalk.gray(' Changed files:'));
|
|
89
|
+
changedFiles.forEach(f => {
|
|
90
|
+
console.log(chalk.gray(` - ${path.relative(process.cwd(), f)}`));
|
|
91
|
+
});
|
|
92
|
+
} else if (changedFiles.length > 10) {
|
|
93
|
+
console.log(chalk.gray(` First 10 changed files:`));
|
|
94
|
+
changedFiles.slice(0, 10).forEach(f => {
|
|
95
|
+
console.log(chalk.gray(` - ${path.relative(process.cwd(), f)}`));
|
|
96
|
+
});
|
|
97
|
+
console.log(chalk.gray(` ... and ${changedFiles.length - 10} more`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return changedFiles;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(chalk.yellow(`⚠️ Failed to get changed files: ${error.message}`));
|
|
104
|
+
console.error(chalk.yellow('ℹ️ Falling back to all files'));
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
65
109
|
/**
|
|
66
110
|
* Get targeting mode description
|
|
67
111
|
*/
|
|
@@ -293,15 +337,22 @@ class FileTargetingService {
|
|
|
293
337
|
if (debug) console.log(`🔍 [DEBUG] applyFiltering: start with ${filteredFiles.length} files`);
|
|
294
338
|
if (debug) console.log(`🔍 [DEBUG] config.include:`, config.include);
|
|
295
339
|
if (debug) console.log(`🔍 [DEBUG] cliOptions.include:`, cliOptions.include);
|
|
340
|
+
if (debug) console.log(`🔍 [DEBUG] cliOptions.changedFiles:`, cliOptions.changedFiles);
|
|
341
|
+
|
|
342
|
+
// IMPORTANT: When using --changed-files, skip include patterns
|
|
343
|
+
// Git already filtered the files, we only need to apply excludes
|
|
344
|
+
const skipIncludePatterns = cliOptions.changedFiles;
|
|
296
345
|
|
|
297
346
|
// 1. Apply config include patterns first (medium priority)
|
|
298
|
-
if (config.include && config.include.length > 0) {
|
|
347
|
+
if (!skipIncludePatterns && config.include && config.include.length > 0) {
|
|
299
348
|
filteredFiles = this.applyIncludePatterns(filteredFiles, config.include, debug);
|
|
300
349
|
if (debug) console.log(`🔍 [DEBUG] After config include: ${filteredFiles.length} files`);
|
|
350
|
+
} else if (skipIncludePatterns && debug) {
|
|
351
|
+
console.log(`🔍 [DEBUG] Skipping config include patterns (--changed-files mode)`);
|
|
301
352
|
}
|
|
302
353
|
|
|
303
354
|
// 2. Apply CLI include overrides (highest priority - completely overrides config)
|
|
304
|
-
if (cliOptions.include) {
|
|
355
|
+
if (!skipIncludePatterns && cliOptions.include) {
|
|
305
356
|
// CLI include completely replaces config include - start fresh from all files
|
|
306
357
|
filteredFiles = this.applyIncludePatterns([...files], cliOptions.include, debug);
|
|
307
358
|
}
|
package/core/git-utils.js
CHANGED
|
@@ -21,13 +21,98 @@ class GitUtils {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Detect if running in PR context (GitHub Actions, GitLab CI, etc.)
|
|
26
|
+
* @returns {Object|null} PR context info or null
|
|
27
|
+
*/
|
|
28
|
+
static detectPRContext() {
|
|
29
|
+
// GitHub Actions
|
|
30
|
+
if (process.env.GITHUB_EVENT_NAME === 'pull_request' ||
|
|
31
|
+
process.env.GITHUB_EVENT_NAME === 'pull_request_target') {
|
|
32
|
+
return {
|
|
33
|
+
provider: 'github',
|
|
34
|
+
baseBranch: process.env.GITHUB_BASE_REF,
|
|
35
|
+
headBranch: process.env.GITHUB_HEAD_REF,
|
|
36
|
+
prNumber: process.env.GITHUB_REF ? process.env.GITHUB_REF.match(/refs\/pull\/(\d+)\/merge/)?.[1] : null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// GitLab CI
|
|
41
|
+
if (process.env.CI_MERGE_REQUEST_ID) {
|
|
42
|
+
return {
|
|
43
|
+
provider: 'gitlab',
|
|
44
|
+
baseBranch: process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME,
|
|
45
|
+
headBranch: process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
|
|
46
|
+
prNumber: process.env.CI_MERGE_REQUEST_IID
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get smart base reference for diff
|
|
55
|
+
* Auto-detects PR context or falls back to common base branches
|
|
56
|
+
* @param {string} cwd - Working directory
|
|
57
|
+
* @returns {string} Base reference for git diff
|
|
58
|
+
*/
|
|
59
|
+
static getSmartBaseRef(cwd = process.cwd()) {
|
|
60
|
+
if (!this.isGitRepository(cwd)) {
|
|
61
|
+
throw new Error('Not a git repository');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check PR context first
|
|
65
|
+
const prContext = this.detectPRContext();
|
|
66
|
+
if (prContext && prContext.baseBranch) {
|
|
67
|
+
const candidates = [
|
|
68
|
+
`origin/${prContext.baseBranch}`,
|
|
69
|
+
`upstream/${prContext.baseBranch}`,
|
|
70
|
+
prContext.baseBranch
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
try {
|
|
75
|
+
execSync(`git rev-parse --verify ${candidate}`, { cwd, stdio: 'ignore' });
|
|
76
|
+
return candidate;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Continue to next candidate
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback to common base branches
|
|
84
|
+
const fallbackBranches = [
|
|
85
|
+
'origin/main',
|
|
86
|
+
'origin/master',
|
|
87
|
+
'origin/develop',
|
|
88
|
+
'upstream/main',
|
|
89
|
+
'upstream/master',
|
|
90
|
+
'main',
|
|
91
|
+
'master',
|
|
92
|
+
'develop'
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const branch of fallbackBranches) {
|
|
96
|
+
try {
|
|
97
|
+
execSync(`git rev-parse --verify ${branch}`, { cwd, stdio: 'ignore' });
|
|
98
|
+
return branch;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Continue to next candidate
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Last resort: use HEAD (uncommitted changes only)
|
|
105
|
+
return 'HEAD';
|
|
106
|
+
}
|
|
107
|
+
|
|
24
108
|
/**
|
|
25
109
|
* Get list of changed files compared to base reference
|
|
26
|
-
* @param {string} baseRef - Base git reference (e.g., 'origin/main')
|
|
110
|
+
* @param {string|null} baseRef - Base git reference (e.g., 'origin/main'). If null, auto-detect.
|
|
27
111
|
* @param {string} cwd - Working directory
|
|
112
|
+
* @param {boolean} includeUncommitted - Include uncommitted changes
|
|
28
113
|
* @returns {string[]} Array of changed file paths
|
|
29
114
|
*/
|
|
30
|
-
static getChangedFiles(baseRef =
|
|
115
|
+
static getChangedFiles(baseRef = null, cwd = process.cwd(), includeUncommitted = true) {
|
|
31
116
|
if (!this.isGitRepository(cwd)) {
|
|
32
117
|
throw new Error('Not a git repository');
|
|
33
118
|
}
|
|
@@ -35,17 +120,156 @@ class GitUtils {
|
|
|
35
120
|
try {
|
|
36
121
|
// Get git root directory
|
|
37
122
|
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
|
|
38
|
-
|
|
39
|
-
|
|
123
|
+
|
|
124
|
+
// Check if we're in PR context
|
|
125
|
+
const prContext = this.detectPRContext();
|
|
126
|
+
|
|
127
|
+
// If in PR context and no explicit baseRef, use PR-specific logic
|
|
128
|
+
if (prContext && !baseRef) {
|
|
129
|
+
return this.getPRChangedFiles(prContext, gitRoot);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Auto-detect base ref if not provided
|
|
133
|
+
const actualBaseRef = baseRef || this.getSmartBaseRef(cwd);
|
|
134
|
+
|
|
135
|
+
const allFiles = new Set();
|
|
136
|
+
|
|
137
|
+
// Get committed changes
|
|
138
|
+
if (actualBaseRef !== 'HEAD') {
|
|
139
|
+
// Use two-dot diff for branch comparison (what's new in this branch)
|
|
140
|
+
const command = `git diff --name-only ${actualBaseRef}..HEAD`;
|
|
141
|
+
const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
|
|
142
|
+
|
|
143
|
+
output
|
|
144
|
+
.split('\n')
|
|
145
|
+
.filter(file => file.trim() !== '')
|
|
146
|
+
.forEach(file => allFiles.add(path.resolve(gitRoot, file)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get uncommitted changes if requested
|
|
150
|
+
if (includeUncommitted) {
|
|
151
|
+
const uncommittedCommand = 'git diff --name-only HEAD';
|
|
152
|
+
try {
|
|
153
|
+
const uncommittedOutput = execSync(uncommittedCommand, { cwd: gitRoot, encoding: 'utf8' });
|
|
154
|
+
uncommittedOutput
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter(file => file.trim() !== '')
|
|
157
|
+
.forEach(file => allFiles.add(path.resolve(gitRoot, file)));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Ignore errors for uncommitted changes (might be empty)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Filter to only existing files
|
|
164
|
+
return Array.from(allFiles).filter(file => fs.existsSync(file));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(`Failed to get changed files: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get changed files in PR context using merge-base
|
|
172
|
+
* @param {Object} prContext - PR context info from detectPRContext()
|
|
173
|
+
* @param {string} gitRoot - Git repository root path
|
|
174
|
+
* @returns {string[]} Array of changed file paths in the PR
|
|
175
|
+
*/
|
|
176
|
+
static getPRChangedFiles(prContext, gitRoot) {
|
|
177
|
+
try {
|
|
178
|
+
const { baseBranch } = prContext;
|
|
179
|
+
|
|
180
|
+
// Try to find the base branch reference
|
|
181
|
+
const baseRef = this.findBaseRef(baseBranch, gitRoot);
|
|
182
|
+
|
|
183
|
+
if (!baseRef) {
|
|
184
|
+
throw new Error(`Cannot find base branch: ${baseBranch}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Ensure we have the latest base branch
|
|
188
|
+
this.ensureBaseRefExists(baseRef, gitRoot);
|
|
189
|
+
|
|
190
|
+
// Use merge-base to find the common ancestor
|
|
191
|
+
let mergeBase;
|
|
192
|
+
try {
|
|
193
|
+
mergeBase = execSync(`git merge-base ${baseRef} HEAD`, {
|
|
194
|
+
cwd: gitRoot,
|
|
195
|
+
encoding: 'utf8'
|
|
196
|
+
}).trim();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// If merge-base fails, fall back to direct comparison
|
|
199
|
+
console.warn(`Warning: Could not find merge-base, using direct diff with ${baseRef}`);
|
|
200
|
+
mergeBase = baseRef;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Get all files changed from merge-base to HEAD
|
|
204
|
+
const command = `git diff --name-only ${mergeBase}...HEAD`;
|
|
40
205
|
const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
|
|
41
|
-
|
|
42
|
-
|
|
206
|
+
|
|
207
|
+
const changedFiles = output
|
|
43
208
|
.split('\n')
|
|
44
209
|
.filter(file => file.trim() !== '')
|
|
45
210
|
.map(file => path.resolve(gitRoot, file))
|
|
46
|
-
.filter(file => fs.existsSync(file));
|
|
211
|
+
.filter(file => fs.existsSync(file));
|
|
212
|
+
|
|
213
|
+
return changedFiles;
|
|
47
214
|
} catch (error) {
|
|
48
|
-
throw new Error(`Failed to get changed files: ${error.message}`);
|
|
215
|
+
throw new Error(`Failed to get PR changed files: ${error.message}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Find base reference for the given branch name
|
|
221
|
+
* @param {string} baseBranch - Base branch name
|
|
222
|
+
* @param {string} gitRoot - Git repository root path
|
|
223
|
+
* @returns {string|null} Base reference or null if not found
|
|
224
|
+
*/
|
|
225
|
+
static findBaseRef(baseBranch, gitRoot) {
|
|
226
|
+
const candidates = [
|
|
227
|
+
`origin/${baseBranch}`,
|
|
228
|
+
`upstream/${baseBranch}`,
|
|
229
|
+
baseBranch
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
for (const candidate of candidates) {
|
|
233
|
+
try {
|
|
234
|
+
execSync(`git rev-parse --verify ${candidate}`, {
|
|
235
|
+
cwd: gitRoot,
|
|
236
|
+
stdio: 'ignore'
|
|
237
|
+
});
|
|
238
|
+
return candidate;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// Continue to next candidate
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Ensure base ref exists (fetch if necessary)
|
|
249
|
+
* @param {string} baseRef - Base reference
|
|
250
|
+
* @param {string} gitRoot - Git repository root path
|
|
251
|
+
*/
|
|
252
|
+
static ensureBaseRefExists(baseRef, gitRoot) {
|
|
253
|
+
try {
|
|
254
|
+
// Check if ref exists
|
|
255
|
+
execSync(`git rev-parse --verify ${baseRef}`, {
|
|
256
|
+
cwd: gitRoot,
|
|
257
|
+
stdio: 'ignore'
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// Try to fetch if it doesn't exist
|
|
261
|
+
const remote = baseRef.split('/')[0];
|
|
262
|
+
if (remote === 'origin' || remote === 'upstream') {
|
|
263
|
+
try {
|
|
264
|
+
console.log(`Fetching ${remote}...`);
|
|
265
|
+
execSync(`git fetch ${remote} --depth=1`, {
|
|
266
|
+
cwd: gitRoot,
|
|
267
|
+
stdio: 'inherit'
|
|
268
|
+
});
|
|
269
|
+
} catch (fetchError) {
|
|
270
|
+
console.warn(`Warning: Failed to fetch ${remote}: ${fetchError.message}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
49
273
|
}
|
|
50
274
|
}
|
|
51
275
|
|
|
@@ -182,14 +182,56 @@ function readJsonFile(jsonFile) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Get git root directory
|
|
187
|
+
* @param {string} cwd - Current working directory
|
|
188
|
+
* @returns {string} Git root path
|
|
189
|
+
*/
|
|
190
|
+
function getGitRoot(cwd = process.cwd()) {
|
|
191
|
+
try {
|
|
192
|
+
const { execSync } = require('child_process');
|
|
193
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', {
|
|
194
|
+
cwd,
|
|
195
|
+
encoding: 'utf8'
|
|
196
|
+
}).trim();
|
|
197
|
+
return gitRoot;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.warn('Not a git repository, using cwd as root');
|
|
200
|
+
return cwd;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Normalize path to be relative from git root
|
|
206
|
+
* @param {string} filePath - File path (absolute or relative)
|
|
207
|
+
* @param {string} gitRoot - Git root directory
|
|
208
|
+
* @returns {string} Normalized relative path
|
|
209
|
+
*/
|
|
210
|
+
function normalizePathFromGitRoot(filePath, gitRoot) {
|
|
211
|
+
let normalized = filePath;
|
|
212
|
+
|
|
213
|
+
// Convert absolute path to relative from git root
|
|
214
|
+
if (filePath.startsWith(gitRoot)) {
|
|
215
|
+
normalized = filePath.slice(gitRoot.length);
|
|
216
|
+
if (normalized.startsWith('/') || normalized.startsWith('\\')) {
|
|
217
|
+
normalized = normalized.slice(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Normalize path separators to forward slash
|
|
222
|
+
normalized = normalized.replace(/\\/g, '/');
|
|
223
|
+
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
|
|
185
227
|
/**
|
|
186
228
|
* Parse violations from JSON data
|
|
187
229
|
* @param {Array|Object} raw - Raw JSON data
|
|
230
|
+
* @param {string} gitRoot - Git root directory for path normalization
|
|
188
231
|
* @returns {Array} Array of violation objects
|
|
189
232
|
*/
|
|
190
|
-
function parseViolations(raw) {
|
|
233
|
+
function parseViolations(raw, gitRoot) {
|
|
191
234
|
const violations = [];
|
|
192
|
-
const cwd = process.env.GITHUB_WORKSPACE || process.cwd();
|
|
193
235
|
|
|
194
236
|
if (Array.isArray(raw)) {
|
|
195
237
|
for (const fileObj of raw) {
|
|
@@ -203,18 +245,8 @@ function parseViolations(raw) {
|
|
|
203
245
|
continue;
|
|
204
246
|
}
|
|
205
247
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// Convert absolute path to relative
|
|
209
|
-
if (relPath.startsWith(cwd)) {
|
|
210
|
-
relPath = relPath.slice(cwd.length);
|
|
211
|
-
if (relPath.startsWith('/') || relPath.startsWith('\\')) {
|
|
212
|
-
relPath = relPath.slice(1);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Normalize path separators
|
|
217
|
-
relPath = relPath.replace(/\\/g, '/');
|
|
248
|
+
// Normalize path relative to git root (same as GitHub API)
|
|
249
|
+
const relPath = normalizePathFromGitRoot(fileObj.filePath, gitRoot);
|
|
218
250
|
|
|
219
251
|
for (const msg of fileObj.messages) {
|
|
220
252
|
if (!msg || typeof msg !== 'object') {
|
|
@@ -243,7 +275,12 @@ function parseViolations(raw) {
|
|
|
243
275
|
if (!Array.isArray(rawViolations)) {
|
|
244
276
|
throw new Error('violations property must be an array');
|
|
245
277
|
}
|
|
246
|
-
|
|
278
|
+
|
|
279
|
+
// Normalize paths for raw violations too
|
|
280
|
+
violations.push(...rawViolations.map(v => ({
|
|
281
|
+
...v,
|
|
282
|
+
file: normalizePathFromGitRoot(v.file, gitRoot)
|
|
283
|
+
})));
|
|
247
284
|
} else {
|
|
248
285
|
throw new Error('JSON data must be an array or object with violations property');
|
|
249
286
|
}
|
|
@@ -521,9 +558,13 @@ async function annotate({
|
|
|
521
558
|
logger.info('Reading result file', { jsonFile });
|
|
522
559
|
const raw = readJsonFile(jsonFile);
|
|
523
560
|
|
|
524
|
-
// Step
|
|
561
|
+
// Step 2.5: Get git root for path normalization
|
|
562
|
+
const gitRoot = getGitRoot();
|
|
563
|
+
logger.debug('Git root directory', { gitRoot });
|
|
564
|
+
|
|
565
|
+
// Step 3: Parse violations with git root normalization
|
|
525
566
|
logger.info('Parsing violations');
|
|
526
|
-
const violations = parseViolations(raw);
|
|
567
|
+
const violations = parseViolations(raw, gitRoot);
|
|
527
568
|
|
|
528
569
|
if (violations.length === 0) {
|
|
529
570
|
logger.info('No violations found');
|
|
@@ -607,6 +648,14 @@ async function annotate({
|
|
|
607
648
|
let linesSkipped = 0;
|
|
608
649
|
let renamedFilesHandled = 0;
|
|
609
650
|
|
|
651
|
+
// Debug: Log sample paths for comparison
|
|
652
|
+
logger.debug('Path comparison debug:', {
|
|
653
|
+
sampleViolationFiles: violations.slice(0, 3).map(v => v.file),
|
|
654
|
+
samplePRFiles: Array.from(prFilesInfo.keys()).slice(0, 3),
|
|
655
|
+
totalViolations: violations.length,
|
|
656
|
+
totalPRFiles: prFilesInfo.size
|
|
657
|
+
});
|
|
658
|
+
|
|
610
659
|
for (const v of violations) {
|
|
611
660
|
let targetFile = v.file;
|
|
612
661
|
let fileInfo = prFilesInfo.get(targetFile);
|
|
@@ -813,9 +862,13 @@ async function postSummaryComment({
|
|
|
813
862
|
logger.info('Reading result file', { jsonFile });
|
|
814
863
|
const raw = readJsonFile(jsonFile);
|
|
815
864
|
|
|
816
|
-
// Step
|
|
865
|
+
// Step 2.5: Get git root for path normalization
|
|
866
|
+
const gitRoot = getGitRoot();
|
|
867
|
+
logger.debug('Git root directory', { gitRoot });
|
|
868
|
+
|
|
869
|
+
// Step 3: Parse violations with git root normalization
|
|
817
870
|
logger.info('Parsing violations for summary');
|
|
818
|
-
const violations = parseViolations(raw);
|
|
871
|
+
const violations = parseViolations(raw, gitRoot);
|
|
819
872
|
|
|
820
873
|
// Step 4: Initialize Octokit
|
|
821
874
|
const token = githubToken || process.env.GITHUB_TOKEN;
|
package/core/output-service.js
CHANGED
|
@@ -28,9 +28,9 @@ class OutputService {
|
|
|
28
28
|
try {
|
|
29
29
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
30
30
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
31
|
-
return packageJson.version || '1.3.
|
|
31
|
+
return packageJson.version || '1.3.21';
|
|
32
32
|
} catch (error) {
|
|
33
|
-
return '1.3.
|
|
33
|
+
return '1.3.21'; // Fallback version
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -44,7 +44,8 @@ class OutputService {
|
|
|
44
44
|
const report = this.generateReport(results, metadata, { ...options, format: effectiveFormat });
|
|
45
45
|
|
|
46
46
|
// Console output
|
|
47
|
-
|
|
47
|
+
// Skip console output when using --github-annotate to avoid JSON clutter
|
|
48
|
+
if (!options.quiet && !githubAnnotateConfig.shouldAnnotate) {
|
|
48
49
|
console.log(report.formatted);
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -90,15 +90,28 @@ jobs:
|
|
|
90
90
|
|
|
91
91
|
## Advanced Usage
|
|
92
92
|
|
|
93
|
-
### 1. Analyze only changed files
|
|
93
|
+
### 1. Analyze only changed files (Auto-detect PR)
|
|
94
|
+
|
|
95
|
+
**⭐ Tính năng mới**: Tự động phát hiện PR context và sử dụng merge-base để diff chính xác!
|
|
94
96
|
|
|
95
97
|
```yaml
|
|
96
|
-
- name: Run SunLint on Changed Files
|
|
98
|
+
- name: Run SunLint on Changed Files (Auto-detect)
|
|
97
99
|
run: sunlint --all --changed-files --github-annotate
|
|
98
100
|
env:
|
|
99
101
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
100
102
|
```
|
|
101
103
|
|
|
104
|
+
**Cách hoạt động**:
|
|
105
|
+
- ✅ Tự động detect GitHub Actions PR event (`GITHUB_EVENT_NAME=pull_request`)
|
|
106
|
+
- ✅ Tự động lấy base branch từ `GITHUB_BASE_REF`
|
|
107
|
+
- ✅ Sử dụng `git merge-base` để tìm common ancestor
|
|
108
|
+
- ✅ So sánh với merge-base thay vì HEAD → Lấy đúng các file thay đổi trong PR
|
|
109
|
+
- ✅ Tự động fetch base branch nếu cần
|
|
110
|
+
|
|
111
|
+
**Không cần chỉ định `--diff-base` nữa!** SunLint sẽ tự động xử lý.
|
|
112
|
+
|
|
113
|
+
**Fallback**: Nếu không phát hiện được PR context, sẽ fallback về logic cũ (so sánh với HEAD hoặc origin/main)
|
|
114
|
+
|
|
102
115
|
### 2. Save report file + annotate
|
|
103
116
|
|
|
104
117
|
```yaml
|
|
@@ -819,7 +819,14 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
819
819
|
// Group files by language for efficient processing
|
|
820
820
|
const filesByLanguage = this.groupFilesByLanguage(files);
|
|
821
821
|
|
|
822
|
+
// Track progress across rules
|
|
823
|
+
const totalRules = rules.length;
|
|
824
|
+
let processedRules = 0;
|
|
825
|
+
|
|
822
826
|
for (const rule of rules) {
|
|
827
|
+
processedRules++;
|
|
828
|
+
const ruleProgress = Math.floor((processedRules / totalRules) * 100);
|
|
829
|
+
|
|
823
830
|
// Special case: Load C047 semantic rule on-demand
|
|
824
831
|
if (rule.id === 'C047' && !this.semanticRules.has('C047')) {
|
|
825
832
|
if (options.verbose) {
|
|
@@ -827,7 +834,7 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
827
834
|
}
|
|
828
835
|
await this.manuallyLoadC047();
|
|
829
836
|
}
|
|
830
|
-
|
|
837
|
+
|
|
831
838
|
// Lazy load rule if not already loaded
|
|
832
839
|
if (!this.isRuleSupported(rule.id)) {
|
|
833
840
|
if (options.verbose) {
|
|
@@ -835,7 +842,7 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
835
842
|
}
|
|
836
843
|
await this.lazyLoadRule(rule.id, options);
|
|
837
844
|
}
|
|
838
|
-
|
|
845
|
+
|
|
839
846
|
if (!this.isRuleSupported(rule.id)) {
|
|
840
847
|
if (options.verbose) {
|
|
841
848
|
console.warn(`⚠️ Rule ${rule.id} not supported by Heuristic engine, skipping...`);
|
|
@@ -845,25 +852,29 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
845
852
|
|
|
846
853
|
try {
|
|
847
854
|
let ruleViolations = [];
|
|
848
|
-
|
|
855
|
+
|
|
849
856
|
// Check if this is a semantic rule first (higher priority)
|
|
850
857
|
if (this.semanticRules.has(rule.id)) {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
858
|
+
const progressInfo = options.batchInfo?.overallProgress !== undefined
|
|
859
|
+
? `[${options.batchInfo.overallProgress}% overall] `
|
|
860
|
+
: '';
|
|
861
|
+
console.log(`🧠 ${progressInfo}Rule ${processedRules}/${totalRules} (${ruleProgress}%): ${rule.id} - Analyzing ${files.length} files...`);
|
|
862
|
+
|
|
854
863
|
ruleViolations = await this.analyzeSemanticRule(rule, files, options);
|
|
855
864
|
} else {
|
|
856
865
|
// Fallback to traditional analysis
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
866
|
+
const progressInfo = options.batchInfo?.overallProgress !== undefined
|
|
867
|
+
? `[${options.batchInfo.overallProgress}% overall] `
|
|
868
|
+
: '';
|
|
869
|
+
console.log(`🔧 ${progressInfo}Rule ${processedRules}/${totalRules} (${ruleProgress}%): ${rule.id} - Analyzing ${files.length} files...`);
|
|
870
|
+
|
|
860
871
|
ruleViolations = await this.analyzeRule(rule, filesByLanguage, options);
|
|
861
872
|
}
|
|
862
873
|
|
|
863
874
|
if (ruleViolations.length > 0) {
|
|
864
875
|
// Group violations by file
|
|
865
876
|
const violationsByFile = this.groupViolationsByFile(ruleViolations);
|
|
866
|
-
|
|
877
|
+
|
|
867
878
|
for (const [filePath, violations] of violationsByFile) {
|
|
868
879
|
// Find or create file result
|
|
869
880
|
let fileResult = results.results.find(r => r.file === filePath);
|
|
@@ -875,8 +886,14 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
875
886
|
}
|
|
876
887
|
}
|
|
877
888
|
|
|
889
|
+
// Log completion
|
|
890
|
+
const progressInfo = options.batchInfo?.overallProgress !== undefined
|
|
891
|
+
? `[${options.batchInfo.overallProgress}% overall] `
|
|
892
|
+
: '';
|
|
893
|
+
console.log(`✅ ${progressInfo}${rule.id}: Found ${ruleViolations.length} violations`);
|
|
894
|
+
|
|
878
895
|
results.metadata.analyzersUsed.push(rule.id);
|
|
879
|
-
|
|
896
|
+
|
|
880
897
|
} catch (error) {
|
|
881
898
|
console.error(`❌ Failed to analyze rule ${rule.id}:`, error.message);
|
|
882
899
|
// Continue with other rules
|
|
@@ -911,16 +928,28 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
911
928
|
|
|
912
929
|
const allViolations = [];
|
|
913
930
|
|
|
914
|
-
// Run semantic analysis for each file
|
|
931
|
+
// Run semantic analysis for each file with progress tracking
|
|
932
|
+
const totalFiles = files.length;
|
|
933
|
+
let processedFiles = 0;
|
|
934
|
+
let lastReportedProgress = 0;
|
|
935
|
+
|
|
915
936
|
for (const filePath of files) {
|
|
916
937
|
try {
|
|
917
|
-
|
|
918
|
-
|
|
938
|
+
processedFiles++;
|
|
939
|
+
const currentProgress = Math.floor((processedFiles / totalFiles) * 100);
|
|
940
|
+
|
|
941
|
+
// Report progress every 10% or when verbose
|
|
942
|
+
if (options.verbose || (currentProgress >= lastReportedProgress + 10 && currentProgress < 100)) {
|
|
943
|
+
const progressInfo = options.batchInfo?.overallProgress !== undefined
|
|
944
|
+
? `[${options.batchInfo.overallProgress}% overall] `
|
|
945
|
+
: '';
|
|
946
|
+
console.log(`🧠 ${progressInfo}${rule.id}: Processing file ${processedFiles}/${totalFiles} (${currentProgress}%) - ${path.basename(filePath)}`);
|
|
947
|
+
lastReportedProgress = currentProgress;
|
|
919
948
|
}
|
|
920
|
-
|
|
949
|
+
|
|
921
950
|
// Call semantic rule's analyzeFile method
|
|
922
951
|
await ruleInstance.analyzeFile(filePath, options);
|
|
923
|
-
|
|
952
|
+
|
|
924
953
|
// Get violations from the rule instance
|
|
925
954
|
const fileViolations = ruleInstance.getViolations();
|
|
926
955
|
allViolations.push(...fileViolations);
|
package/package.json
CHANGED