fchek 1.0.0
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/README.md +64 -0
- package/bin/fchek.js +107 -0
- package/lib/api.js +110 -0
- package/lib/audit.js +211 -0
- package/lib/bench.js +248 -0
- package/lib/config.js +191 -0
- package/lib/context.js +356 -0
- package/lib/convention.js +526 -0
- package/lib/coverage.js +604 -0
- package/lib/db.js +135 -0
- package/lib/deps-check.js +264 -0
- package/lib/deps.js +374 -0
- package/lib/docker.js +84 -0
- package/lib/doctor.js +149 -0
- package/lib/dom.js +226 -0
- package/lib/fuzz.js +470 -0
- package/lib/git.js +290 -0
- package/lib/goto.js +544 -0
- package/lib/launch.js +182 -0
- package/lib/lint.js +624 -0
- package/lib/new_features.test.js +181 -0
- package/lib/output.js +46 -0
- package/lib/port.js +173 -0
- package/lib/process.js +228 -0
- package/lib/profile.js +453 -0
- package/lib/python.js +41 -0
- package/lib/race.js +186 -0
- package/lib/registry.js +179 -0
- package/lib/repl.js +135 -0
- package/lib/run.js +403 -0
- package/lib/screenshot.js +152 -0
- package/lib/secrets.js +257 -0
- package/lib/state.js +219 -0
- package/lib/test.js +471 -0
- package/lib/vuln.js +253 -0
- package/lib/watch.js +240 -0
- package/lib/winlog.js +123 -0
- package/package.json +27 -0
- package/skills/ACTIVATE.md +274 -0
- package/skills/README.md +163 -0
- package/skills/agent.md +444 -0
- package/skills/api.md +47 -0
- package/skills/bench.md +117 -0
- package/skills/context.md +116 -0
- package/skills/convention.md +143 -0
- package/skills/coverage.md +99 -0
- package/skills/csharp.md +97 -0
- package/skills/db.md +66 -0
- package/skills/deps-check.md +135 -0
- package/skills/deps.md +143 -0
- package/skills/docker.md +61 -0
- package/skills/dom.md +56 -0
- package/skills/fuzz.md +167 -0
- package/skills/goto.md +111 -0
- package/skills/lint.md +123 -0
- package/skills/port.md +57 -0
- package/skills/profile.md +91 -0
- package/skills/race.md +117 -0
- package/skills/repl.md +81 -0
- package/skills/rules.md +318 -0
- package/skills/run.md +135 -0
- package/skills/secrets.md +170 -0
- package/skills/security.md +360 -0
- package/skills/state.md +261 -0
- package/skills/vuln.md +57 -0
- package/skills/windows.md +320 -0
package/lib/lint.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lint.js — linter + auto-fix + show diff of what changed
|
|
5
|
+
*
|
|
6
|
+
* Python (.py) → ruff check --fix (fast, modern)
|
|
7
|
+
* Rust → cargo clippy --fix
|
|
8
|
+
* JS/TS → eslint --fix
|
|
9
|
+
* C/C++ → clang-tidy --fix-errors (or clang-format)
|
|
10
|
+
* Go → gofmt -w + go vet
|
|
11
|
+
*
|
|
12
|
+
* Always:
|
|
13
|
+
* 1. Read file before fix
|
|
14
|
+
* 2. Apply fixes
|
|
15
|
+
* 3. Read file after fix
|
|
16
|
+
* 4. Build unified diff
|
|
17
|
+
* 5. Return issues + diff in JSON
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { spawnSync, execSync } = require('child_process');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const { output, ok, fail } = require('./output');
|
|
24
|
+
|
|
25
|
+
const HELP = `
|
|
26
|
+
fchek lint <file_or_dir> [--no-fix] [--strict]
|
|
27
|
+
|
|
28
|
+
Run linter, apply safe auto-fixes, and show a diff of what changed.
|
|
29
|
+
|
|
30
|
+
Supported:
|
|
31
|
+
.py → ruff (pip install ruff)
|
|
32
|
+
.rs / Cargo → cargo clippy --fix
|
|
33
|
+
.js / .ts → eslint --fix (npm install -g eslint)
|
|
34
|
+
.c / .cpp → clang-tidy + clang-format
|
|
35
|
+
.go → gofmt -w + go vet
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--no-fix Report issues only, do not modify files
|
|
39
|
+
--strict Include warnings (not just errors)
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
fchek lint main.py
|
|
43
|
+
fchek lint src/
|
|
44
|
+
fchek lint app.ts --no-fix
|
|
45
|
+
fchek lint . --strict (Rust: cargo clippy in cwd)
|
|
46
|
+
`.trim();
|
|
47
|
+
|
|
48
|
+
const DEFAULT_TIMEOUT = 60_000;
|
|
49
|
+
|
|
50
|
+
function commandExists(cmd) {
|
|
51
|
+
try {
|
|
52
|
+
execSync(process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
|
|
53
|
+
return true;
|
|
54
|
+
} catch { return false; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readFile(p) {
|
|
58
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Build a simple unified-diff-like structure between two strings */
|
|
62
|
+
function buildLineDiff(before, after, filePath) {
|
|
63
|
+
if (before === after) return [];
|
|
64
|
+
const bLines = before.split('\n');
|
|
65
|
+
const aLines = after.split('\n');
|
|
66
|
+
const hunks = [];
|
|
67
|
+
const maxLen = Math.max(bLines.length, aLines.length);
|
|
68
|
+
|
|
69
|
+
let i = 0;
|
|
70
|
+
while (i < maxLen) {
|
|
71
|
+
if (bLines[i] !== aLines[i]) {
|
|
72
|
+
const hunkStart = i;
|
|
73
|
+
const hunk = { file: filePath, from_line: i + 1, changes: [] };
|
|
74
|
+
while (i < maxLen && (bLines[i] !== aLines[i] || (i - hunkStart < 3))) {
|
|
75
|
+
if (i >= maxLen) break;
|
|
76
|
+
if (bLines[i] !== aLines[i]) {
|
|
77
|
+
if (bLines[i] !== undefined) hunk.changes.push({ op: '-', line: i + 1, text: bLines[i] });
|
|
78
|
+
if (aLines[i] !== undefined) hunk.changes.push({ op: '+', line: i + 1, text: aLines[i] });
|
|
79
|
+
} else {
|
|
80
|
+
hunk.changes.push({ op: ' ', line: i + 1, text: bLines[i] });
|
|
81
|
+
}
|
|
82
|
+
i++;
|
|
83
|
+
if (hunk.changes.length > 20) break;
|
|
84
|
+
}
|
|
85
|
+
hunks.push(hunk);
|
|
86
|
+
} else {
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return hunks.slice(0, 15); // cap hunks
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Python: ruff ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function lintPython(target, fix, strict, timeoutMs) {
|
|
96
|
+
if (!commandExists('ruff')) {
|
|
97
|
+
return output(fail('ruff not found. Install: pip install ruff'));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 1. Capture issues BEFORE fixing
|
|
101
|
+
const checkArgs = ['check', '--output-format=json', target];
|
|
102
|
+
const checkRes = spawnSync('ruff', checkArgs, { encoding: 'utf8', timeout: timeoutMs });
|
|
103
|
+
|
|
104
|
+
let issuesBefore = [];
|
|
105
|
+
try { issuesBefore = JSON.parse(checkRes.stdout || '[]'); } catch {}
|
|
106
|
+
|
|
107
|
+
let diff = [];
|
|
108
|
+
let issuesAfter = issuesBefore;
|
|
109
|
+
|
|
110
|
+
if (fix) {
|
|
111
|
+
// Snapshot files before fix
|
|
112
|
+
const snapshotBefore = snapshotFiles(target, ['.py']);
|
|
113
|
+
|
|
114
|
+
const fixArgs = ['check', '--fix', target];
|
|
115
|
+
spawnSync('ruff', fixArgs, { encoding: 'utf8', timeout: timeoutMs });
|
|
116
|
+
spawnSync('ruff', ['format', target], { encoding: 'utf8', timeout: timeoutMs });
|
|
117
|
+
|
|
118
|
+
// Snapshot after fix
|
|
119
|
+
const snapshotAfter = snapshotFiles(target, ['.py']);
|
|
120
|
+
|
|
121
|
+
// Build diff for changed files
|
|
122
|
+
for (const [file, before] of Object.entries(snapshotBefore)) {
|
|
123
|
+
const after = snapshotAfter[file];
|
|
124
|
+
if (after && before !== after) {
|
|
125
|
+
diff.push(...buildLineDiff(before, after, file));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Re-check to see remaining issues
|
|
130
|
+
const recheckRes = spawnSync('ruff', ['check', '--output-format=json', target], { encoding: 'utf8', timeout: timeoutMs });
|
|
131
|
+
try { issuesAfter = JSON.parse(recheckRes.stdout || '[]'); } catch {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fixedCount = issuesBefore.length - issuesAfter.length;
|
|
135
|
+
|
|
136
|
+
output(ok({
|
|
137
|
+
target,
|
|
138
|
+
lang: 'python',
|
|
139
|
+
tool: 'ruff',
|
|
140
|
+
fix_applied: fix,
|
|
141
|
+
issues_before: issuesBefore.length,
|
|
142
|
+
issues_after: issuesAfter.length,
|
|
143
|
+
fixed_count: fixedCount,
|
|
144
|
+
remaining_issues: issuesAfter.slice(0, 30).map(i => ({
|
|
145
|
+
file: i.filename,
|
|
146
|
+
line: i.location?.row,
|
|
147
|
+
col: i.location?.column,
|
|
148
|
+
code: i.code,
|
|
149
|
+
message: i.message,
|
|
150
|
+
})),
|
|
151
|
+
diff: diff.slice(0, 20),
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Rust: cargo clippy ───────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
function lintRust(target, fix, strict, timeoutMs) {
|
|
158
|
+
if (!commandExists('cargo')) {
|
|
159
|
+
return output(fail('cargo not found. Install Rust: https://rustup.rs'));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const cwd = path.resolve(target === '.' ? process.cwd() : target);
|
|
163
|
+
const strictFlag = strict ? ['--', '-W', 'clippy::all'] : [];
|
|
164
|
+
|
|
165
|
+
// Run clippy check first to get issues
|
|
166
|
+
const checkArgs = ['clippy', '--message-format=json', ...strictFlag];
|
|
167
|
+
const checkRes = spawnSync('cargo', checkArgs, { encoding: 'utf8', cwd, timeout: timeoutMs });
|
|
168
|
+
|
|
169
|
+
const issuesBefore = parseCargoMessages(checkRes.stdout || '');
|
|
170
|
+
|
|
171
|
+
let diff = [];
|
|
172
|
+
let issuesAfter = issuesBefore;
|
|
173
|
+
let fixNote = null;
|
|
174
|
+
|
|
175
|
+
if (fix) {
|
|
176
|
+
// cargo clippy --fix requires either a clean git repo OR --allow-dirty/--allow-staged.
|
|
177
|
+
// If not a git repo at all, those flags still cause an error on some versions.
|
|
178
|
+
// Detect whether we are inside a git repo.
|
|
179
|
+
const isGitRepo = (() => {
|
|
180
|
+
try {
|
|
181
|
+
const r = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
182
|
+
encoding: 'utf8', cwd, timeout: 3000,
|
|
183
|
+
});
|
|
184
|
+
return r.status === 0;
|
|
185
|
+
} catch { return false; }
|
|
186
|
+
})();
|
|
187
|
+
|
|
188
|
+
const fixArgs = isGitRepo
|
|
189
|
+
? ['clippy', '--fix', '--allow-dirty', '--allow-staged', ...strictFlag]
|
|
190
|
+
: ['clippy', '--fix', ...strictFlag]; // no git flags if no repo
|
|
191
|
+
|
|
192
|
+
const snapshotBefore = snapshotDir(cwd, ['.rs']);
|
|
193
|
+
const fixRes = spawnSync('cargo', fixArgs, { encoding: 'utf8', cwd, timeout: timeoutMs });
|
|
194
|
+
|
|
195
|
+
// If clippy --fix failed because of git check even without the flags, report clearly
|
|
196
|
+
if (fixRes.status !== 0) {
|
|
197
|
+
const errOut = (fixRes.stderr || '');
|
|
198
|
+
if (errOut.includes('no VCS found') || errOut.includes('uncommitted changes')) {
|
|
199
|
+
fixNote = 'cargo clippy --fix requires a git repository or clean working tree. ' +
|
|
200
|
+
'Run: git init && git add . && git commit -m init — then re-run fchek lint.';
|
|
201
|
+
// Still report issues found, just without fixes applied
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const snapshotAfter = snapshotDir(cwd, ['.rs']);
|
|
206
|
+
for (const [file, before] of Object.entries(snapshotBefore)) {
|
|
207
|
+
const after = snapshotAfter[file];
|
|
208
|
+
if (after && before !== after) {
|
|
209
|
+
diff.push(...buildLineDiff(before, after, file));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const recheckRes = spawnSync('cargo', checkArgs, { encoding: 'utf8', cwd, timeout: timeoutMs });
|
|
214
|
+
issuesAfter = parseCargoMessages(recheckRes.stdout || '');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
output(ok({
|
|
218
|
+
target: cwd,
|
|
219
|
+
lang: 'rust',
|
|
220
|
+
tool: 'cargo-clippy',
|
|
221
|
+
fix_applied: fix && !fixNote,
|
|
222
|
+
fix_note: fixNote,
|
|
223
|
+
issues_before: issuesBefore.length,
|
|
224
|
+
issues_after: issuesAfter.length,
|
|
225
|
+
fixed_count: issuesBefore.length - issuesAfter.length,
|
|
226
|
+
remaining_issues: issuesAfter.slice(0, 20),
|
|
227
|
+
diff: diff.slice(0, 20),
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseCargoMessages(raw) {
|
|
232
|
+
const issues = [];
|
|
233
|
+
for (const line of raw.split('\n')) {
|
|
234
|
+
try {
|
|
235
|
+
const msg = JSON.parse(line);
|
|
236
|
+
if (msg.reason === 'compiler-message' && msg.message?.level === 'warning') {
|
|
237
|
+
issues.push({
|
|
238
|
+
message: msg.message.message,
|
|
239
|
+
code: msg.message.code?.code ?? null,
|
|
240
|
+
spans: (msg.message.spans || []).slice(0, 2).map(s => ({
|
|
241
|
+
file: s.file_name, line: s.line_start,
|
|
242
|
+
})),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
} catch {}
|
|
246
|
+
}
|
|
247
|
+
return issues;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── JS/TS: eslint ────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function lintJs(target, fix, strict, timeoutMs) {
|
|
253
|
+
const eslint = commandExists('eslint') ? 'eslint' : null;
|
|
254
|
+
if (!eslint) {
|
|
255
|
+
return output(fail('eslint not found. Install: npm install -g eslint'));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const snapshotBefore = snapshotFiles(target, ['.js', '.ts', '.jsx', '.tsx', '.mjs']);
|
|
259
|
+
const checkArgs = [target, '--format=json', fix ? '--fix' : ''];
|
|
260
|
+
const res = spawnSync(eslint, checkArgs.filter(Boolean), { encoding: 'utf8', timeout: timeoutMs });
|
|
261
|
+
|
|
262
|
+
let issues = [];
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(res.stdout || '[]');
|
|
265
|
+
for (const file of parsed) {
|
|
266
|
+
for (const msg of file.messages || []) {
|
|
267
|
+
if (!strict && msg.severity < 2) continue;
|
|
268
|
+
issues.push({
|
|
269
|
+
file: file.filePath,
|
|
270
|
+
line: msg.line,
|
|
271
|
+
col: msg.column,
|
|
272
|
+
rule: msg.ruleId,
|
|
273
|
+
message: msg.message,
|
|
274
|
+
severity: msg.severity === 2 ? 'error' : 'warning',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch {}
|
|
279
|
+
|
|
280
|
+
let diff = [];
|
|
281
|
+
if (fix) {
|
|
282
|
+
const snapshotAfter = snapshotFiles(target, ['.js', '.ts', '.jsx', '.tsx', '.mjs']);
|
|
283
|
+
for (const [file, before] of Object.entries(snapshotBefore)) {
|
|
284
|
+
const after = snapshotAfter[file];
|
|
285
|
+
if (after && before !== after) diff.push(...buildLineDiff(before, after, file));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
output(ok({
|
|
290
|
+
target,
|
|
291
|
+
lang: 'javascript',
|
|
292
|
+
tool: 'eslint',
|
|
293
|
+
fix_applied: fix,
|
|
294
|
+
remaining_issues: issues.slice(0, 30),
|
|
295
|
+
issue_count: issues.length,
|
|
296
|
+
diff: diff.slice(0, 20),
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── C#: dotnet format + dotnet build ────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
function lintCSharp(target, fix, timeoutMs) {
|
|
303
|
+
if (!commandExists('dotnet')) {
|
|
304
|
+
return output(fail('dotnet not found. Install .NET SDK: https://dotnet.microsoft.com/download'));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Find project root (where .csproj or .sln lives)
|
|
308
|
+
let projDir = path.resolve(fs.statSync(target).isDirectory() ? target : path.dirname(target));
|
|
309
|
+
for (let i = 0; i < 6; i++) {
|
|
310
|
+
const found = fs.readdirSync(projDir).find(f => f.endsWith('.csproj') || f.endsWith('.sln'));
|
|
311
|
+
if (found) break;
|
|
312
|
+
const parent = path.dirname(projDir);
|
|
313
|
+
if (parent === projDir) { projDir = null; break; }
|
|
314
|
+
projDir = parent;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!projDir) {
|
|
318
|
+
return output(fail('No .csproj or .sln found. Run: dotnet new console -o MyApp'));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Snapshot .cs files before fix
|
|
322
|
+
const snapshotBefore = fix ? snapshotFiles(projDir, ['.cs']) : {};
|
|
323
|
+
|
|
324
|
+
// Run dotnet format
|
|
325
|
+
const formatArgs = fix
|
|
326
|
+
? ['format', projDir]
|
|
327
|
+
: ['format', projDir, '--verify-no-changes'];
|
|
328
|
+
|
|
329
|
+
const formatRes = spawnSync('dotnet', formatArgs, {
|
|
330
|
+
encoding: 'utf8', cwd: projDir, timeout: timeoutMs,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Build diff if fix was applied
|
|
334
|
+
const diff = [];
|
|
335
|
+
if (fix) {
|
|
336
|
+
const snapshotAfter = snapshotFiles(projDir, ['.cs']);
|
|
337
|
+
for (const [file, before] of Object.entries(snapshotBefore)) {
|
|
338
|
+
const after = snapshotAfter[file];
|
|
339
|
+
if (after && before !== after) diff.push(...buildLineDiff(before, after, file));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Run dotnet build to catch compiler errors/warnings
|
|
344
|
+
const buildRes = spawnSync('dotnet', ['build', projDir, '--no-restore', '-v', 'quiet'],
|
|
345
|
+
{ encoding: 'utf8', cwd: projDir, timeout: timeoutMs });
|
|
346
|
+
|
|
347
|
+
const buildOutput = (buildRes.stdout || '') + (buildRes.stderr || '');
|
|
348
|
+
const issues = [];
|
|
349
|
+
for (const line of buildOutput.split('\n')) {
|
|
350
|
+
// Format: file.cs(line,col): warning/error CS1234: message
|
|
351
|
+
const m = line.match(/^(.+\.cs)\((\d+),(\d+)\):\s+(warning|error)\s+(CS\d+):\s+(.+)$/);
|
|
352
|
+
if (m) {
|
|
353
|
+
issues.push({
|
|
354
|
+
file: m[1], line: parseInt(m[2]), col: parseInt(m[3]),
|
|
355
|
+
severity: m[4], code: m[5], message: m[6].trim(),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const formatFailed = !fix && formatRes.status !== 0;
|
|
361
|
+
|
|
362
|
+
output(ok({
|
|
363
|
+
target: projDir,
|
|
364
|
+
lang: 'csharp',
|
|
365
|
+
tools: ['dotnet format', 'dotnet build'],
|
|
366
|
+
fix_applied: fix,
|
|
367
|
+
format_needs_changes: formatFailed,
|
|
368
|
+
issues_count: issues.length,
|
|
369
|
+
remaining_issues: issues.slice(0, 30),
|
|
370
|
+
diff: diff.slice(0, 20),
|
|
371
|
+
build_clean: buildRes.status === 0,
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ─── C/C++: clang-format + clang-tidy ────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
function lintC(file, fix, timeoutMs) {
|
|
378
|
+
const results = { issues: [], diff: [] };
|
|
379
|
+
|
|
380
|
+
// clang-format
|
|
381
|
+
if (commandExists('clang-format')) {
|
|
382
|
+
const before = readFile(file);
|
|
383
|
+
const fmtRes = spawnSync('clang-format', [file], { encoding: 'utf8', timeout: timeoutMs });
|
|
384
|
+
const after = fmtRes.stdout || '';
|
|
385
|
+
if (fix && before && before !== after) {
|
|
386
|
+
fs.writeFileSync(file, after, 'utf8');
|
|
387
|
+
results.diff.push(...buildLineDiff(before, after, file));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// clang-tidy issues (no auto-fix to avoid breaking changes)
|
|
392
|
+
if (commandExists('clang-tidy')) {
|
|
393
|
+
const tidyRes = spawnSync('clang-tidy', [file, '--'], { encoding: 'utf8', timeout: timeoutMs });
|
|
394
|
+
const lines = (tidyRes.stdout || tidyRes.stderr || '').split('\n');
|
|
395
|
+
for (const line of lines) {
|
|
396
|
+
const m = line.match(/^(.+):(\d+):(\d+):\s+(warning|error):\s+(.+)\[(.+)\]$/);
|
|
397
|
+
if (m) {
|
|
398
|
+
results.issues.push({
|
|
399
|
+
file: m[1], line: parseInt(m[2]), col: parseInt(m[3]),
|
|
400
|
+
severity: m[4], message: m[5], code: m[6],
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
output(ok({
|
|
407
|
+
target: path.resolve(file),
|
|
408
|
+
lang: 'c',
|
|
409
|
+
tools: ['clang-format', 'clang-tidy'],
|
|
410
|
+
fix_applied: fix,
|
|
411
|
+
remaining_issues: results.issues.slice(0, 30),
|
|
412
|
+
issue_count: results.issues.length,
|
|
413
|
+
diff: results.diff.slice(0, 20),
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─── Go: gofmt + go vet ──────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
function lintGo(target, fix, timeoutMs) {
|
|
420
|
+
if (!commandExists('gofmt')) {
|
|
421
|
+
return output(fail('gofmt not found. Install Go: https://go.dev'));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const files = fs.statSync(target).isDirectory()
|
|
425
|
+
? findFiles(target, ['.go'])
|
|
426
|
+
: [target];
|
|
427
|
+
|
|
428
|
+
const allDiff = [];
|
|
429
|
+
const vetIssues = [];
|
|
430
|
+
|
|
431
|
+
for (const file of files) {
|
|
432
|
+
const before = readFile(file);
|
|
433
|
+
const fmtRes = spawnSync('gofmt', ['-l', file], { encoding: 'utf8', timeout: 10_000 });
|
|
434
|
+
const needsFmt = (fmtRes.stdout || '').trim() !== '';
|
|
435
|
+
|
|
436
|
+
if (fix && needsFmt) {
|
|
437
|
+
spawnSync('gofmt', ['-w', file], { encoding: 'utf8' });
|
|
438
|
+
const after = readFile(file);
|
|
439
|
+
if (before && after && before !== after) {
|
|
440
|
+
allDiff.push(...buildLineDiff(before, after, file));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// go vet
|
|
446
|
+
const vetRes = spawnSync('go', ['vet', './...'], { encoding: 'utf8', cwd: path.resolve(target), timeout: timeoutMs });
|
|
447
|
+
for (const line of (vetRes.stderr || '').split('\n')) {
|
|
448
|
+
if (line.trim()) vetIssues.push(line.trim());
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
output(ok({
|
|
452
|
+
target,
|
|
453
|
+
lang: 'go',
|
|
454
|
+
tools: ['gofmt', 'go vet'],
|
|
455
|
+
fix_applied: fix,
|
|
456
|
+
vet_issues: vetIssues.slice(0, 20),
|
|
457
|
+
issue_count: vetIssues.length,
|
|
458
|
+
diff: allDiff.slice(0, 20),
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
function findFiles(dir, exts) {
|
|
465
|
+
const results = [];
|
|
466
|
+
const ignored = new Set(['node_modules', '.git', 'dist', 'target', 'build']);
|
|
467
|
+
function walk(d) {
|
|
468
|
+
let entries;
|
|
469
|
+
try { entries = fs.readdirSync(d); } catch { return; }
|
|
470
|
+
for (const name of entries) {
|
|
471
|
+
if (ignored.has(name)) continue;
|
|
472
|
+
const full = path.join(d, name);
|
|
473
|
+
const stat = fs.statSync(full);
|
|
474
|
+
if (stat.isDirectory()) walk(full);
|
|
475
|
+
else if (exts.includes(path.extname(name).toLowerCase())) results.push(full);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
walk(dir);
|
|
479
|
+
return results;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function snapshotFiles(target, exts) {
|
|
483
|
+
const snap = {};
|
|
484
|
+
const stat = fs.existsSync(target) && fs.statSync(target);
|
|
485
|
+
if (!stat) return snap;
|
|
486
|
+
const files = stat.isDirectory() ? findFiles(target, exts) : [target];
|
|
487
|
+
for (const f of files) {
|
|
488
|
+
const content = readFile(f);
|
|
489
|
+
if (content !== null) snap[f] = content;
|
|
490
|
+
}
|
|
491
|
+
return snap;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function snapshotDir(dir, exts) {
|
|
495
|
+
return snapshotFiles(dir, exts);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
async function run(args) {
|
|
501
|
+
if (args.length === 0 || args[0] === '--help') {
|
|
502
|
+
console.log(HELP);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const target = args[0];
|
|
507
|
+
const noFix = args.includes('--no-fix');
|
|
508
|
+
const strict = args.includes('--strict');
|
|
509
|
+
const fix = !noFix;
|
|
510
|
+
|
|
511
|
+
if (!fs.existsSync(target)) {
|
|
512
|
+
return output(fail(`Not found: ${target}`));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const ext = path.extname(target).toLowerCase();
|
|
516
|
+
const isDir = fs.statSync(target).isDirectory();
|
|
517
|
+
|
|
518
|
+
// Single file — route directly by extension
|
|
519
|
+
if (!isDir) {
|
|
520
|
+
if (ext === '.cs') return lintCSharp(target, fix, DEFAULT_TIMEOUT);
|
|
521
|
+
if (ext === '.py') return lintPython(target, fix, strict, DEFAULT_TIMEOUT);
|
|
522
|
+
if (ext === '.rs') return lintRust(target, fix, strict, DEFAULT_TIMEOUT);
|
|
523
|
+
if (['.js','.ts','.jsx','.tsx','.mjs'].includes(ext)) return lintJs(target, fix, strict, DEFAULT_TIMEOUT);
|
|
524
|
+
if (['.c','.cpp','.cc'].includes(ext)) return lintC(target, fix, DEFAULT_TIMEOUT);
|
|
525
|
+
if (ext === '.go') return lintGo(target, fix, DEFAULT_TIMEOUT);
|
|
526
|
+
return output(fail(`Cannot detect linter for: ${target}. Supported: .py .rs .js .ts .c .cpp .go .cs`));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Directory — detect ALL languages present and run each linter
|
|
530
|
+
const hasCsproj = fs.readdirSync(target).some(f => f.endsWith('.csproj') || f.endsWith('.sln'));
|
|
531
|
+
const hasCargo = fs.existsSync(path.join(target, 'Cargo.toml'));
|
|
532
|
+
const hasPkgJson = fs.existsSync(path.join(target, 'package.json'));
|
|
533
|
+
const hasGoMod = fs.existsSync(path.join(target, 'go.mod'));
|
|
534
|
+
|
|
535
|
+
const hasPy = findFiles(target, ['.py']).length > 0;
|
|
536
|
+
const hasJs = findFiles(target, ['.js', '.ts', '.jsx', '.tsx', '.mjs']).length > 0;
|
|
537
|
+
const hasC = findFiles(target, ['.c', '.cpp', '.cc']).length > 0;
|
|
538
|
+
const hasCs = findFiles(target, ['.cs']).length > 0;
|
|
539
|
+
const hasGo = findFiles(target, ['.go']).length > 0;
|
|
540
|
+
|
|
541
|
+
const langs = [];
|
|
542
|
+
if (hasCsproj || hasCs) langs.push('csharp');
|
|
543
|
+
if (hasCargo) langs.push('rust');
|
|
544
|
+
if (hasPkgJson || hasJs) langs.push('javascript');
|
|
545
|
+
if (hasGoMod || hasGo) langs.push('go');
|
|
546
|
+
if (hasPy) langs.push('python');
|
|
547
|
+
if (hasC && !hasCargo) langs.push('c');
|
|
548
|
+
|
|
549
|
+
if (langs.length === 0) {
|
|
550
|
+
return output(fail(`No supported source files found in: ${target}. Supported: .py .rs .js .ts .c .cpp .go .cs`));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Single language — run directly (returns output itself)
|
|
554
|
+
if (langs.length === 1) {
|
|
555
|
+
const lang = langs[0];
|
|
556
|
+
if (lang === 'csharp') return lintCSharp(target, fix, DEFAULT_TIMEOUT);
|
|
557
|
+
if (lang === 'rust') return lintRust(target, fix, strict, DEFAULT_TIMEOUT);
|
|
558
|
+
if (lang === 'javascript') return lintJs(target, fix, strict, DEFAULT_TIMEOUT);
|
|
559
|
+
if (lang === 'go') return lintGo(target, fix, DEFAULT_TIMEOUT);
|
|
560
|
+
if (lang === 'python') return lintPython(target, fix, strict, DEFAULT_TIMEOUT);
|
|
561
|
+
if (lang === 'c') return lintC(target, fix, DEFAULT_TIMEOUT);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Multiple languages — capture output from each linter and combine
|
|
565
|
+
const results = [];
|
|
566
|
+
let totalIssues = 0;
|
|
567
|
+
|
|
568
|
+
for (const lang of langs) {
|
|
569
|
+
// Temporarily capture console.log to get the JSON result
|
|
570
|
+
const lines = [];
|
|
571
|
+
const origLog = console.log;
|
|
572
|
+
console.log = (s) => lines.push(s);
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
if (lang === 'csharp') await lintCSharp(target, fix, DEFAULT_TIMEOUT);
|
|
576
|
+
else if (lang === 'rust') await lintRust(target, fix, strict, DEFAULT_TIMEOUT);
|
|
577
|
+
else if (lang === 'javascript') await lintJs(target, fix, strict, DEFAULT_TIMEOUT);
|
|
578
|
+
else if (lang === 'go') await lintGo(target, fix, DEFAULT_TIMEOUT);
|
|
579
|
+
else if (lang === 'python') await lintPython(target, fix, strict, DEFAULT_TIMEOUT);
|
|
580
|
+
else if (lang === 'c') {
|
|
581
|
+
// C linter works per-file — collect all .c/.cpp files
|
|
582
|
+
const cFiles = findFiles(target, ['.c', '.cpp', '.cc']);
|
|
583
|
+
for (const cFile of cFiles.slice(0, 20)) {
|
|
584
|
+
await lintC(cFile, fix, DEFAULT_TIMEOUT);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch {}
|
|
588
|
+
|
|
589
|
+
console.log = origLog;
|
|
590
|
+
|
|
591
|
+
// Parse last JSON line from captured output
|
|
592
|
+
let result = null;
|
|
593
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
594
|
+
try { result = JSON.parse(lines[i]); break; } catch {}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (result?.data) {
|
|
598
|
+
const d = result.data;
|
|
599
|
+
const issues = d.issues_count ?? d.issue_count ?? d.remaining_issues?.length ?? d.vet_issues?.length ?? 0;
|
|
600
|
+
totalIssues += issues;
|
|
601
|
+
results.push({
|
|
602
|
+
lang,
|
|
603
|
+
tool: d.tool ?? d.tools ?? null,
|
|
604
|
+
issues_count: issues,
|
|
605
|
+
fixed_count: d.fixed_count ?? null,
|
|
606
|
+
build_clean: d.build_clean ?? null,
|
|
607
|
+
diff_count: d.diff?.length ?? 0,
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
results.push({ lang, error: result?.error ?? 'no output' });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
output(ok({
|
|
615
|
+
target,
|
|
616
|
+
langs_detected: langs,
|
|
617
|
+
total_issues: totalIssues,
|
|
618
|
+
fix_applied: fix,
|
|
619
|
+
results,
|
|
620
|
+
verdict: totalIssues === 0 ? 'clean' : 'issues_found',
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
module.exports = { run };
|