ai-code-reviewer-plus 0.1.0 → 1.0.0-beta.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/index.js +0 -0
- package/package.json +12 -64
- package/.claude/skills/ai-code-reviewer-plus/CLAUDE.md +0 -36
- package/.claude/skills/ai-code-reviewer-plus/skill.md +0 -77
- package/.claude-plugin/marketplace.json +0 -14
- package/.claude-plugin/plugin.json +0 -9
- package/LICENSE +0 -21
- package/README.md +0 -263
- package/dist/analyzers/index.d.ts +0 -31
- package/dist/analyzers/index.test.d.ts +0 -1
- package/dist/chunk-VRJ4NJAF.mjs +0 -670
- package/dist/cli.d.ts +0 -4
- package/dist/cli.js +0 -315
- package/dist/cli.mjs +0 -121
- package/dist/collectors/diff-collector.d.ts +0 -19
- package/dist/collectors/diff-collector.test.d.ts +0 -1
- package/dist/collectors/project-detector.d.ts +0 -8
- package/dist/collectors/project-detector.test.d.ts +0 -1
- package/dist/errors.d.ts +0 -46
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -713
- package/dist/index.mjs +0 -40
- package/dist/types.d.ts +0 -146
- package/dist/utils/config.d.ts +0 -16
- package/dist/utils/format.d.ts +0 -24
- package/dist/utils/format.test.d.ts +0 -1
package/dist/chunk-VRJ4NJAF.mjs
DELETED
|
@@ -1,670 +0,0 @@
|
|
|
1
|
-
// src/analyzers/index.ts
|
|
2
|
-
var RULES = [
|
|
3
|
-
// Security rules
|
|
4
|
-
{
|
|
5
|
-
id: "SEC-001",
|
|
6
|
-
dimension: "security",
|
|
7
|
-
severity: "BLOCKER",
|
|
8
|
-
pattern: /\.innerHTML\s*=\s*/,
|
|
9
|
-
title: "XSS: Direct innerHTML assignment",
|
|
10
|
-
description: "Assigning to innerHTML with unsanitized input creates an XSS vulnerability.",
|
|
11
|
-
suggestion: "Use textContent or sanitize input before rendering.",
|
|
12
|
-
fixExample: "element.textContent = sanitize(userInput)"
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: "SEC-002",
|
|
16
|
-
dimension: "security",
|
|
17
|
-
severity: "BLOCKER",
|
|
18
|
-
pattern: /eval\s*\(/,
|
|
19
|
-
title: "XSS: Use of eval()",
|
|
20
|
-
description: "eval() executes arbitrary code, creating a severe security risk.",
|
|
21
|
-
suggestion: "Avoid eval(). Use JSON.parse() for data or Function constructor for controlled logic.",
|
|
22
|
-
fixExample: "const data = JSON.parse(jsonString)"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: "SEC-003",
|
|
26
|
-
dimension: "security",
|
|
27
|
-
severity: "HIGH",
|
|
28
|
-
pattern: /document\.write\s*\(/,
|
|
29
|
-
title: "XSS: Use of document.write()",
|
|
30
|
-
description: "document.write() can inject arbitrary HTML, enabling XSS attacks.",
|
|
31
|
-
suggestion: "Use DOM manipulation methods like createElement and appendChild."
|
|
32
|
-
},
|
|
33
|
-
// Correctness rules
|
|
34
|
-
{
|
|
35
|
-
id: "COR-001",
|
|
36
|
-
dimension: "correctness",
|
|
37
|
-
severity: "HIGH",
|
|
38
|
-
pattern: /\.\w+\s*\(\s*\)\s*\.\s*\w+\s*\(\s*\)\s*\.\s*\w+\s*\(\s*\)/,
|
|
39
|
-
title: "Potential null reference: Long method chain",
|
|
40
|
-
description: "Chained method calls without null checks can throw TypeError at runtime.",
|
|
41
|
-
suggestion: "Add null checks or use optional chaining (?.).",
|
|
42
|
-
fixExample: "obj?.method1()?.method2()?.method3()"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "COR-002",
|
|
46
|
-
dimension: "correctness",
|
|
47
|
-
severity: "MEDIUM",
|
|
48
|
-
pattern: /==(?!=)/,
|
|
49
|
-
title: "Loose equality comparison",
|
|
50
|
-
description: "Using == instead of === can produce unexpected type coercion results.",
|
|
51
|
-
suggestion: "Use === for strict equality comparison.",
|
|
52
|
-
fixExample: "value === expected"
|
|
53
|
-
},
|
|
54
|
-
// Performance rules
|
|
55
|
-
{
|
|
56
|
-
id: "PER-001",
|
|
57
|
-
dimension: "performance",
|
|
58
|
-
severity: "HIGH",
|
|
59
|
-
pattern: /for\s*\(.*await/,
|
|
60
|
-
title: "Sequential async in loop",
|
|
61
|
-
description: "Using await inside a for loop processes operations sequentially, often causing N+1 latency.",
|
|
62
|
-
suggestion: "Use Promise.all() for parallel execution, or process in batches.",
|
|
63
|
-
fixExample: "await Promise.all(items.map(i => fetch(i)))"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: "PER-002",
|
|
67
|
-
dimension: "performance",
|
|
68
|
-
severity: "MEDIUM",
|
|
69
|
-
pattern: /console\.log\s*\(/,
|
|
70
|
-
title: "Console.log in production code",
|
|
71
|
-
description: "Leftover console.log calls add overhead and may leak sensitive data.",
|
|
72
|
-
suggestion: "Remove console.log or use a proper logging library."
|
|
73
|
-
},
|
|
74
|
-
// Maintainability rules
|
|
75
|
-
{
|
|
76
|
-
id: "MAIN-001",
|
|
77
|
-
dimension: "maintainability",
|
|
78
|
-
severity: "LOW",
|
|
79
|
-
pattern: /\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*HACK/i,
|
|
80
|
-
title: "Unresolved TODO/FIXME comment",
|
|
81
|
-
description: "TODO/FIXME/HACK comments indicate unfinished or problematic code.",
|
|
82
|
-
suggestion: "Resolve the comment or create a tracked issue."
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "MAIN-002",
|
|
86
|
-
dimension: "maintainability",
|
|
87
|
-
severity: "LOW",
|
|
88
|
-
pattern: /:\s*any\b/,
|
|
89
|
-
title: "Use of `any` type",
|
|
90
|
-
description: "Using any bypasses TypeScript type checking, reducing type safety.",
|
|
91
|
-
suggestion: "Replace any with a specific type or unknown.",
|
|
92
|
-
fixExample: "const data: unknown = value"
|
|
93
|
-
},
|
|
94
|
-
// Best practices rules
|
|
95
|
-
{
|
|
96
|
-
id: "BP-001",
|
|
97
|
-
dimension: "best-practices",
|
|
98
|
-
severity: "MEDIUM",
|
|
99
|
-
pattern: /var\s+\w/,
|
|
100
|
-
title: "Use of var instead of let/const",
|
|
101
|
-
description: "var has function scope and hoisting, which can cause subtle bugs.",
|
|
102
|
-
suggestion: "Use const for values that don't change, let for those that do.",
|
|
103
|
-
fixExample: "const value = 42"
|
|
104
|
-
}
|
|
105
|
-
];
|
|
106
|
-
function analyzeDiffs(diffs, enabledRules, disabledRules, severityOverrides) {
|
|
107
|
-
const findings = [];
|
|
108
|
-
const activeRules = filterRules(RULES, enabledRules, disabledRules);
|
|
109
|
-
for (const diff of diffs) {
|
|
110
|
-
for (const hunk of diff.hunks) {
|
|
111
|
-
for (const rule of activeRules) {
|
|
112
|
-
for (const line of hunk.lines) {
|
|
113
|
-
if (!line.startsWith("+")) continue;
|
|
114
|
-
const content = line.slice(1);
|
|
115
|
-
if (!rule.pattern.test(content)) continue;
|
|
116
|
-
const severity = severityOverrides?.[rule.id] ?? rule.severity;
|
|
117
|
-
findings.push({
|
|
118
|
-
id: rule.id,
|
|
119
|
-
dimension: rule.dimension,
|
|
120
|
-
severity,
|
|
121
|
-
title: rule.title,
|
|
122
|
-
description: rule.description,
|
|
123
|
-
file: diff.file,
|
|
124
|
-
line: hunk.newStart + countAddedLinesBefore(hunk, line),
|
|
125
|
-
codeSnippet: content.trim(),
|
|
126
|
-
suggestion: rule.suggestion,
|
|
127
|
-
fixExample: rule.fixExample,
|
|
128
|
-
confidence: "high"
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return findings;
|
|
135
|
-
}
|
|
136
|
-
function analyzeCode(code, file, enabledRules, disabledRules, severityOverrides) {
|
|
137
|
-
const findings = [];
|
|
138
|
-
const activeRules = filterRules(RULES, enabledRules, disabledRules);
|
|
139
|
-
const lines = code.split("\n");
|
|
140
|
-
for (let i = 0; i < lines.length; i++) {
|
|
141
|
-
for (const rule of activeRules) {
|
|
142
|
-
if (!rule.pattern.test(lines[i])) continue;
|
|
143
|
-
const severity = severityOverrides?.[rule.id] ?? rule.severity;
|
|
144
|
-
findings.push({
|
|
145
|
-
id: rule.id,
|
|
146
|
-
dimension: rule.dimension,
|
|
147
|
-
severity,
|
|
148
|
-
title: rule.title,
|
|
149
|
-
description: rule.description,
|
|
150
|
-
file,
|
|
151
|
-
line: i + 1,
|
|
152
|
-
codeSnippet: lines[i].trim(),
|
|
153
|
-
suggestion: rule.suggestion,
|
|
154
|
-
fixExample: rule.fixExample,
|
|
155
|
-
confidence: "high"
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return findings;
|
|
160
|
-
}
|
|
161
|
-
function getAvailableRules() {
|
|
162
|
-
const grouped = {};
|
|
163
|
-
for (const rule of RULES) {
|
|
164
|
-
;
|
|
165
|
-
(grouped[rule.dimension] ??= []).push(rule);
|
|
166
|
-
}
|
|
167
|
-
return grouped;
|
|
168
|
-
}
|
|
169
|
-
function filterRules(allRules, enabled, disabled) {
|
|
170
|
-
if (enabled && enabled.length > 0) {
|
|
171
|
-
const set = new Set(enabled);
|
|
172
|
-
return allRules.filter((r) => set.has(r.id));
|
|
173
|
-
}
|
|
174
|
-
if (disabled && disabled.length > 0) {
|
|
175
|
-
const set = new Set(disabled);
|
|
176
|
-
return allRules.filter((r) => !set.has(r.id));
|
|
177
|
-
}
|
|
178
|
-
return allRules;
|
|
179
|
-
}
|
|
180
|
-
function countAddedLinesBefore(hunk, targetLine) {
|
|
181
|
-
let count = 0;
|
|
182
|
-
for (const line of hunk.lines) {
|
|
183
|
-
if (line === targetLine) return count;
|
|
184
|
-
if (line.startsWith("+")) count++;
|
|
185
|
-
}
|
|
186
|
-
return count;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// src/collectors/diff-collector.ts
|
|
190
|
-
import { execFile } from "child_process";
|
|
191
|
-
import { promisify } from "util";
|
|
192
|
-
|
|
193
|
-
// src/errors.ts
|
|
194
|
-
var GitCommandError = class _GitCommandError extends Error {
|
|
195
|
-
constructor(options) {
|
|
196
|
-
const msg = options.message ?? `git ${[options.command, ...options.args ?? []].join(" ")} failed${options.exitCode != null ? ` (exit ${options.exitCode})` : ""}`;
|
|
197
|
-
super(msg);
|
|
198
|
-
this.name = "GitCommandError";
|
|
199
|
-
this.command = options.command ?? "git";
|
|
200
|
-
this.args = options.args ?? [];
|
|
201
|
-
this.exitCode = options.exitCode;
|
|
202
|
-
this.cwd = options.cwd;
|
|
203
|
-
if (options.cause && Error.captureStackTrace) {
|
|
204
|
-
Error.captureStackTrace(this, _GitCommandError);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
var ParseError = class extends Error {
|
|
209
|
-
constructor(options) {
|
|
210
|
-
const msg = options.message ?? `Failed to parse ${options.parser} output`;
|
|
211
|
-
super(msg);
|
|
212
|
-
this.name = "ParseError";
|
|
213
|
-
this.parser = options.parser;
|
|
214
|
-
this.rawInput = options.rawInput;
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
var ConfigError = class extends Error {
|
|
218
|
-
constructor(options) {
|
|
219
|
-
const msg = options.message ?? `Invalid configuration${options.field ? ` in field '${options.field}'` : ""}`;
|
|
220
|
-
super(msg);
|
|
221
|
-
this.name = "ConfigError";
|
|
222
|
-
this.filePath = options.filePath;
|
|
223
|
-
this.field = options.field;
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
// src/collectors/diff-collector.ts
|
|
228
|
-
var exec = promisify(execFile);
|
|
229
|
-
async function collectDiff(options) {
|
|
230
|
-
const { root, targetBranch = "main", commitHash } = options;
|
|
231
|
-
const args = ["diff", "--no-color"];
|
|
232
|
-
if (commitHash) {
|
|
233
|
-
args.push(commitHash);
|
|
234
|
-
} else {
|
|
235
|
-
args.push(targetBranch, "HEAD");
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
const { stdout } = await exec("git", args, { cwd: root, maxBuffer: 50 * 1024 * 1024 });
|
|
239
|
-
return parseDiffOutput(stdout);
|
|
240
|
-
} catch (err) {
|
|
241
|
-
throw new GitCommandError({
|
|
242
|
-
command: "diff",
|
|
243
|
-
args,
|
|
244
|
-
cwd: root,
|
|
245
|
-
message: "Failed to collect git diff",
|
|
246
|
-
cause: err instanceof Error ? err : void 0
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
async function collectCommitDiff(options) {
|
|
251
|
-
const { root, hash } = options;
|
|
252
|
-
const args = ["diff", "--no-color", hash];
|
|
253
|
-
try {
|
|
254
|
-
const { stdout } = await exec("git", args, { cwd: root });
|
|
255
|
-
return parseDiffOutput(stdout);
|
|
256
|
-
} catch (err) {
|
|
257
|
-
throw new GitCommandError({
|
|
258
|
-
command: "diff",
|
|
259
|
-
args,
|
|
260
|
-
cwd: root,
|
|
261
|
-
message: `Failed to collect diff for commit ${hash}`,
|
|
262
|
-
cause: err instanceof Error ? err : void 0
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function parseDiffOutput(stdout) {
|
|
267
|
-
if (!stdout.trim()) return [];
|
|
268
|
-
const diffs = [];
|
|
269
|
-
const fileSections = stdout.split(/^diff --git /m).filter((s) => s.trim());
|
|
270
|
-
for (const section of fileSections) {
|
|
271
|
-
const diff = parseFileSection(section);
|
|
272
|
-
if (diff) diffs.push(diff);
|
|
273
|
-
}
|
|
274
|
-
return diffs;
|
|
275
|
-
}
|
|
276
|
-
function parseFileSection(section) {
|
|
277
|
-
const lines = section.split("\n");
|
|
278
|
-
const headerLine = lines[0];
|
|
279
|
-
const pathMatch = headerLine?.match(/^a\/(.+) b\/(.+)/);
|
|
280
|
-
if (!pathMatch) return null;
|
|
281
|
-
const file = pathMatch[2];
|
|
282
|
-
const from = pathMatch[1];
|
|
283
|
-
let status = "modified", currentHunk = null, additions = 0, deletions = 0;
|
|
284
|
-
if (lines.some((l) => l.startsWith("new file mode"))) {
|
|
285
|
-
status = "added";
|
|
286
|
-
} else if (lines.some((l) => l.startsWith("deleted file mode"))) {
|
|
287
|
-
status = "deleted";
|
|
288
|
-
} else if (lines.some((l) => l.startsWith("rename from "))) {
|
|
289
|
-
status = "renamed";
|
|
290
|
-
}
|
|
291
|
-
const hunks = [];
|
|
292
|
-
for (const line of lines) {
|
|
293
|
-
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
294
|
-
if (hunkMatch) {
|
|
295
|
-
if (currentHunk) hunks.push(currentHunk);
|
|
296
|
-
currentHunk = {
|
|
297
|
-
header: line,
|
|
298
|
-
oldStart: Number(hunkMatch[1]),
|
|
299
|
-
oldCount: Number(hunkMatch[2] ?? 1),
|
|
300
|
-
newStart: Number(hunkMatch[3]),
|
|
301
|
-
newCount: Number(hunkMatch[4] ?? 1),
|
|
302
|
-
lines: []
|
|
303
|
-
};
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (line.startsWith("+") && currentHunk) {
|
|
307
|
-
additions++;
|
|
308
|
-
currentHunk.lines.push(line);
|
|
309
|
-
} else if (line.startsWith("-") && currentHunk && !line.startsWith("---")) {
|
|
310
|
-
deletions++;
|
|
311
|
-
currentHunk.lines.push(line);
|
|
312
|
-
} else if (line.startsWith(" ") && currentHunk) {
|
|
313
|
-
currentHunk.lines.push(line);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (currentHunk) hunks.push(currentHunk);
|
|
317
|
-
return {
|
|
318
|
-
file,
|
|
319
|
-
from: status === "renamed" || status === "deleted" ? from : void 0,
|
|
320
|
-
status,
|
|
321
|
-
additions,
|
|
322
|
-
deletions,
|
|
323
|
-
hunks
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// src/collectors/project-detector.ts
|
|
328
|
-
import { existsSync, readFileSync } from "fs";
|
|
329
|
-
import { join } from "path";
|
|
330
|
-
async function detectProject(root) {
|
|
331
|
-
const markers = [
|
|
332
|
-
{ type: "node", file: "package.json" },
|
|
333
|
-
{ type: "go", file: "go.mod" },
|
|
334
|
-
{ type: "python", file: "requirements.txt" },
|
|
335
|
-
{ type: "rust", file: "Cargo.toml" }
|
|
336
|
-
];
|
|
337
|
-
for (const marker of markers) {
|
|
338
|
-
const filePath = join(root, marker.file);
|
|
339
|
-
if (existsSync(filePath)) {
|
|
340
|
-
if (marker.type === "node") {
|
|
341
|
-
return detectNodeProject(root, filePath);
|
|
342
|
-
}
|
|
343
|
-
if (marker.type === "go") {
|
|
344
|
-
return {
|
|
345
|
-
type: "go",
|
|
346
|
-
language: "go",
|
|
347
|
-
packageManager: "go mod",
|
|
348
|
-
root
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
if (marker.type === "python") {
|
|
352
|
-
return {
|
|
353
|
-
type: "python",
|
|
354
|
-
language: "python",
|
|
355
|
-
packageManager: "pip",
|
|
356
|
-
root
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return {
|
|
362
|
-
type: "unknown",
|
|
363
|
-
language: "unknown",
|
|
364
|
-
root
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
function detectNodeProject(root, packageJsonPath) {
|
|
368
|
-
try {
|
|
369
|
-
const content = readFileSync(packageJsonPath, "utf-8");
|
|
370
|
-
const pkg = JSON.parse(content);
|
|
371
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
372
|
-
if (deps.vue || deps.Vue) {
|
|
373
|
-
const version = deps.vue || deps.Vue || "";
|
|
374
|
-
const isVue3 = version.startsWith("^3") || version.startsWith("3");
|
|
375
|
-
return {
|
|
376
|
-
type: "vue",
|
|
377
|
-
framework: isVue3 ? "vue3" : "vue2",
|
|
378
|
-
language: "typescript",
|
|
379
|
-
packageManager: detectPackageManager(root),
|
|
380
|
-
root
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
if (deps.react || deps.React) {
|
|
384
|
-
const version = deps.react || deps.React || "";
|
|
385
|
-
const isReact18 = version.startsWith("^18") || version.startsWith("18");
|
|
386
|
-
return {
|
|
387
|
-
type: "react",
|
|
388
|
-
framework: isReact18 ? "react18" : "react",
|
|
389
|
-
language: "typescript",
|
|
390
|
-
packageManager: detectPackageManager(root),
|
|
391
|
-
root
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
return {
|
|
395
|
-
type: "node",
|
|
396
|
-
language: "javascript",
|
|
397
|
-
packageManager: detectPackageManager(root),
|
|
398
|
-
root
|
|
399
|
-
};
|
|
400
|
-
} catch {
|
|
401
|
-
return {
|
|
402
|
-
type: "node",
|
|
403
|
-
language: "javascript",
|
|
404
|
-
packageManager: "npm",
|
|
405
|
-
root
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
function detectPackageManager(root) {
|
|
410
|
-
if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
411
|
-
if (existsSync(join(root, "yarn.lock"))) return "yarn";
|
|
412
|
-
if (existsSync(join(root, "package-lock.json"))) return "npm";
|
|
413
|
-
return "npm";
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// src/types.ts
|
|
417
|
-
var VERSION = "0.1.0";
|
|
418
|
-
|
|
419
|
-
// src/utils/config.ts
|
|
420
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
421
|
-
import { join as join2 } from "path";
|
|
422
|
-
var DEFAULT_CONFIG = {
|
|
423
|
-
rules: {
|
|
424
|
-
enabled: ["COR-001", "SEC-001", "SEC-002", "PER-001", "MAIN-001"],
|
|
425
|
-
disabled: [],
|
|
426
|
-
severityOverrides: {}
|
|
427
|
-
},
|
|
428
|
-
excludePaths: ["node_modules/", "dist/", "coverage/", "vendor/", ".git/"],
|
|
429
|
-
maxFindingsPerFile: 20,
|
|
430
|
-
outputFormat: "markdown",
|
|
431
|
-
targetBranch: "main"
|
|
432
|
-
};
|
|
433
|
-
function mergeConfig(user) {
|
|
434
|
-
return {
|
|
435
|
-
...DEFAULT_CONFIG,
|
|
436
|
-
...user,
|
|
437
|
-
rules: {
|
|
438
|
-
...DEFAULT_CONFIG.rules,
|
|
439
|
-
...user.rules,
|
|
440
|
-
enabled: user.rules?.enabled ?? DEFAULT_CONFIG.rules.enabled,
|
|
441
|
-
disabled: user.rules?.disabled ?? DEFAULT_CONFIG.rules.disabled,
|
|
442
|
-
severityOverrides: user.rules?.severityOverrides ?? DEFAULT_CONFIG.rules.severityOverrides
|
|
443
|
-
},
|
|
444
|
-
excludePaths: user.excludePaths ?? DEFAULT_CONFIG.excludePaths,
|
|
445
|
-
maxFindingsPerFile: user.maxFindingsPerFile ?? DEFAULT_CONFIG.maxFindingsPerFile,
|
|
446
|
-
outputFormat: user.outputFormat ?? DEFAULT_CONFIG.outputFormat
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
function getDefaultConfig() {
|
|
450
|
-
return structuredClone(DEFAULT_CONFIG);
|
|
451
|
-
}
|
|
452
|
-
function loadConfigFile(root, fileName = ".ai-code-reviewer-plus.yml") {
|
|
453
|
-
const filePath = join2(root, fileName);
|
|
454
|
-
if (!existsSync2(filePath)) {
|
|
455
|
-
return {};
|
|
456
|
-
}
|
|
457
|
-
try {
|
|
458
|
-
const content = readFileSync2(filePath, "utf-8");
|
|
459
|
-
return parseSimpleYAML(content);
|
|
460
|
-
} catch (err) {
|
|
461
|
-
throw new ConfigError({
|
|
462
|
-
filePath,
|
|
463
|
-
message: `Failed to load config file: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
function parseSimpleYAML(content) {
|
|
468
|
-
const config = {
|
|
469
|
-
rules: {
|
|
470
|
-
enabled: [],
|
|
471
|
-
disabled: [],
|
|
472
|
-
severityOverrides: {}
|
|
473
|
-
},
|
|
474
|
-
excludePaths: []
|
|
475
|
-
};
|
|
476
|
-
const lines = content.split("\n");
|
|
477
|
-
let currentSection = "";
|
|
478
|
-
for (const line of lines) {
|
|
479
|
-
const trimmed = line.trim();
|
|
480
|
-
if (trimmed.startsWith("#") || !trimmed) continue;
|
|
481
|
-
if (trimmed.startsWith("rules:")) {
|
|
482
|
-
currentSection = "rules";
|
|
483
|
-
} else if (trimmed.startsWith("excludePaths:")) {
|
|
484
|
-
currentSection = "excludePaths";
|
|
485
|
-
} else if (trimmed.startsWith("maxFindingsPerFile:")) {
|
|
486
|
-
const value = trimmed.split(":")[1]?.trim();
|
|
487
|
-
if (value) config.maxFindingsPerFile = Number(value);
|
|
488
|
-
} else if (trimmed.startsWith("outputFormat:")) {
|
|
489
|
-
const value = trimmed.split(":")[1]?.trim();
|
|
490
|
-
if (value && (value === "markdown" || value === "json")) {
|
|
491
|
-
config.outputFormat = value;
|
|
492
|
-
}
|
|
493
|
-
} else if (trimmed.startsWith("- ") && currentSection) {
|
|
494
|
-
const value = trimmed.slice(2);
|
|
495
|
-
if (currentSection === "rules") {
|
|
496
|
-
const ruleId = value.split(" ")[0];
|
|
497
|
-
if (ruleId) {
|
|
498
|
-
config.rules.enabled.push(ruleId);
|
|
499
|
-
}
|
|
500
|
-
} else if (currentSection === "excludePaths") {
|
|
501
|
-
config.excludePaths.push(value);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return config;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// src/utils/format.ts
|
|
509
|
-
function formatFinding(finding) {
|
|
510
|
-
const severityEmoji = {
|
|
511
|
-
BLOCKER: "\u{1F6AB}",
|
|
512
|
-
HIGH: "\u{1F534}",
|
|
513
|
-
MEDIUM: "\u{1F7E1}",
|
|
514
|
-
LOW: "\u{1F7E2}",
|
|
515
|
-
SUGGESTION: "\u{1F4A1}"
|
|
516
|
-
};
|
|
517
|
-
const emoji = severityEmoji[finding.severity];
|
|
518
|
-
const location = finding.line ? `:${finding.line}` : "";
|
|
519
|
-
let output = `- ${emoji} **${finding.id}** [${finding.severity}] \`${finding.file}${location}\`
|
|
520
|
-
`;
|
|
521
|
-
output += ` **${finding.title}**
|
|
522
|
-
`;
|
|
523
|
-
output += ` ${finding.description}
|
|
524
|
-
`;
|
|
525
|
-
if (finding.codeSnippet) {
|
|
526
|
-
output += ` \`\`\`
|
|
527
|
-
${finding.codeSnippet}
|
|
528
|
-
\`\`\`
|
|
529
|
-
`;
|
|
530
|
-
}
|
|
531
|
-
if (finding.suggestion) {
|
|
532
|
-
output += ` \u{1F4A1} ${finding.suggestion}
|
|
533
|
-
`;
|
|
534
|
-
}
|
|
535
|
-
if (finding.fixExample) {
|
|
536
|
-
output += ` **Fix:**
|
|
537
|
-
\`\`\`
|
|
538
|
-
${finding.fixExample}
|
|
539
|
-
\`\`\`
|
|
540
|
-
`;
|
|
541
|
-
}
|
|
542
|
-
const confidenceEmoji = { high: "\u{1F7E2}", medium: "\u{1F7E1}", low: "\u{1F534}" };
|
|
543
|
-
output += ` Confidence: ${confidenceEmoji[finding.confidence]}
|
|
544
|
-
`;
|
|
545
|
-
return output;
|
|
546
|
-
}
|
|
547
|
-
function formatSummary(result) {
|
|
548
|
-
const { summary, duration } = result;
|
|
549
|
-
let output = `## \u{1F4CA} Review Summary
|
|
550
|
-
|
|
551
|
-
`;
|
|
552
|
-
output += `- **Total findings**: ${summary.total}
|
|
553
|
-
`;
|
|
554
|
-
output += `- **\u{1F6AB} Blockers**: ${summary.blockers}
|
|
555
|
-
`;
|
|
556
|
-
output += `- **\u{1F534} High**: ${summary.high}
|
|
557
|
-
`;
|
|
558
|
-
output += `- **\u{1F7E1} Medium**: ${summary.medium}
|
|
559
|
-
`;
|
|
560
|
-
output += `- **\u{1F7E2} Low**: ${summary.low}
|
|
561
|
-
`;
|
|
562
|
-
output += `- **\u{1F4A1} Suggestions**: ${summary.suggestions}
|
|
563
|
-
`;
|
|
564
|
-
output += `- **Files reviewed**: ${result.filesReviewed.length}
|
|
565
|
-
`;
|
|
566
|
-
output += `- **Duration**: ${formatDuration(duration)}
|
|
567
|
-
`;
|
|
568
|
-
return output;
|
|
569
|
-
}
|
|
570
|
-
function formatReviewReport(result) {
|
|
571
|
-
let output = `# \u{1F50D} Code Review Report
|
|
572
|
-
|
|
573
|
-
`;
|
|
574
|
-
output += formatSummary(result);
|
|
575
|
-
output += `
|
|
576
|
-
---
|
|
577
|
-
|
|
578
|
-
`;
|
|
579
|
-
const blockers = result.findings.filter((f) => f.severity === "BLOCKER");
|
|
580
|
-
const high = result.findings.filter((f) => f.severity === "HIGH");
|
|
581
|
-
const medium = result.findings.filter((f) => f.severity === "MEDIUM");
|
|
582
|
-
const low = result.findings.filter((f) => f.severity === "LOW");
|
|
583
|
-
const suggestions = result.findings.filter((f) => f.severity === "SUGGESTION");
|
|
584
|
-
if (blockers.length > 0) {
|
|
585
|
-
output += `## \u{1F6AB} Blockers (Must Fix)
|
|
586
|
-
|
|
587
|
-
`;
|
|
588
|
-
for (const f of blockers) {
|
|
589
|
-
output += `${formatFinding(f)}
|
|
590
|
-
`;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
if (high.length > 0) {
|
|
594
|
-
output += `## \u{1F534} High Priority
|
|
595
|
-
|
|
596
|
-
`;
|
|
597
|
-
for (const f of high) {
|
|
598
|
-
output += `${formatFinding(f)}
|
|
599
|
-
`;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
if (medium.length > 0) {
|
|
603
|
-
output += `## \u{1F7E1} Medium Priority
|
|
604
|
-
|
|
605
|
-
`;
|
|
606
|
-
for (const f of medium) {
|
|
607
|
-
output += `${formatFinding(f)}
|
|
608
|
-
`;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
if (low.length > 0) {
|
|
612
|
-
output += `## \u{1F7E2} Low Priority
|
|
613
|
-
|
|
614
|
-
`;
|
|
615
|
-
for (const f of low) {
|
|
616
|
-
output += `${formatFinding(f)}
|
|
617
|
-
`;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
if (suggestions.length > 0) {
|
|
621
|
-
output += `## \u{1F4A1} Suggestions
|
|
622
|
-
|
|
623
|
-
`;
|
|
624
|
-
for (const f of suggestions) {
|
|
625
|
-
output += `${formatFinding(f)}
|
|
626
|
-
`;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
return output;
|
|
630
|
-
}
|
|
631
|
-
function formatDiffSummary(diff) {
|
|
632
|
-
const statusEmoji = {
|
|
633
|
-
added: "\u2795",
|
|
634
|
-
deleted: "\u274C",
|
|
635
|
-
modified: "\u{1F4DD}",
|
|
636
|
-
renamed: "\u{1F4E6}"
|
|
637
|
-
};
|
|
638
|
-
const emoji = statusEmoji[diff.status];
|
|
639
|
-
const renameInfo = diff.status === "renamed" && diff.from ? ` (${diff.from} \u2192 ${diff.file})` : "";
|
|
640
|
-
return `- ${emoji} \`${diff.file}\` +${diff.additions}/-${diff.deletions}${renameInfo}`;
|
|
641
|
-
}
|
|
642
|
-
function formatDuration(ms) {
|
|
643
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
644
|
-
const seconds = Math.round(ms / 1e3);
|
|
645
|
-
if (seconds < 60) return `${seconds}s`;
|
|
646
|
-
const minutes = Math.floor(seconds / 60);
|
|
647
|
-
const secs = seconds % 60;
|
|
648
|
-
return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
export {
|
|
652
|
-
analyzeDiffs,
|
|
653
|
-
analyzeCode,
|
|
654
|
-
getAvailableRules,
|
|
655
|
-
GitCommandError,
|
|
656
|
-
ParseError,
|
|
657
|
-
ConfigError,
|
|
658
|
-
collectDiff,
|
|
659
|
-
collectCommitDiff,
|
|
660
|
-
detectProject,
|
|
661
|
-
VERSION,
|
|
662
|
-
mergeConfig,
|
|
663
|
-
getDefaultConfig,
|
|
664
|
-
loadConfigFile,
|
|
665
|
-
formatFinding,
|
|
666
|
-
formatSummary,
|
|
667
|
-
formatReviewReport,
|
|
668
|
-
formatDiffSummary,
|
|
669
|
-
formatDuration
|
|
670
|
-
};
|
package/dist/cli.d.ts
DELETED