@sun-asterisk/sunlint 1.3.21 → 1.3.23
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/git-utils.js +205 -0
- package/core/output-service.js +2 -1
- package/docs/GITHUB_ACTIONS_INTEGRATION.md +68 -5
- 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
|
package/core/git-utils.js
CHANGED
|
@@ -121,6 +121,23 @@ class GitUtils {
|
|
|
121
121
|
// Get git root directory
|
|
122
122
|
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
|
|
123
123
|
|
|
124
|
+
// Check if we're in PR context
|
|
125
|
+
const prContext = this.detectPRContext();
|
|
126
|
+
|
|
127
|
+
// If in PR context and no explicit baseRef, try PR-specific logic
|
|
128
|
+
if (prContext && !baseRef) {
|
|
129
|
+
try {
|
|
130
|
+
const prFiles = this.getPRChangedFiles(prContext, gitRoot);
|
|
131
|
+
if (prFiles && prFiles.length >= 0) {
|
|
132
|
+
return prFiles;
|
|
133
|
+
}
|
|
134
|
+
} catch (prError) {
|
|
135
|
+
// Log warning and fallback to standard logic
|
|
136
|
+
console.warn(`⚠️ PR context detected but failed to get changed files: ${prError.message}`);
|
|
137
|
+
console.log(` Falling back to standard git diff logic...`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
// Auto-detect base ref if not provided
|
|
125
142
|
const actualBaseRef = baseRef || this.getSmartBaseRef(cwd);
|
|
126
143
|
|
|
@@ -159,6 +176,194 @@ class GitUtils {
|
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Get changed files in PR context using merge-base
|
|
181
|
+
* @param {Object} prContext - PR context info from detectPRContext()
|
|
182
|
+
* @param {string} gitRoot - Git repository root path
|
|
183
|
+
* @returns {string[]} Array of changed file paths in the PR
|
|
184
|
+
*/
|
|
185
|
+
static getPRChangedFiles(prContext, gitRoot) {
|
|
186
|
+
try {
|
|
187
|
+
const { baseBranch, provider } = prContext;
|
|
188
|
+
|
|
189
|
+
console.log(`🔍 Detecting changed files for PR (provider: ${provider}, base: ${baseBranch})`);
|
|
190
|
+
|
|
191
|
+
// Try to find the base branch reference
|
|
192
|
+
let baseRef = this.findBaseRef(baseBranch, gitRoot);
|
|
193
|
+
|
|
194
|
+
if (!baseRef) {
|
|
195
|
+
console.log(`⚠️ Base ref not found locally, attempting to fetch origin/${baseBranch}...`);
|
|
196
|
+
|
|
197
|
+
// Try to fetch and create the ref
|
|
198
|
+
const fetchSuccess = this.ensureBaseRefExists(`origin/${baseBranch}`, gitRoot);
|
|
199
|
+
|
|
200
|
+
if (fetchSuccess) {
|
|
201
|
+
baseRef = `origin/${baseBranch}`;
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error(`Cannot find or fetch base branch: ${baseBranch}`);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Ensure we have the latest
|
|
207
|
+
console.log(`✅ Found base ref: ${baseRef}`);
|
|
208
|
+
this.ensureBaseRefExists(baseRef, gitRoot);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Use merge-base to find the common ancestor
|
|
212
|
+
let mergeBase;
|
|
213
|
+
try {
|
|
214
|
+
mergeBase = execSync(`git merge-base ${baseRef} HEAD`, {
|
|
215
|
+
cwd: gitRoot,
|
|
216
|
+
encoding: 'utf8'
|
|
217
|
+
}).trim();
|
|
218
|
+
console.log(`✅ Found merge-base: ${mergeBase.substring(0, 8)}`);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// If merge-base fails, fall back to direct comparison
|
|
221
|
+
console.warn(`⚠️ Could not find merge-base, using direct diff with ${baseRef}`);
|
|
222
|
+
mergeBase = baseRef;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get all files changed from merge-base to HEAD
|
|
226
|
+
const command = `git diff --name-only ${mergeBase}...HEAD`;
|
|
227
|
+
const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
|
|
228
|
+
|
|
229
|
+
const changedFiles = output
|
|
230
|
+
.split('\n')
|
|
231
|
+
.filter(file => file.trim() !== '')
|
|
232
|
+
.map(file => path.resolve(gitRoot, file))
|
|
233
|
+
.filter(file => fs.existsSync(file));
|
|
234
|
+
|
|
235
|
+
console.log(`✅ Found ${changedFiles.length} changed files in PR`);
|
|
236
|
+
|
|
237
|
+
return changedFiles;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw new Error(`Failed to get PR changed files: ${error.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Find base reference for the given branch name
|
|
245
|
+
* @param {string} baseBranch - Base branch name
|
|
246
|
+
* @param {string} gitRoot - Git repository root path
|
|
247
|
+
* @returns {string|null} Base reference or null if not found
|
|
248
|
+
*/
|
|
249
|
+
static findBaseRef(baseBranch, gitRoot) {
|
|
250
|
+
const candidates = [
|
|
251
|
+
`origin/${baseBranch}`,
|
|
252
|
+
`upstream/${baseBranch}`,
|
|
253
|
+
baseBranch
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
for (const candidate of candidates) {
|
|
257
|
+
try {
|
|
258
|
+
execSync(`git rev-parse --verify ${candidate}`, {
|
|
259
|
+
cwd: gitRoot,
|
|
260
|
+
stdio: 'ignore'
|
|
261
|
+
});
|
|
262
|
+
return candidate;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
// Continue to next candidate
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Ensure base ref exists (fetch if necessary)
|
|
273
|
+
* @param {string} baseRef - Base reference
|
|
274
|
+
* @param {string} gitRoot - Git repository root path
|
|
275
|
+
*/
|
|
276
|
+
static ensureBaseRefExists(baseRef, gitRoot) {
|
|
277
|
+
try {
|
|
278
|
+
// Check if ref exists
|
|
279
|
+
execSync(`git rev-parse --verify ${baseRef}`, {
|
|
280
|
+
cwd: gitRoot,
|
|
281
|
+
stdio: 'ignore'
|
|
282
|
+
});
|
|
283
|
+
// Ref exists, return true
|
|
284
|
+
return true;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
// Try to fetch if it doesn't exist
|
|
287
|
+
const parts = baseRef.split('/');
|
|
288
|
+
const remote = parts[0];
|
|
289
|
+
const branch = parts.slice(1).join('/');
|
|
290
|
+
|
|
291
|
+
if (remote === 'origin' || remote === 'upstream') {
|
|
292
|
+
try {
|
|
293
|
+
console.log(`⬇️ Fetching ${remote}/${branch}...`);
|
|
294
|
+
|
|
295
|
+
// Check if this is a shallow repository (common in GitHub Actions)
|
|
296
|
+
const isShallow = this.isShallowRepository(gitRoot);
|
|
297
|
+
|
|
298
|
+
if (isShallow) {
|
|
299
|
+
console.log(` ℹ️ Detected shallow clone, fetching with additional history...`);
|
|
300
|
+
|
|
301
|
+
// For shallow clones, we need to:
|
|
302
|
+
// 1. Fetch the base branch
|
|
303
|
+
// 2. Get enough history to find merge-base
|
|
304
|
+
try {
|
|
305
|
+
// Unshallow current branch first to get more history
|
|
306
|
+
execSync(`git fetch --deepen=50`, {
|
|
307
|
+
cwd: gitRoot,
|
|
308
|
+
stdio: 'pipe',
|
|
309
|
+
encoding: 'utf8'
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Then fetch the base branch with history
|
|
313
|
+
execSync(`git fetch ${remote} ${branch} --depth=50`, {
|
|
314
|
+
cwd: gitRoot,
|
|
315
|
+
stdio: 'pipe',
|
|
316
|
+
encoding: 'utf8'
|
|
317
|
+
});
|
|
318
|
+
} catch (shallowError) {
|
|
319
|
+
// If deepen fails, try direct fetch
|
|
320
|
+
console.log(` ℹ️ Trying direct fetch...`);
|
|
321
|
+
execSync(`git fetch ${remote} ${branch}`, {
|
|
322
|
+
cwd: gitRoot,
|
|
323
|
+
stdio: 'pipe',
|
|
324
|
+
encoding: 'utf8'
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
// Normal fetch for non-shallow repos
|
|
329
|
+
execSync(`git fetch ${remote} ${branch}`, {
|
|
330
|
+
cwd: gitRoot,
|
|
331
|
+
stdio: 'pipe',
|
|
332
|
+
encoding: 'utf8'
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Verify it now exists
|
|
337
|
+
execSync(`git rev-parse --verify ${baseRef}`, {
|
|
338
|
+
cwd: gitRoot,
|
|
339
|
+
stdio: 'ignore'
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
console.log(`✅ Successfully fetched ${baseRef}`);
|
|
343
|
+
return true;
|
|
344
|
+
} catch (fetchError) {
|
|
345
|
+
console.warn(`⚠️ Failed to fetch ${baseRef}: ${fetchError.message}`);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check if repository is shallow
|
|
355
|
+
* @param {string} gitRoot - Git repository root path
|
|
356
|
+
* @returns {boolean} True if shallow
|
|
357
|
+
*/
|
|
358
|
+
static isShallowRepository(gitRoot) {
|
|
359
|
+
try {
|
|
360
|
+
const shallowFile = path.join(gitRoot, '.git', 'shallow');
|
|
361
|
+
return fs.existsSync(shallowFile);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
162
367
|
/**
|
|
163
368
|
* Get list of staged files
|
|
164
369
|
* @param {string} cwd - Working directory
|
package/core/output-service.js
CHANGED
|
@@ -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,78 @@ 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, tự động fetch base branch, và sử dụng merge-base để diff chính xác!
|
|
94
96
|
|
|
95
97
|
```yaml
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
steps:
|
|
99
|
+
- name: Checkout code
|
|
100
|
+
uses: actions/checkout@v4
|
|
101
|
+
with:
|
|
102
|
+
fetch-depth: 0 # Recommended: fetch full history for accurate diff
|
|
103
|
+
|
|
104
|
+
- name: Run SunLint on Changed Files (Auto-detect)
|
|
105
|
+
run: sunlint --all --changed-files --github-annotate
|
|
106
|
+
env:
|
|
107
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**✨ Đơn giản nhất có thể - không cần config gì thêm!**
|
|
111
|
+
|
|
112
|
+
**Cách hoạt động**:
|
|
113
|
+
|
|
114
|
+
- ✅ Tự động detect GitHub Actions PR event (`GITHUB_EVENT_NAME=pull_request`)
|
|
115
|
+
- ✅ Tự động lấy base branch từ `GITHUB_BASE_REF`
|
|
116
|
+
- ✅ **Tự động fetch base branch nếu chưa có** (không cần thêm step!)
|
|
117
|
+
- ✅ Detect shallow clone và fetch thêm history nếu cần
|
|
118
|
+
- ✅ Sử dụng `git merge-base` để tìm common ancestor
|
|
119
|
+
- ✅ So sánh với merge-base thay vì HEAD → Lấy đúng các file thay đổi trong PR
|
|
120
|
+
|
|
121
|
+
**Không cần:**
|
|
122
|
+
|
|
123
|
+
- ❌ Chỉ định `--diff-base`
|
|
124
|
+
- ❌ Thêm step fetch base branch thủ công
|
|
125
|
+
- ❌ Config phức tạp
|
|
126
|
+
|
|
127
|
+
**Fallback**: Nếu không phát hiện được PR context hoặc không fetch được base branch, sẽ fallback về standard git diff logic
|
|
128
|
+
|
|
129
|
+
**❓ Có cần `fetch-depth: 0` không?**
|
|
130
|
+
|
|
131
|
+
**Không bắt buộc!** Nhưng **strongly recommended** vì performance.
|
|
132
|
+
|
|
133
|
+
| Option | Performance | Checkout Time | Total Time | Accuracy |
|
|
134
|
+
|--------|-------------|---------------|------------|----------|
|
|
135
|
+
| **With `fetch-depth: 0`** ⭐ | Fast | +2-3s | **Fastest** | 100% |
|
|
136
|
+
| Without (shallow clone) | Slower | Fast | Slower | ~95% |
|
|
137
|
+
|
|
138
|
+
**Với `fetch-depth: 0` (Recommended):**
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
- uses: actions/checkout@v4
|
|
142
|
+
with:
|
|
143
|
+
fetch-depth: 0 # One-time full fetch
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- ✅ **Fastest total time** - fetch once, use immediately
|
|
147
|
+
- ✅ **100% accurate** - full history for merge-base
|
|
148
|
+
- ✅ **Simpler** - no runtime fetch needed
|
|
149
|
+
|
|
150
|
+
**Không có `fetch-depth: 0` (Vẫn work):**
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
- uses: actions/checkout@v4 # Shallow clone (depth=1)
|
|
100
154
|
```
|
|
101
155
|
|
|
156
|
+
- ✅ Fast checkout
|
|
157
|
+
- ⚠️ **Slower total time** - SunLint must fetch at runtime:
|
|
158
|
+
- Detect shallow clone
|
|
159
|
+
- `git fetch --deepen=50`
|
|
160
|
+
- `git fetch origin/main --depth=50`
|
|
161
|
+
- ⚠️ May miss history for very large PRs
|
|
162
|
+
|
|
163
|
+
**Recommendation**: Dùng `fetch-depth: 0` trừ khi bạn có lý do đặc biệt (e.g., monorepo cực lớn)
|
|
164
|
+
|
|
102
165
|
### 2. Save report file + annotate
|
|
103
166
|
|
|
104
167
|
```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