@gulu9527/code-trust 0.1.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.
@@ -0,0 +1,2631 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command as Command6 } from "commander";
5
+
6
+ // src/cli/commands/scan.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/core/config.ts
10
+ import { cosmiconfig } from "cosmiconfig";
11
+
12
+ // src/types/config.ts
13
+ var DEFAULT_CONFIG = {
14
+ version: 1,
15
+ include: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
16
+ exclude: [
17
+ "**/*.test.ts",
18
+ "**/*.spec.ts",
19
+ "**/node_modules/**",
20
+ "**/dist/**",
21
+ "**/build/**"
22
+ ],
23
+ weights: {
24
+ security: 0.3,
25
+ logic: 0.25,
26
+ structure: 0.2,
27
+ style: 0.1,
28
+ coverage: 0.15
29
+ },
30
+ thresholds: {
31
+ "min-score": 70,
32
+ "max-function-length": 40,
33
+ "max-cyclomatic-complexity": 10,
34
+ "max-cognitive-complexity": 20,
35
+ "max-nesting-depth": 4,
36
+ "max-params": 5
37
+ },
38
+ rules: {
39
+ disabled: [],
40
+ overrides: {}
41
+ },
42
+ detection: {
43
+ enabled: true,
44
+ "show-probability": true
45
+ }
46
+ };
47
+
48
+ // src/core/config.ts
49
+ var MODULE_NAME = "codetrust";
50
+ async function loadConfig(searchFrom) {
51
+ const explorer = cosmiconfig(MODULE_NAME, {
52
+ searchPlaces: [
53
+ `.${MODULE_NAME}.yml`,
54
+ `.${MODULE_NAME}.yaml`,
55
+ `.${MODULE_NAME}.json`,
56
+ `.${MODULE_NAME}rc`,
57
+ `${MODULE_NAME}.config.js`,
58
+ `${MODULE_NAME}.config.ts`
59
+ ]
60
+ });
61
+ const result = await explorer.search(searchFrom);
62
+ if (!result || result.isEmpty) {
63
+ return DEFAULT_CONFIG;
64
+ }
65
+ return mergeConfig(DEFAULT_CONFIG, result.config);
66
+ }
67
+ function mergeConfig(defaults, overrides) {
68
+ return {
69
+ ...defaults,
70
+ ...overrides,
71
+ weights: { ...defaults.weights, ...overrides.weights },
72
+ thresholds: { ...defaults.thresholds, ...overrides.thresholds },
73
+ rules: {
74
+ disabled: overrides.rules?.disabled ?? defaults.rules.disabled,
75
+ overrides: { ...defaults.rules.overrides, ...overrides.rules?.overrides }
76
+ },
77
+ detection: { ...defaults.detection, ...overrides.detection }
78
+ };
79
+ }
80
+ function generateDefaultConfig() {
81
+ return `# .codetrust.yml
82
+ version: 1
83
+
84
+ # Scan scope
85
+ include:
86
+ - "src/**/*.ts"
87
+ - "src/**/*.js"
88
+ exclude:
89
+ - "**/*.test.ts"
90
+ - "**/*.spec.ts"
91
+ - "**/node_modules/**"
92
+ - "**/dist/**"
93
+
94
+ # Dimension weights (must sum to 1.0)
95
+ weights:
96
+ security: 0.30
97
+ logic: 0.25
98
+ structure: 0.20
99
+ style: 0.10
100
+ coverage: 0.15
101
+
102
+ # Thresholds
103
+ thresholds:
104
+ min-score: 70
105
+ max-function-length: 40
106
+ max-cyclomatic-complexity: 10
107
+ max-nesting-depth: 4
108
+ max-params: 5
109
+
110
+ # Rules
111
+ rules:
112
+ disabled: []
113
+ overrides: {}
114
+
115
+ # AI code detection
116
+ detection:
117
+ enabled: true
118
+ show-probability: true
119
+ `;
120
+ }
121
+
122
+ // src/core/engine.ts
123
+ import { readFile } from "fs/promises";
124
+ import { readFileSync, existsSync as existsSync3 } from "fs";
125
+ import { resolve as resolve2, dirname as dirname3 } from "path";
126
+ import { fileURLToPath } from "url";
127
+
128
+ // src/parsers/diff.ts
129
+ import simpleGit from "simple-git";
130
+ var DiffParser = class {
131
+ git;
132
+ constructor(workDir) {
133
+ this.git = simpleGit(workDir);
134
+ }
135
+ async getStagedFiles() {
136
+ const diffDetail = await this.git.diff(["--cached", "--unified=3"]);
137
+ return this.parseDiffOutput(diffDetail);
138
+ }
139
+ async getDiffFromRef(ref) {
140
+ const diffDetail = await this.git.diff([ref, "--unified=3"]);
141
+ return this.parseDiffOutput(diffDetail);
142
+ }
143
+ async getChangedFiles() {
144
+ const diffDetail = await this.git.diff(["--unified=3"]);
145
+ const stagedDetail = await this.git.diff(["--cached", "--unified=3"]);
146
+ const allDiff = diffDetail + "\n" + stagedDetail;
147
+ return this.parseDiffOutput(allDiff);
148
+ }
149
+ async getLastCommitDiff() {
150
+ const diffDetail = await this.git.diff(["HEAD~1", "HEAD", "--unified=3"]);
151
+ return this.parseDiffOutput(diffDetail);
152
+ }
153
+ async getCurrentCommitHash() {
154
+ try {
155
+ const hash = await this.git.revparse(["HEAD"]);
156
+ return hash.trim().slice(0, 7);
157
+ } catch {
158
+ return void 0;
159
+ }
160
+ }
161
+ async getFileContent(filePath) {
162
+ try {
163
+ const content = await this.git.show([`HEAD:${filePath}`]);
164
+ return content;
165
+ } catch {
166
+ return void 0;
167
+ }
168
+ }
169
+ parseDiffOutput(diffOutput) {
170
+ const files = [];
171
+ const fileDiffs = diffOutput.split(/^diff --git /m).filter(Boolean);
172
+ for (const fileDiff of fileDiffs) {
173
+ const file = this.parseFileDiff(fileDiff);
174
+ if (file) {
175
+ files.push(file);
176
+ }
177
+ }
178
+ return files;
179
+ }
180
+ parseFileDiff(fileDiff) {
181
+ const lines = fileDiff.split("\n");
182
+ const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
183
+ if (!headerMatch) return null;
184
+ const filePath = headerMatch[2];
185
+ let status = "modified";
186
+ let additions = 0;
187
+ let deletions = 0;
188
+ if (fileDiff.includes("new file mode")) {
189
+ status = "added";
190
+ } else if (fileDiff.includes("deleted file mode")) {
191
+ status = "deleted";
192
+ } else if (fileDiff.includes("rename from")) {
193
+ status = "renamed";
194
+ }
195
+ const hunks = this.parseHunks(fileDiff);
196
+ for (const line of fileDiff.split("\n")) {
197
+ if (line.startsWith("+") && !line.startsWith("+++")) {
198
+ additions++;
199
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
200
+ deletions++;
201
+ }
202
+ }
203
+ return {
204
+ filePath,
205
+ status,
206
+ additions,
207
+ deletions,
208
+ hunks
209
+ };
210
+ }
211
+ parseHunks(fileDiff) {
212
+ const hunks = [];
213
+ const hunkRegex = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@(.*)$/gm;
214
+ let match;
215
+ const lines = fileDiff.split("\n");
216
+ while ((match = hunkRegex.exec(fileDiff)) !== null) {
217
+ const oldStart = parseInt(match[1], 10);
218
+ const oldLines = parseInt(match[2] || "1", 10);
219
+ const newStart = parseInt(match[3], 10);
220
+ const newLines = parseInt(match[4] || "1", 10);
221
+ const hunkStartIndex = lines.findIndex((l) => l.includes(match[0]));
222
+ const hunkContent = [];
223
+ if (hunkStartIndex >= 0) {
224
+ for (let i = hunkStartIndex + 1; i < lines.length; i++) {
225
+ if (lines[i].startsWith("@@ ") || lines[i].startsWith("diff --git ")) {
226
+ break;
227
+ }
228
+ hunkContent.push(lines[i]);
229
+ }
230
+ }
231
+ hunks.push({
232
+ oldStart,
233
+ oldLines,
234
+ newStart,
235
+ newLines,
236
+ content: hunkContent.join("\n")
237
+ });
238
+ }
239
+ return hunks;
240
+ }
241
+ };
242
+
243
+ // src/i18n/index.ts
244
+ import { execSync } from "child_process";
245
+ var _cachedLocale = null;
246
+ function isZhLocale() {
247
+ if (_cachedLocale !== null) return _cachedLocale;
248
+ if (process.env.CODETRUST_LANG?.startsWith("zh")) {
249
+ _cachedLocale = true;
250
+ return true;
251
+ }
252
+ if (process.env.CODETRUST_LANG && !process.env.CODETRUST_LANG.startsWith("zh")) {
253
+ _cachedLocale = false;
254
+ return false;
255
+ }
256
+ const envVars = [
257
+ process.env.LANG,
258
+ process.env.LC_ALL,
259
+ process.env.LC_MESSAGES,
260
+ process.env.LANGUAGE
261
+ ];
262
+ for (const v of envVars) {
263
+ if (v?.startsWith("zh")) {
264
+ _cachedLocale = true;
265
+ return true;
266
+ }
267
+ }
268
+ if (process.platform === "darwin") {
269
+ try {
270
+ const appleLocale = execSync("defaults read -g AppleLocale 2>/dev/null", {
271
+ encoding: "utf-8",
272
+ timeout: 1e3
273
+ }).trim();
274
+ if (appleLocale.startsWith("zh")) {
275
+ _cachedLocale = true;
276
+ return true;
277
+ }
278
+ } catch {
279
+ }
280
+ }
281
+ _cachedLocale = false;
282
+ return false;
283
+ }
284
+ function t(en2, zh2) {
285
+ return isZhLocale() ? zh2 : en2;
286
+ }
287
+
288
+ // src/rules/builtin/unnecessary-try-catch.ts
289
+ var unnecessaryTryCatchRule = {
290
+ id: "logic/unnecessary-try-catch",
291
+ category: "logic",
292
+ severity: "medium",
293
+ title: "Unnecessary try-catch wrapping simple statement",
294
+ description: "AI often wraps simple, non-throwing statements in try-catch blocks with generic console.log/error in the catch. This is hallucinated error handling.",
295
+ check(context) {
296
+ const issues = [];
297
+ const lines = context.fileContent.split("\n");
298
+ let i = 0;
299
+ while (i < lines.length) {
300
+ const line = lines[i];
301
+ const trimmed = line.trim();
302
+ if (trimmed.startsWith("try") && trimmed.includes("{")) {
303
+ const tryBlock = extractBlock(lines, i);
304
+ if (tryBlock) {
305
+ const { bodyLines, catchBodyLines, endLine } = tryBlock;
306
+ const nonEmptyBody = bodyLines.filter((l) => l.trim().length > 0);
307
+ const nonEmptyCatch = catchBodyLines.filter((l) => l.trim().length > 0);
308
+ const isSimpleBody = nonEmptyBody.length <= 2;
309
+ const isGenericCatch = nonEmptyCatch.length <= 2 && nonEmptyCatch.some(
310
+ (l) => /console\.(log|error|warn)/.test(l) || /throw\s+(new\s+)?Error/.test(l) || l.trim() === ""
311
+ );
312
+ const bodyHasOnlyAssignments = nonEmptyBody.every(
313
+ (l) => /^\s*(const|let|var)\s+/.test(l) || /^\s*\w+(\.\w+)*\s*=\s*/.test(l) || /^\s*return\s+/.test(l)
314
+ );
315
+ if (isSimpleBody && isGenericCatch && bodyHasOnlyAssignments) {
316
+ issues.push({
317
+ ruleId: "logic/unnecessary-try-catch",
318
+ severity: "medium",
319
+ category: "logic",
320
+ file: context.filePath,
321
+ startLine: i + 1,
322
+ endLine: endLine + 1,
323
+ message: t(
324
+ "Unnecessary try-catch wrapping a simple statement with generic error handling. This is likely AI-hallucinated error handling.",
325
+ "\u4E0D\u5FC5\u8981\u7684 try-catch \u5305\u88F9\u4E86\u7B80\u5355\u8BED\u53E5\uFF0Ccatch \u4E2D\u53EA\u6709\u901A\u7528\u7684\u9519\u8BEF\u65E5\u5FD7\u3002\u8FD9\u5F88\u53EF\u80FD\u662F AI \u5E7B\u89C9\u751F\u6210\u7684\u9519\u8BEF\u5904\u7406\u3002"
326
+ ),
327
+ suggestion: t(
328
+ "Remove the try-catch block or add meaningful error recovery logic.",
329
+ "\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u6DFB\u52A0\u6709\u610F\u4E49\u7684\u9519\u8BEF\u6062\u590D\u903B\u8F91\u3002"
330
+ )
331
+ });
332
+ }
333
+ i = endLine + 1;
334
+ continue;
335
+ }
336
+ }
337
+ i++;
338
+ }
339
+ return issues;
340
+ }
341
+ };
342
+ function extractBlock(lines, tryLineIndex) {
343
+ let braceCount = 0;
344
+ let foundTryOpen = false;
345
+ let tryBodyStart = -1;
346
+ let tryBodyEnd = -1;
347
+ let catchStart = -1;
348
+ let catchBodyStart = -1;
349
+ let catchBodyEnd = -1;
350
+ for (let i = tryLineIndex; i < lines.length; i++) {
351
+ const line = lines[i];
352
+ for (const ch of line) {
353
+ if (ch === "{") {
354
+ braceCount++;
355
+ if (!foundTryOpen) {
356
+ foundTryOpen = true;
357
+ tryBodyStart = i;
358
+ }
359
+ } else if (ch === "}") {
360
+ braceCount--;
361
+ if (braceCount === 0 && tryBodyEnd === -1) {
362
+ tryBodyEnd = i;
363
+ } else if (braceCount === 0 && catchBodyEnd === -1 && catchBodyStart !== -1) {
364
+ catchBodyEnd = i;
365
+ break;
366
+ }
367
+ }
368
+ }
369
+ if (tryBodyEnd !== -1 && catchStart === -1) {
370
+ if (line.includes("catch")) {
371
+ catchStart = i;
372
+ }
373
+ }
374
+ if (catchStart !== -1 && catchBodyStart === -1 && line.includes("{")) {
375
+ catchBodyStart = i;
376
+ }
377
+ if (catchBodyEnd !== -1) break;
378
+ }
379
+ if (tryBodyStart === -1 || tryBodyEnd === -1 || catchBodyStart === -1 || catchBodyEnd === -1) {
380
+ return null;
381
+ }
382
+ const bodyLines = lines.slice(tryBodyStart + 1, tryBodyEnd);
383
+ const catchBodyLines = lines.slice(catchBodyStart + 1, catchBodyEnd);
384
+ return {
385
+ bodyLines,
386
+ catchStart,
387
+ catchBodyLines,
388
+ endLine: catchBodyEnd
389
+ };
390
+ }
391
+
392
+ // src/rules/builtin/over-defensive.ts
393
+ var overDefensiveRule = {
394
+ id: "logic/over-defensive",
395
+ category: "logic",
396
+ severity: "low",
397
+ title: "Over-defensive coding pattern",
398
+ description: "AI-generated code often includes excessive null/undefined checks, redundant type guards, and unnecessary validations that the type system already handles.",
399
+ check(context) {
400
+ const issues = [];
401
+ const lines = context.fileContent.split("\n");
402
+ let consecutiveChecks = 0;
403
+ let checkStartLine = -1;
404
+ for (let i = 0; i < lines.length; i++) {
405
+ const trimmed = lines[i].trim();
406
+ if (isDefensiveCheck(trimmed)) {
407
+ if (consecutiveChecks === 0) {
408
+ checkStartLine = i;
409
+ }
410
+ consecutiveChecks++;
411
+ } else if (trimmed.length > 0) {
412
+ if (consecutiveChecks >= 3) {
413
+ issues.push({
414
+ ruleId: "logic/over-defensive",
415
+ severity: "low",
416
+ category: "logic",
417
+ file: context.filePath,
418
+ startLine: checkStartLine + 1,
419
+ endLine: i,
420
+ message: t(
421
+ `${consecutiveChecks} consecutive defensive checks detected. AI tends to add excessive null/undefined guards.`,
422
+ `\u68C0\u6D4B\u5230 ${consecutiveChecks} \u4E2A\u8FDE\u7EED\u7684\u9632\u5FA1\u6027\u68C0\u67E5\u3002AI \u503E\u5411\u4E8E\u6DFB\u52A0\u8FC7\u591A\u7684 null/undefined \u5B88\u536B\u3002`
423
+ ),
424
+ suggestion: t(
425
+ "Consider if these checks are necessary \u2014 TypeScript types may already prevent these cases. Remove redundant guards.",
426
+ "\u8003\u8651\u8FD9\u4E9B\u68C0\u67E5\u662F\u5426\u5FC5\u8981 \u2014 TypeScript \u7C7B\u578B\u53EF\u80FD\u5DF2\u7ECF\u9632\u6B62\u4E86\u8FD9\u4E9B\u60C5\u51B5\u3002\u79FB\u9664\u5197\u4F59\u7684\u5B88\u536B\u3002"
427
+ )
428
+ });
429
+ }
430
+ consecutiveChecks = 0;
431
+ }
432
+ }
433
+ if (consecutiveChecks >= 3) {
434
+ issues.push({
435
+ ruleId: "logic/over-defensive",
436
+ severity: "low",
437
+ category: "logic",
438
+ file: context.filePath,
439
+ startLine: checkStartLine + 1,
440
+ endLine: lines.length,
441
+ message: t(
442
+ `${consecutiveChecks} consecutive defensive checks detected at end of block.`,
443
+ `\u5728\u4EE3\u7801\u5757\u672B\u5C3E\u68C0\u6D4B\u5230 ${consecutiveChecks} \u4E2A\u8FDE\u7EED\u7684\u9632\u5FA1\u6027\u68C0\u67E5\u3002`
444
+ ),
445
+ suggestion: t(
446
+ "Review if these defensive checks are truly necessary.",
447
+ "\u68C0\u67E5\u8FD9\u4E9B\u9632\u5FA1\u6027\u68C0\u67E5\u662F\u5426\u771F\u6B63\u5FC5\u8981\u3002"
448
+ )
449
+ });
450
+ }
451
+ detectRedundantTypeofChecks(context, lines, issues);
452
+ return issues;
453
+ }
454
+ };
455
+ function isDefensiveCheck(line) {
456
+ const patterns = [
457
+ /^if\s*\(\s*!?\w+(\.\w+)*\s*(===?|!==?)\s*(null|undefined|''|"")\s*\)/,
458
+ /^if\s*\(\s*!?\w+(\.\w+)*\s*\)\s*\{?\s*(return|throw)/,
459
+ /^if\s*\(\s*typeof\s+\w+\s*(===?|!==?)\s*['"]undefined['"]\s*\)/,
460
+ /^if\s*\(\s*\w+(\.\w+)*\s*==\s*null\s*\)/,
461
+ /^if\s*\(\s*!\w+(\.\w+)*\s*\)\s*\{?\s*$/,
462
+ /^\w+(\.\w+)*\s*\?\?=/,
463
+ /^if\s*\(\s*Array\.isArray\(\w+\)\s*&&\s*\w+\.length\s*(>|>=|===?)\s*0\s*\)/
464
+ ];
465
+ return patterns.some((p) => p.test(line));
466
+ }
467
+ function detectRedundantTypeofChecks(context, lines, issues) {
468
+ for (let i = 0; i < lines.length; i++) {
469
+ const trimmed = lines[i].trim();
470
+ const typeofMatch = trimmed.match(
471
+ /if\s*\(\s*typeof\s+(\w+)\s*(!==?|===?)\s*['"]undefined['"]\s*\)/
472
+ );
473
+ if (typeofMatch) {
474
+ const varName = typeofMatch[1];
475
+ const prevLines = lines.slice(Math.max(0, i - 5), i);
476
+ const hasDeclaration = prevLines.some(
477
+ (l) => new RegExp(`(const|let|var)\\s+${varName}\\s*[:=]`).test(l)
478
+ );
479
+ if (hasDeclaration) {
480
+ issues.push({
481
+ ruleId: "logic/over-defensive",
482
+ severity: "low",
483
+ category: "logic",
484
+ file: context.filePath,
485
+ startLine: i + 1,
486
+ endLine: i + 1,
487
+ message: t(
488
+ `Redundant typeof check for "${varName}" \u2014 variable is declared within ${prevLines.length} line(s) above and cannot be undefined.`,
489
+ `\u5BF9 "${varName}" \u7684 typeof \u68C0\u67E5\u662F\u5197\u4F59\u7684 \u2014 \u53D8\u91CF\u5DF2\u5728\u4E0A\u65B9 ${prevLines.length} \u884C\u5185\u58F0\u660E\uFF0C\u4E0D\u53EF\u80FD\u4E3A undefined\u3002`
490
+ ),
491
+ suggestion: t(
492
+ `Remove the typeof check for "${varName}".`,
493
+ `\u79FB\u9664\u5BF9 "${varName}" \u7684 typeof \u68C0\u67E5\u3002`
494
+ )
495
+ });
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ // src/rules/builtin/dead-logic.ts
502
+ var deadLogicRule = {
503
+ id: "logic/dead-branch",
504
+ category: "logic",
505
+ severity: "medium",
506
+ title: "Dead logic branch detected",
507
+ description: "AI-generated code sometimes contains conditions that are always true or false, unreachable code after return/throw, or assign-then-immediately-reassign patterns.",
508
+ check(context) {
509
+ const issues = [];
510
+ const lines = context.fileContent.split("\n");
511
+ detectAlwaysTrueFalse(context, lines, issues);
512
+ detectCodeAfterReturn(context, lines, issues);
513
+ detectImmediateReassign(context, lines, issues);
514
+ return issues;
515
+ }
516
+ };
517
+ function detectAlwaysTrueFalse(context, lines, issues) {
518
+ for (let i = 0; i < lines.length; i++) {
519
+ const trimmed = lines[i].trim();
520
+ const alwaysTruePatterns = [
521
+ /if\s*\(\s*true\s*\)/,
522
+ /if\s*\(\s*1\s*\)/,
523
+ /if\s*\(\s*['"].+['"]\s*\)/
524
+ ];
525
+ const alwaysFalsePatterns = [
526
+ /if\s*\(\s*false\s*\)/,
527
+ /if\s*\(\s*0\s*\)/,
528
+ /if\s*\(\s*null\s*\)/,
529
+ /if\s*\(\s*undefined\s*\)/,
530
+ /if\s*\(\s*''\s*\)/,
531
+ /if\s*\(\s*""\s*\)/
532
+ ];
533
+ for (const pattern of alwaysTruePatterns) {
534
+ if (pattern.test(trimmed)) {
535
+ issues.push({
536
+ ruleId: "logic/dead-branch",
537
+ severity: "medium",
538
+ category: "logic",
539
+ file: context.filePath,
540
+ startLine: i + 1,
541
+ endLine: i + 1,
542
+ message: t(
543
+ "Condition is always true \u2014 this branch always executes.",
544
+ "\u6761\u4EF6\u59CB\u7EC8\u4E3A true \u2014 \u6B64\u5206\u652F\u59CB\u7EC8\u4F1A\u6267\u884C\u3002"
545
+ ),
546
+ suggestion: t(
547
+ "Remove the condition and keep only the body, or fix the logic.",
548
+ "\u79FB\u9664\u6761\u4EF6\u5224\u65AD\u53EA\u4FDD\u7559\u4E3B\u4F53\uFF0C\u6216\u4FEE\u590D\u903B\u8F91\u3002"
549
+ )
550
+ });
551
+ }
552
+ }
553
+ for (const pattern of alwaysFalsePatterns) {
554
+ if (pattern.test(trimmed)) {
555
+ issues.push({
556
+ ruleId: "logic/dead-branch",
557
+ severity: "medium",
558
+ category: "logic",
559
+ file: context.filePath,
560
+ startLine: i + 1,
561
+ endLine: i + 1,
562
+ message: t(
563
+ "Condition is always false \u2014 this branch never executes.",
564
+ "\u6761\u4EF6\u59CB\u7EC8\u4E3A false \u2014 \u6B64\u5206\u652F\u6C38\u8FDC\u4E0D\u4F1A\u6267\u884C\u3002"
565
+ ),
566
+ suggestion: t(
567
+ "Remove the dead branch entirely.",
568
+ "\u5B8C\u5168\u79FB\u9664\u8BE5\u6B7B\u4EE3\u7801\u5206\u652F\u3002"
569
+ )
570
+ });
571
+ }
572
+ }
573
+ }
574
+ }
575
+ function detectCodeAfterReturn(context, lines, issues) {
576
+ let braceDepth = 0;
577
+ let lastReturnDepth = -1;
578
+ let lastReturnLine = -1;
579
+ for (let i = 0; i < lines.length; i++) {
580
+ const trimmed = lines[i].trim();
581
+ for (const ch of trimmed) {
582
+ if (ch === "{") braceDepth++;
583
+ if (ch === "}") {
584
+ if (braceDepth === lastReturnDepth) {
585
+ lastReturnDepth = -1;
586
+ lastReturnLine = -1;
587
+ }
588
+ braceDepth--;
589
+ }
590
+ }
591
+ if (/^(return|throw)\b/.test(trimmed) && !trimmed.includes("=>")) {
592
+ const endsOpen = /[{(\[,]$/.test(trimmed) || /^(return|throw)\s*$/.test(trimmed);
593
+ if (endsOpen) continue;
594
+ lastReturnDepth = braceDepth;
595
+ lastReturnLine = i;
596
+ } else if (lastReturnLine !== -1 && braceDepth === lastReturnDepth && trimmed.length > 0 && trimmed !== "}" && trimmed !== "};" && !trimmed.startsWith("//") && !trimmed.startsWith("case ") && !trimmed.startsWith("default:") && !trimmed.startsWith("default :")) {
597
+ issues.push({
598
+ ruleId: "logic/dead-branch",
599
+ severity: "medium",
600
+ category: "logic",
601
+ file: context.filePath,
602
+ startLine: i + 1,
603
+ endLine: i + 1,
604
+ message: t(
605
+ `Unreachable code after return/throw at line ${lastReturnLine + 1}.`,
606
+ `\u7B2C ${lastReturnLine + 1} \u884C\u7684 return/throw \u4E4B\u540E\u5B58\u5728\u4E0D\u53EF\u8FBE\u4EE3\u7801\u3002`
607
+ ),
608
+ suggestion: t(
609
+ "Remove unreachable code or restructure the logic.",
610
+ "\u79FB\u9664\u4E0D\u53EF\u8FBE\u4EE3\u7801\u6216\u91CD\u6784\u903B\u8F91\u3002"
611
+ )
612
+ });
613
+ lastReturnDepth = -1;
614
+ lastReturnLine = -1;
615
+ }
616
+ }
617
+ }
618
+ function detectImmediateReassign(context, lines, issues) {
619
+ for (let i = 0; i < lines.length - 1; i++) {
620
+ const current = lines[i].trim();
621
+ const next = lines[i + 1].trim();
622
+ const assignMatch = current.match(/^(let|var)\s+(\w+)\s*=\s*.+;?\s*$/);
623
+ if (assignMatch) {
624
+ const varName = assignMatch[2];
625
+ const reassignPattern = new RegExp(`^${varName}\\s*=\\s*.+;?\\s*$`);
626
+ if (reassignPattern.test(next)) {
627
+ issues.push({
628
+ ruleId: "logic/dead-branch",
629
+ severity: "low",
630
+ category: "logic",
631
+ file: context.filePath,
632
+ startLine: i + 1,
633
+ endLine: i + 2,
634
+ message: t(
635
+ `Variable "${varName}" is assigned and immediately reassigned on the next line.`,
636
+ `\u53D8\u91CF "${varName}" \u8D4B\u503C\u540E\u7ACB\u5373\u5728\u4E0B\u4E00\u884C\u88AB\u91CD\u65B0\u8D4B\u503C\u3002`
637
+ ),
638
+ suggestion: t(
639
+ `Remove the first assignment or combine into a single declaration.`,
640
+ `\u79FB\u9664\u7B2C\u4E00\u6B21\u8D4B\u503C\uFF0C\u6216\u5408\u5E76\u4E3A\u4E00\u6B21\u58F0\u660E\u3002`
641
+ )
642
+ });
643
+ }
644
+ }
645
+ }
646
+ }
647
+
648
+ // src/parsers/ast.ts
649
+ import { parse, AST_NODE_TYPES } from "@typescript-eslint/typescript-estree";
650
+
651
+ // src/parsers/walk.ts
652
+ function walkAST(node, visitor, parent = null) {
653
+ const result = visitor(node, parent);
654
+ if (result === false) return;
655
+ for (const key of Object.keys(node)) {
656
+ if (key === "parent") continue;
657
+ const child = node[key];
658
+ if (child && typeof child === "object") {
659
+ if (Array.isArray(child)) {
660
+ for (const item of child) {
661
+ if (isASTNode(item)) {
662
+ walkAST(item, visitor, node);
663
+ }
664
+ }
665
+ } else if (isASTNode(child)) {
666
+ walkAST(child, visitor, node);
667
+ }
668
+ }
669
+ }
670
+ }
671
+ function isASTNode(value) {
672
+ return value !== null && typeof value === "object" && "type" in value;
673
+ }
674
+
675
+ // src/parsers/ast.ts
676
+ var _astCache = /* @__PURE__ */ new Map();
677
+ function parseCode(code, filePath) {
678
+ const cacheKey = `${filePath}:${code.length}:${simpleHash(code)}`;
679
+ const cached = _astCache.get(cacheKey);
680
+ if (cached) return cached;
681
+ const ast = parse(code, {
682
+ loc: true,
683
+ range: true,
684
+ comment: true,
685
+ jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
686
+ filePath
687
+ });
688
+ const result = { ast, filePath };
689
+ _astCache.set(cacheKey, result);
690
+ if (_astCache.size > 50) {
691
+ const firstKey = _astCache.keys().next().value;
692
+ if (firstKey) _astCache.delete(firstKey);
693
+ }
694
+ return result;
695
+ }
696
+ function simpleHash(str) {
697
+ let hash = 0;
698
+ for (let i = 0; i < str.length; i++) {
699
+ hash = (hash << 5) - hash + str.charCodeAt(i) | 0;
700
+ }
701
+ return hash;
702
+ }
703
+ function extractFunctions(parsed) {
704
+ const functions = [];
705
+ visitNode(parsed.ast, functions);
706
+ return functions;
707
+ }
708
+ function visitNode(root, functions) {
709
+ const methodBodies = /* @__PURE__ */ new WeakSet();
710
+ walkAST(root, (node) => {
711
+ if (node.type === AST_NODE_TYPES.FunctionExpression && methodBodies.has(node)) {
712
+ return false;
713
+ }
714
+ if (node.type === AST_NODE_TYPES.FunctionDeclaration || node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.MethodDefinition) {
715
+ const info = analyzeFunctionNode(node);
716
+ if (info) functions.push(info);
717
+ if (node.type === AST_NODE_TYPES.MethodDefinition) {
718
+ methodBodies.add(node.value);
719
+ }
720
+ }
721
+ return;
722
+ });
723
+ }
724
+ function analyzeFunctionNode(node) {
725
+ let name = "<anonymous>";
726
+ let params = [];
727
+ let body = null;
728
+ if (node.type === AST_NODE_TYPES.FunctionDeclaration) {
729
+ name = node.id?.name ?? "<anonymous>";
730
+ params = node.params;
731
+ body = node.body;
732
+ } else if (node.type === AST_NODE_TYPES.FunctionExpression) {
733
+ name = node.id?.name ?? "<anonymous>";
734
+ params = node.params;
735
+ body = node.body;
736
+ } else if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
737
+ name = "<arrow>";
738
+ params = node.params;
739
+ body = node.body;
740
+ } else if (node.type === AST_NODE_TYPES.MethodDefinition) {
741
+ if (node.key.type === AST_NODE_TYPES.Identifier) {
742
+ name = node.key.name;
743
+ }
744
+ const value = node.value;
745
+ params = value.params;
746
+ body = value.body;
747
+ }
748
+ if (!body || !node.loc) return null;
749
+ const startLine = node.loc.start.line;
750
+ const endLine = node.loc.end.line;
751
+ return {
752
+ name,
753
+ startLine,
754
+ endLine,
755
+ lineCount: endLine - startLine + 1,
756
+ paramCount: params.length,
757
+ cyclomaticComplexity: body ? calculateCyclomaticComplexity(body) : 1,
758
+ cognitiveComplexity: body ? calculateCognitiveComplexity(body) : 0,
759
+ maxNestingDepth: body ? calculateMaxNestingDepth(body) : 0
760
+ };
761
+ }
762
+ function calculateCyclomaticComplexity(root) {
763
+ let complexity = 1;
764
+ walkAST(root, (n) => {
765
+ switch (n.type) {
766
+ case AST_NODE_TYPES.IfStatement:
767
+ case AST_NODE_TYPES.ConditionalExpression:
768
+ case AST_NODE_TYPES.ForStatement:
769
+ case AST_NODE_TYPES.ForInStatement:
770
+ case AST_NODE_TYPES.ForOfStatement:
771
+ case AST_NODE_TYPES.WhileStatement:
772
+ case AST_NODE_TYPES.DoWhileStatement:
773
+ case AST_NODE_TYPES.CatchClause:
774
+ complexity++;
775
+ break;
776
+ case AST_NODE_TYPES.SwitchCase:
777
+ if (n.test) complexity++;
778
+ break;
779
+ case AST_NODE_TYPES.LogicalExpression:
780
+ if (n.operator === "&&" || n.operator === "||" || n.operator === "??") {
781
+ complexity++;
782
+ }
783
+ break;
784
+ }
785
+ });
786
+ return complexity;
787
+ }
788
+ function calculateCognitiveComplexity(root) {
789
+ let complexity = 0;
790
+ const depthMap = /* @__PURE__ */ new WeakMap();
791
+ depthMap.set(root, 0);
792
+ walkAST(root, (n, parent) => {
793
+ const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
794
+ const isNesting = isNestingNode(n);
795
+ const depth = isNesting ? parentDepth + 1 : parentDepth;
796
+ depthMap.set(n, depth);
797
+ if (isNesting) {
798
+ complexity += 1 + parentDepth;
799
+ }
800
+ if (n.type === AST_NODE_TYPES.LogicalExpression && (n.operator === "&&" || n.operator === "||" || n.operator === "??")) {
801
+ complexity += 1;
802
+ }
803
+ });
804
+ return complexity;
805
+ }
806
+ function calculateMaxNestingDepth(root) {
807
+ let maxDepth = 0;
808
+ const depthMap = /* @__PURE__ */ new WeakMap();
809
+ depthMap.set(root, 0);
810
+ walkAST(root, (n, parent) => {
811
+ const parentDepth = parent ? depthMap.get(parent) ?? 0 : 0;
812
+ const isNesting = n.type === AST_NODE_TYPES.IfStatement || n.type === AST_NODE_TYPES.ForStatement || n.type === AST_NODE_TYPES.ForInStatement || n.type === AST_NODE_TYPES.ForOfStatement || n.type === AST_NODE_TYPES.WhileStatement || n.type === AST_NODE_TYPES.DoWhileStatement || n.type === AST_NODE_TYPES.SwitchStatement || n.type === AST_NODE_TYPES.TryStatement;
813
+ const currentDepth = isNesting ? parentDepth + 1 : parentDepth;
814
+ depthMap.set(n, currentDepth);
815
+ if (currentDepth > maxDepth) maxDepth = currentDepth;
816
+ });
817
+ return maxDepth;
818
+ }
819
+ function isNestingNode(n) {
820
+ return n.type === AST_NODE_TYPES.IfStatement || n.type === AST_NODE_TYPES.ForStatement || n.type === AST_NODE_TYPES.ForInStatement || n.type === AST_NODE_TYPES.ForOfStatement || n.type === AST_NODE_TYPES.WhileStatement || n.type === AST_NODE_TYPES.DoWhileStatement || n.type === AST_NODE_TYPES.SwitchStatement || n.type === AST_NODE_TYPES.CatchClause || n.type === AST_NODE_TYPES.ConditionalExpression;
821
+ }
822
+
823
+ // src/rules/builtin/unused-variables.ts
824
+ var unusedVariablesRule = {
825
+ id: "logic/unused-variables",
826
+ category: "logic",
827
+ severity: "low",
828
+ title: "Unused variable detected",
829
+ description: "AI-generated code sometimes declares variables that are never used, indicating incomplete or hallucinated logic.",
830
+ check(context) {
831
+ const issues = [];
832
+ let ast;
833
+ try {
834
+ const parsed = parseCode(context.fileContent, context.filePath);
835
+ ast = parsed.ast;
836
+ } catch {
837
+ return issues;
838
+ }
839
+ const declarations = /* @__PURE__ */ new Map();
840
+ const references = /* @__PURE__ */ new Set();
841
+ collectDeclarationsAndReferences(ast, declarations, references);
842
+ for (const [name, info] of declarations) {
843
+ if (name.startsWith("_")) continue;
844
+ if (info.kind === "export") continue;
845
+ if (!references.has(name)) {
846
+ issues.push({
847
+ ruleId: "logic/unused-variables",
848
+ severity: "low",
849
+ category: "logic",
850
+ file: context.filePath,
851
+ startLine: info.line,
852
+ endLine: info.line,
853
+ message: t(
854
+ `Variable "${name}" is declared but never used.`,
855
+ `\u53D8\u91CF "${name}" \u5DF2\u58F0\u660E\u4F46\u4ECE\u672A\u4F7F\u7528\u3002`
856
+ ),
857
+ suggestion: t(
858
+ `Remove the unused variable "${name}" or prefix it with _ if intentionally unused.`,
859
+ `\u79FB\u9664\u672A\u4F7F\u7528\u7684\u53D8\u91CF "${name}"\uFF0C\u6216\u7528 _ \u524D\u7F00\u6807\u8BB0\u4E3A\u6709\u610F\u5FFD\u7565\u3002`
860
+ )
861
+ });
862
+ }
863
+ }
864
+ return issues;
865
+ }
866
+ };
867
+ function collectDeclarationsAndReferences(root, declarations, references) {
868
+ const exportedNames = /* @__PURE__ */ new Set();
869
+ walkAST(root, (node) => {
870
+ if (node.type === AST_NODE_TYPES.ExportNamedDeclaration) {
871
+ walkAST(node, (inner) => {
872
+ if (inner.type === AST_NODE_TYPES.VariableDeclarator && inner.id.type === AST_NODE_TYPES.Identifier) {
873
+ exportedNames.add(inner.id.name);
874
+ }
875
+ if (inner.type === AST_NODE_TYPES.FunctionDeclaration && inner.id) {
876
+ exportedNames.add(inner.id.name);
877
+ }
878
+ });
879
+ }
880
+ });
881
+ walkAST(root, (node, parent) => {
882
+ const parentType = parent?.type;
883
+ if (node.type === AST_NODE_TYPES.VariableDeclarator) {
884
+ if (node.id.type === AST_NODE_TYPES.Identifier) {
885
+ declarations.set(node.id.name, {
886
+ line: node.loc?.start.line ?? 0,
887
+ kind: exportedNames.has(node.id.name) ? "export" : "local"
888
+ });
889
+ }
890
+ if (node.init) {
891
+ walkAST(node.init, (n) => {
892
+ if (n.type === AST_NODE_TYPES.Identifier) {
893
+ references.add(n.name);
894
+ }
895
+ });
896
+ }
897
+ return false;
898
+ }
899
+ if (node.type === AST_NODE_TYPES.FunctionDeclaration && node.id) {
900
+ declarations.set(node.id.name, {
901
+ line: node.loc?.start.line ?? 0,
902
+ kind: exportedNames.has(node.id.name) ? "export" : "local"
903
+ });
904
+ }
905
+ if (node.type === AST_NODE_TYPES.Identifier && parentType !== "VariableDeclarator" && parentType !== "FunctionDeclaration") {
906
+ references.add(node.name);
907
+ }
908
+ return;
909
+ });
910
+ }
911
+
912
+ // src/rules/builtin/duplicate-condition.ts
913
+ var duplicateConditionRule = {
914
+ id: "logic/duplicate-condition",
915
+ category: "logic",
916
+ severity: "medium",
917
+ title: "Duplicate condition in if-else chain",
918
+ description: "AI-generated code sometimes contains duplicate conditions in if-else chains, making later branches unreachable.",
919
+ check(context) {
920
+ const issues = [];
921
+ let ast;
922
+ try {
923
+ const parsed = parseCode(context.fileContent, context.filePath);
924
+ ast = parsed.ast;
925
+ } catch {
926
+ return issues;
927
+ }
928
+ const visited = /* @__PURE__ */ new WeakSet();
929
+ walkAST(ast, (node) => {
930
+ if (node.type === AST_NODE_TYPES.IfStatement && !visited.has(node)) {
931
+ const conditions = [];
932
+ collectIfElseChainConditions(node, conditions, visited);
933
+ if (conditions.length >= 2) {
934
+ const seen = /* @__PURE__ */ new Map();
935
+ for (const cond of conditions) {
936
+ if (seen.has(cond.text)) {
937
+ const firstLine = seen.get(cond.text);
938
+ issues.push({
939
+ ruleId: "logic/duplicate-condition",
940
+ severity: "medium",
941
+ category: "logic",
942
+ file: context.filePath,
943
+ startLine: cond.line,
944
+ endLine: cond.line,
945
+ message: t(
946
+ `Duplicate condition "${truncate(cond.text, 40)}" \u2014 same condition already checked at line ${firstLine}.`,
947
+ `\u91CD\u590D\u6761\u4EF6 "${truncate(cond.text, 40)}" \u2014 \u76F8\u540C\u6761\u4EF6\u5DF2\u5728\u7B2C ${firstLine} \u884C\u68C0\u67E5\u8FC7\u3002`
948
+ ),
949
+ suggestion: t(
950
+ "Remove the duplicate branch or change the condition.",
951
+ "\u79FB\u9664\u91CD\u590D\u7684\u5206\u652F\u6216\u4FEE\u6539\u6761\u4EF6\u3002"
952
+ )
953
+ });
954
+ } else {
955
+ seen.set(cond.text, cond.line);
956
+ }
957
+ }
958
+ }
959
+ }
960
+ });
961
+ return issues;
962
+ }
963
+ };
964
+ function collectIfElseChainConditions(node, conditions, visited) {
965
+ visited.add(node);
966
+ const condText = stringifyCondition(node.test);
967
+ conditions.push({ text: condText, line: node.loc?.start.line ?? 0 });
968
+ if (node.alternate?.type === AST_NODE_TYPES.IfStatement) {
969
+ collectIfElseChainConditions(node.alternate, conditions, visited);
970
+ }
971
+ }
972
+ function stringifyCondition(node) {
973
+ switch (node.type) {
974
+ case AST_NODE_TYPES.Identifier:
975
+ return node.name;
976
+ case AST_NODE_TYPES.Literal:
977
+ return String(node.value);
978
+ case AST_NODE_TYPES.BinaryExpression:
979
+ case AST_NODE_TYPES.LogicalExpression:
980
+ return `${stringifyCondition(node.left)} ${node.operator} ${stringifyCondition(node.right)}`;
981
+ case AST_NODE_TYPES.UnaryExpression:
982
+ return `${node.operator}${stringifyCondition(node.argument)}`;
983
+ case AST_NODE_TYPES.MemberExpression:
984
+ return `${stringifyCondition(node.object)}.${stringifyCondition(node.property)}`;
985
+ case AST_NODE_TYPES.CallExpression:
986
+ return `${stringifyCondition(node.callee)}(...)`;
987
+ default:
988
+ return `[${node.type}]`;
989
+ }
990
+ }
991
+ function truncate(s, maxLen) {
992
+ return s.length > maxLen ? s.slice(0, maxLen) + "..." : s;
993
+ }
994
+
995
+ // src/rules/builtin/security.ts
996
+ var securityRules = [
997
+ {
998
+ id: "security/hardcoded-secret",
999
+ category: "security",
1000
+ severity: "high",
1001
+ title: "Hardcoded secret or API key",
1002
+ description: "Hardcoded secrets, API keys, passwords, or tokens in source code.",
1003
+ check(context) {
1004
+ const issues = [];
1005
+ const lines = context.fileContent.split("\n");
1006
+ const secretPatterns = [
1007
+ // API keys / tokens
1008
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`][A-Za-z0-9_\-]{16,}['"`]/i, label: "API key" },
1009
+ { pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}['"`]/i, label: "secret/password" },
1010
+ // AWS
1011
+ { pattern: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
1012
+ // GitHub
1013
+ { pattern: /gh[ps]_[A-Za-z0-9_]{36,}/i, label: "GitHub Token" },
1014
+ // Generic long hex/base64 strings assigned to key-like variables
1015
+ { pattern: /(?:key|secret|token|auth)\s*[:=]\s*['"`][A-Fa-f0-9]{32,}['"`]/i, label: "hex secret" },
1016
+ // Private key
1017
+ { pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, label: "private key" },
1018
+ // JWT
1019
+ { pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, label: "JWT token" }
1020
+ ];
1021
+ for (let i = 0; i < lines.length; i++) {
1022
+ const line = lines[i];
1023
+ const trimmed = line.trim();
1024
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("import ")) continue;
1025
+ if (/process\.env\b/.test(line)) continue;
1026
+ for (const { pattern, label } of secretPatterns) {
1027
+ if (pattern.test(line)) {
1028
+ issues.push({
1029
+ ruleId: "security/hardcoded-secret",
1030
+ severity: "high",
1031
+ category: "security",
1032
+ file: context.filePath,
1033
+ startLine: i + 1,
1034
+ endLine: i + 1,
1035
+ message: t(
1036
+ `Possible hardcoded ${label} detected. Never commit secrets to source code.`,
1037
+ `\u68C0\u6D4B\u5230\u53EF\u80FD\u7684\u786C\u7F16\u7801${label}\u3002\u6C38\u8FDC\u4E0D\u8981\u5C06\u5BC6\u94A5\u63D0\u4EA4\u5230\u6E90\u4EE3\u7801\u4E2D\u3002`
1038
+ ),
1039
+ suggestion: t(
1040
+ "Use environment variables or a secrets manager instead.",
1041
+ "\u8BF7\u6539\u7528\u73AF\u5883\u53D8\u91CF\u6216\u5BC6\u94A5\u7BA1\u7406\u670D\u52A1\u3002"
1042
+ )
1043
+ });
1044
+ break;
1045
+ }
1046
+ }
1047
+ }
1048
+ return issues;
1049
+ }
1050
+ },
1051
+ {
1052
+ id: "security/eval-usage",
1053
+ category: "security",
1054
+ severity: "high",
1055
+ title: "Dangerous eval() usage",
1056
+ description: "eval() can execute arbitrary code and is a major security risk.",
1057
+ check(context) {
1058
+ const issues = [];
1059
+ const lines = context.fileContent.split("\n");
1060
+ for (let i = 0; i < lines.length; i++) {
1061
+ const trimmed = lines[i].trim();
1062
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1063
+ const evalPatterns = [
1064
+ { pattern: /\beval\s*\(/, label: "eval()" },
1065
+ { pattern: /new\s+Function\s*\(/, label: "new Function()" },
1066
+ { pattern: /\b(setTimeout|setInterval)\s*\(\s*['"`]/, label: "setTimeout/setInterval with string" }
1067
+ ];
1068
+ for (const { pattern, label } of evalPatterns) {
1069
+ if (pattern.test(lines[i])) {
1070
+ issues.push({
1071
+ ruleId: "security/eval-usage",
1072
+ severity: "high",
1073
+ category: "security",
1074
+ file: context.filePath,
1075
+ startLine: i + 1,
1076
+ endLine: i + 1,
1077
+ message: t(
1078
+ `Dangerous ${label} detected \u2014 can execute arbitrary code.`,
1079
+ `\u68C0\u6D4B\u5230\u5371\u9669\u7684 ${label} \u2014 \u53EF\u6267\u884C\u4EFB\u610F\u4EE3\u7801\u3002`
1080
+ ),
1081
+ suggestion: t(
1082
+ `Avoid ${label}. Use safer alternatives like JSON.parse() or proper function references.`,
1083
+ `\u907F\u514D\u4F7F\u7528 ${label}\u3002\u4F7F\u7528\u66F4\u5B89\u5168\u7684\u66FF\u4EE3\u65B9\u6848\uFF0C\u5982 JSON.parse() \u6216\u51FD\u6570\u5F15\u7528\u3002`
1084
+ )
1085
+ });
1086
+ break;
1087
+ }
1088
+ }
1089
+ }
1090
+ return issues;
1091
+ }
1092
+ },
1093
+ {
1094
+ id: "security/sql-injection",
1095
+ category: "security",
1096
+ severity: "high",
1097
+ title: "Potential SQL injection",
1098
+ description: "String concatenation or template literals in SQL queries can lead to SQL injection.",
1099
+ check(context) {
1100
+ const issues = [];
1101
+ const lines = context.fileContent.split("\n");
1102
+ for (let i = 0; i < lines.length; i++) {
1103
+ const trimmed = lines[i].trim();
1104
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1105
+ const sqlKeywords = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b/i;
1106
+ if (sqlKeywords.test(lines[i])) {
1107
+ if (/\$\{[^}]+\}/.test(lines[i]) || /['"]\s*\+\s*\w+/.test(lines[i])) {
1108
+ issues.push({
1109
+ ruleId: "security/sql-injection",
1110
+ severity: "high",
1111
+ category: "security",
1112
+ file: context.filePath,
1113
+ startLine: i + 1,
1114
+ endLine: i + 1,
1115
+ message: t(
1116
+ "Potential SQL injection \u2014 string interpolation in SQL query.",
1117
+ "\u6F5C\u5728\u7684 SQL \u6CE8\u5165 \u2014 SQL \u67E5\u8BE2\u4E2D\u4F7F\u7528\u4E86\u5B57\u7B26\u4E32\u63D2\u503C\u3002"
1118
+ ),
1119
+ suggestion: t(
1120
+ "Use parameterized queries or prepared statements instead.",
1121
+ "\u8BF7\u6539\u7528\u53C2\u6570\u5316\u67E5\u8BE2\u6216\u9884\u7F16\u8BD1\u8BED\u53E5\u3002"
1122
+ )
1123
+ });
1124
+ }
1125
+ }
1126
+ }
1127
+ return issues;
1128
+ }
1129
+ },
1130
+ {
1131
+ id: "security/dangerous-html",
1132
+ category: "security",
1133
+ severity: "medium",
1134
+ title: "Dangerous HTML manipulation",
1135
+ description: "Direct innerHTML/outerHTML assignment can lead to XSS attacks.",
1136
+ check(context) {
1137
+ const issues = [];
1138
+ const lines = context.fileContent.split("\n");
1139
+ for (let i = 0; i < lines.length; i++) {
1140
+ const trimmed = lines[i].trim();
1141
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1142
+ if (/\.(innerHTML|outerHTML)\s*=/.test(lines[i]) || /dangerouslySetInnerHTML/.test(lines[i])) {
1143
+ issues.push({
1144
+ ruleId: "security/dangerous-html",
1145
+ severity: "medium",
1146
+ category: "security",
1147
+ file: context.filePath,
1148
+ startLine: i + 1,
1149
+ endLine: i + 1,
1150
+ message: t(
1151
+ "Direct HTML assignment detected \u2014 potential XSS vulnerability.",
1152
+ "\u68C0\u6D4B\u5230\u76F4\u63A5 HTML \u8D4B\u503C \u2014 \u53EF\u80FD\u5B58\u5728 XSS \u6F0F\u6D1E\u3002"
1153
+ ),
1154
+ suggestion: t(
1155
+ "Use safe DOM APIs like textContent, or sanitize HTML before insertion.",
1156
+ "\u4F7F\u7528\u5B89\u5168\u7684 DOM API\uFF08\u5982 textContent\uFF09\uFF0C\u6216\u5728\u63D2\u5165\u524D\u5BF9 HTML \u8FDB\u884C\u6E05\u6D17\u3002"
1157
+ )
1158
+ });
1159
+ }
1160
+ }
1161
+ return issues;
1162
+ }
1163
+ }
1164
+ ];
1165
+
1166
+ // src/rules/builtin/empty-catch.ts
1167
+ var emptyCatchRule = {
1168
+ id: "logic/empty-catch",
1169
+ category: "logic",
1170
+ severity: "medium",
1171
+ title: "Empty or useless catch block",
1172
+ description: "AI-generated code often includes catch blocks that silently swallow errors (empty body) or pointlessly re-throw the same error without modification.",
1173
+ check(context) {
1174
+ const issues = [];
1175
+ const lines = context.fileContent.split("\n");
1176
+ let inBlockComment = false;
1177
+ for (let i = 0; i < lines.length; i++) {
1178
+ const trimmed = lines[i].trim();
1179
+ if (inBlockComment) {
1180
+ if (trimmed.includes("*/")) inBlockComment = false;
1181
+ continue;
1182
+ }
1183
+ if (trimmed.startsWith("/*")) {
1184
+ if (!trimmed.includes("*/")) inBlockComment = true;
1185
+ continue;
1186
+ }
1187
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1188
+ const catchMatch = trimmed.match(/\bcatch\s*\(\s*(\w+)?\s*\)\s*\{/);
1189
+ if (!catchMatch) continue;
1190
+ const catchVarName = catchMatch[1] || "";
1191
+ const blockContent = extractCatchBody(lines, i);
1192
+ if (!blockContent) continue;
1193
+ const { bodyLines, endLine } = blockContent;
1194
+ const meaningful = bodyLines.filter(
1195
+ (l) => l.trim().length > 0 && !l.trim().startsWith("//") && !l.trim().startsWith("*")
1196
+ );
1197
+ if (meaningful.length === 0) {
1198
+ issues.push({
1199
+ ruleId: "logic/empty-catch",
1200
+ severity: "medium",
1201
+ category: "logic",
1202
+ file: context.filePath,
1203
+ startLine: i + 1,
1204
+ endLine: endLine + 1,
1205
+ message: t(
1206
+ "Empty catch block silently swallows errors. This is a common AI hallucination pattern.",
1207
+ "\u7A7A\u7684 catch \u5757\u9759\u9ED8\u541E\u6389\u4E86\u9519\u8BEF\u3002\u8FD9\u662F\u5E38\u89C1\u7684 AI \u5E7B\u89C9\u6A21\u5F0F\u3002"
1208
+ ),
1209
+ suggestion: t(
1210
+ "Add error handling logic, or remove the try-catch if the operation cannot fail.",
1211
+ "\u6DFB\u52A0\u9519\u8BEF\u5904\u7406\u903B\u8F91\uFF0C\u6216\u5728\u64CD\u4F5C\u4E0D\u4F1A\u629B\u9519\u65F6\u79FB\u9664 try-catch\u3002"
1212
+ )
1213
+ });
1214
+ continue;
1215
+ }
1216
+ if (catchVarName && meaningful.length === 1) {
1217
+ const onlyLine = meaningful[0].trim();
1218
+ if (onlyLine === `throw ${catchVarName};` || onlyLine === `throw ${catchVarName}`) {
1219
+ issues.push({
1220
+ ruleId: "logic/empty-catch",
1221
+ severity: "medium",
1222
+ category: "logic",
1223
+ file: context.filePath,
1224
+ startLine: i + 1,
1225
+ endLine: endLine + 1,
1226
+ message: t(
1227
+ `Catch block only re-throws the original error "${catchVarName}" without modification. The try-catch is pointless.`,
1228
+ `catch \u5757\u4EC5\u539F\u6837\u91CD\u65B0\u629B\u51FA\u9519\u8BEF "${catchVarName}"\uFF0C\u6CA1\u6709\u4EFB\u4F55\u4FEE\u6539\u3002try-catch \u6BEB\u65E0\u610F\u4E49\u3002`
1229
+ ),
1230
+ suggestion: t(
1231
+ "Remove the try-catch block entirely, or wrap the error with additional context.",
1232
+ "\u5B8C\u5168\u79FB\u9664 try-catch \u5757\uFF0C\u6216\u5728\u91CD\u65B0\u629B\u51FA\u65F6\u6DFB\u52A0\u989D\u5916\u7684\u4E0A\u4E0B\u6587\u4FE1\u606F\u3002"
1233
+ )
1234
+ });
1235
+ }
1236
+ }
1237
+ }
1238
+ return issues;
1239
+ }
1240
+ };
1241
+ function extractCatchBody(lines, catchLineIndex) {
1242
+ const catchLine = lines[catchLineIndex];
1243
+ const catchIdx = catchLine.indexOf("catch");
1244
+ if (catchIdx === -1) return null;
1245
+ let braceCount = 0;
1246
+ let started = false;
1247
+ let bodyStart = -1;
1248
+ for (let i = catchLineIndex; i < lines.length; i++) {
1249
+ const line = lines[i];
1250
+ const startJ = i === catchLineIndex ? catchIdx : 0;
1251
+ for (let j = startJ; j < line.length; j++) {
1252
+ const ch = line[j];
1253
+ if (ch === "{") {
1254
+ braceCount++;
1255
+ if (!started) {
1256
+ started = true;
1257
+ bodyStart = i;
1258
+ }
1259
+ } else if (ch === "}") {
1260
+ braceCount--;
1261
+ if (started && braceCount === 0) {
1262
+ return {
1263
+ bodyLines: lines.slice(bodyStart + 1, i),
1264
+ endLine: i
1265
+ };
1266
+ }
1267
+ }
1268
+ }
1269
+ }
1270
+ return null;
1271
+ }
1272
+
1273
+ // src/rules/builtin/identical-branches.ts
1274
+ var identicalBranchesRule = {
1275
+ id: "logic/identical-branches",
1276
+ category: "logic",
1277
+ severity: "medium",
1278
+ title: "Identical if/else branches",
1279
+ description: "AI-generated code sometimes contains if/else blocks where both branches have identical code, making the condition meaningless.",
1280
+ check(context) {
1281
+ const issues = [];
1282
+ const parsed = parseCode(context.fileContent, context.filePath);
1283
+ if (!parsed) return issues;
1284
+ const source = context.fileContent;
1285
+ walkAST(parsed.ast, (node) => {
1286
+ if (node.type === AST_NODE_TYPES.IfStatement && node.consequent && node.alternate) {
1287
+ if (node.alternate.type === AST_NODE_TYPES.IfStatement) return;
1288
+ const thenCode = extractBlockText(source, node.consequent);
1289
+ const elseCode = extractBlockText(source, node.alternate);
1290
+ if (thenCode && elseCode && thenCode === elseCode && thenCode.length > 0) {
1291
+ issues.push({
1292
+ ruleId: "logic/identical-branches",
1293
+ severity: "medium",
1294
+ category: "logic",
1295
+ file: context.filePath,
1296
+ startLine: node.loc?.start.line ?? 0,
1297
+ endLine: node.loc?.end.line ?? 0,
1298
+ message: t(
1299
+ "The if and else branches contain identical code. The condition is meaningless.",
1300
+ "if \u548C else \u5206\u652F\u5305\u542B\u5B8C\u5168\u76F8\u540C\u7684\u4EE3\u7801\uFF0C\u6761\u4EF6\u5224\u65AD\u6BEB\u65E0\u610F\u4E49\u3002"
1301
+ ),
1302
+ suggestion: t(
1303
+ "Remove the if/else and keep only one copy of the code, or fix the branching logic.",
1304
+ "\u79FB\u9664 if/else\uFF0C\u53EA\u4FDD\u7559\u4E00\u4EFD\u4EE3\u7801\uFF1B\u6216\u4FEE\u590D\u5206\u652F\u903B\u8F91\u3002"
1305
+ )
1306
+ });
1307
+ }
1308
+ }
1309
+ });
1310
+ return issues;
1311
+ }
1312
+ };
1313
+ function extractBlockText(source, node) {
1314
+ if (!node.range) return "";
1315
+ let text = source.slice(node.range[0], node.range[1]);
1316
+ if (text.startsWith("{")) text = text.slice(1);
1317
+ if (text.endsWith("}")) text = text.slice(0, -1);
1318
+ return text.trim().replace(/\s+/g, " ");
1319
+ }
1320
+
1321
+ // src/rules/builtin/redundant-else.ts
1322
+ var redundantElseRule = {
1323
+ id: "logic/redundant-else",
1324
+ category: "logic",
1325
+ severity: "low",
1326
+ title: "Redundant else after return/throw",
1327
+ description: "AI-generated code often uses else blocks after if blocks that already return/throw. The else is unnecessary and adds nesting.",
1328
+ check(context) {
1329
+ const issues = [];
1330
+ const parsed = parseCode(context.fileContent, context.filePath);
1331
+ if (!parsed) return issues;
1332
+ walkAST(parsed.ast, (node) => {
1333
+ if (node.type === AST_NODE_TYPES.IfStatement && node.consequent && node.alternate && node.alternate.type !== AST_NODE_TYPES.IfStatement) {
1334
+ if (blockEndsWithExit(node.consequent)) {
1335
+ issues.push({
1336
+ ruleId: "logic/redundant-else",
1337
+ severity: "low",
1338
+ category: "logic",
1339
+ file: context.filePath,
1340
+ startLine: node.loc?.start.line ?? 0,
1341
+ endLine: node.loc?.end.line ?? 0,
1342
+ message: t(
1343
+ "Unnecessary else \u2014 the if block already returns/throws. The else adds pointless nesting.",
1344
+ "\u4E0D\u5FC5\u8981\u7684 else \u2014 if \u5757\u5DF2\u7ECF return/throw \u4E86\uFF0Celse \u589E\u52A0\u4E86\u65E0\u610F\u4E49\u7684\u5D4C\u5957\u3002"
1345
+ ),
1346
+ suggestion: t(
1347
+ "Remove the else wrapper and place its code after the if block (early return pattern).",
1348
+ "\u79FB\u9664 else \u5305\u88F9\uFF0C\u5C06\u5176\u4EE3\u7801\u653E\u5728 if \u5757\u4E4B\u540E\uFF08\u63D0\u524D\u8FD4\u56DE\u6A21\u5F0F\uFF09\u3002"
1349
+ )
1350
+ });
1351
+ }
1352
+ }
1353
+ return;
1354
+ });
1355
+ return issues;
1356
+ }
1357
+ };
1358
+ function blockEndsWithExit(node) {
1359
+ if (node.type === AST_NODE_TYPES.BlockStatement) {
1360
+ if (!Array.isArray(node.body)) return false;
1361
+ const body = node.body;
1362
+ if (body.length === 0) return false;
1363
+ const last = body[body.length - 1];
1364
+ return last.type === AST_NODE_TYPES.ReturnStatement || last.type === AST_NODE_TYPES.ThrowStatement;
1365
+ }
1366
+ return node.type === AST_NODE_TYPES.ReturnStatement || node.type === AST_NODE_TYPES.ThrowStatement;
1367
+ }
1368
+
1369
+ // src/rules/builtin/console-in-code.ts
1370
+ var consoleInCodeRule = {
1371
+ id: "logic/console-in-code",
1372
+ category: "logic",
1373
+ severity: "info",
1374
+ title: "Console statement left in code",
1375
+ description: "AI-generated code often includes console.log/warn/error statements intended for debugging that should be removed or replaced with a proper logger.",
1376
+ check(context) {
1377
+ const issues = [];
1378
+ const lines = context.fileContent.split("\n");
1379
+ const lowerPath = context.filePath.toLowerCase();
1380
+ if (lowerPath.includes("/cli/") || lowerPath.includes("logger") || lowerPath.includes("log.") || lowerPath.endsWith(".test.ts") || lowerPath.endsWith(".test.js") || lowerPath.endsWith(".spec.ts") || lowerPath.endsWith(".spec.js")) {
1381
+ return issues;
1382
+ }
1383
+ let count = 0;
1384
+ const locations = [];
1385
+ let inBlockComment = false;
1386
+ for (let i = 0; i < lines.length; i++) {
1387
+ const trimmed = lines[i].trim();
1388
+ if (inBlockComment) {
1389
+ if (trimmed.includes("*/")) inBlockComment = false;
1390
+ continue;
1391
+ }
1392
+ if (trimmed.startsWith("/*")) {
1393
+ if (!trimmed.includes("*/")) inBlockComment = true;
1394
+ continue;
1395
+ }
1396
+ if (trimmed.startsWith("//")) continue;
1397
+ if (/\bconsole\.(log|warn|error|info|debug|trace)\s*\(/.test(trimmed)) {
1398
+ count++;
1399
+ locations.push(i + 1);
1400
+ }
1401
+ }
1402
+ if (count >= 3) {
1403
+ issues.push({
1404
+ ruleId: "logic/console-in-code",
1405
+ severity: "info",
1406
+ category: "logic",
1407
+ file: context.filePath,
1408
+ startLine: locations[0],
1409
+ endLine: locations[locations.length - 1],
1410
+ message: t(
1411
+ `${count} console statements found. AI-generated code often leaves debug logging that should be removed or replaced with a proper logger.`,
1412
+ `\u53D1\u73B0 ${count} \u4E2A console \u8BED\u53E5\u3002AI \u751F\u6210\u7684\u4EE3\u7801\u7ECF\u5E38\u7559\u4E0B\u8C03\u8BD5\u65E5\u5FD7\uFF0C\u5E94\u8BE5\u79FB\u9664\u6216\u66FF\u6362\u4E3A\u6B63\u5F0F\u7684\u65E5\u5FD7\u5DE5\u5177\u3002`
1413
+ ),
1414
+ suggestion: t(
1415
+ "Remove console statements or replace with a structured logger (e.g. winston, pino).",
1416
+ "\u79FB\u9664 console \u8BED\u53E5\u6216\u66FF\u6362\u4E3A\u7ED3\u6784\u5316\u65E5\u5FD7\u5DE5\u5177\uFF08\u5982 winston\u3001pino\uFF09\u3002"
1417
+ )
1418
+ });
1419
+ }
1420
+ return issues;
1421
+ }
1422
+ };
1423
+
1424
+ // src/rules/builtin/phantom-import.ts
1425
+ import { existsSync } from "fs";
1426
+ import { resolve, dirname } from "path";
1427
+ var phantomImportRule = {
1428
+ id: "logic/phantom-import",
1429
+ category: "logic",
1430
+ severity: "high",
1431
+ title: "Phantom import \u2014 module does not exist",
1432
+ description: "AI-generated code frequently imports from non-existent relative paths, indicating hallucinated modules or functions.",
1433
+ check(context) {
1434
+ const issues = [];
1435
+ if (!context.filePath || context.filePath === "<unknown>") {
1436
+ return issues;
1437
+ }
1438
+ let ast;
1439
+ try {
1440
+ const parsed = parseCode(context.fileContent, context.filePath);
1441
+ ast = parsed.ast;
1442
+ } catch {
1443
+ return issues;
1444
+ }
1445
+ const fileDir = dirname(resolve(context.filePath));
1446
+ walkAST(ast, (node) => {
1447
+ if (node.type === AST_NODE_TYPES.ImportDeclaration) {
1448
+ const source = node.source.value;
1449
+ if (typeof source === "string" && isRelativePath(source)) {
1450
+ if (!resolveModulePath(fileDir, source)) {
1451
+ issues.push({
1452
+ ruleId: "logic/phantom-import",
1453
+ severity: "high",
1454
+ category: "logic",
1455
+ file: context.filePath,
1456
+ startLine: node.loc?.start.line ?? 0,
1457
+ endLine: node.loc?.end.line ?? 0,
1458
+ message: t(
1459
+ `Import from "${source}" \u2014 module does not exist.`,
1460
+ `\u5BFC\u5165 "${source}" \u2014 \u6A21\u5757\u4E0D\u5B58\u5728\u3002`
1461
+ ),
1462
+ suggestion: t(
1463
+ "Verify the import path. The AI may have hallucinated this module.",
1464
+ "\u68C0\u67E5\u5BFC\u5165\u8DEF\u5F84\uFF0CAI \u53EF\u80FD\u7F16\u9020\u4E86\u8FD9\u4E2A\u6A21\u5757\u3002"
1465
+ )
1466
+ });
1467
+ }
1468
+ }
1469
+ }
1470
+ if (node.type === AST_NODE_TYPES.ImportExpression && node.source.type === AST_NODE_TYPES.Literal && typeof node.source.value === "string") {
1471
+ const source = node.source.value;
1472
+ if (isRelativePath(source) && !resolveModulePath(fileDir, source)) {
1473
+ issues.push({
1474
+ ruleId: "logic/phantom-import",
1475
+ severity: "high",
1476
+ category: "logic",
1477
+ file: context.filePath,
1478
+ startLine: node.loc?.start.line ?? 0,
1479
+ endLine: node.loc?.end.line ?? 0,
1480
+ message: t(
1481
+ `Dynamic import "${source}" \u2014 module does not exist.`,
1482
+ `\u52A8\u6001\u5BFC\u5165 "${source}" \u2014 \u6A21\u5757\u4E0D\u5B58\u5728\u3002`
1483
+ ),
1484
+ suggestion: t(
1485
+ "Verify the import path. The AI may have hallucinated this module.",
1486
+ "\u68C0\u67E5\u5BFC\u5165\u8DEF\u5F84\uFF0CAI \u53EF\u80FD\u7F16\u9020\u4E86\u8FD9\u4E2A\u6A21\u5757\u3002"
1487
+ )
1488
+ });
1489
+ }
1490
+ }
1491
+ if (node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "require" && node.arguments.length >= 1 && node.arguments[0].type === AST_NODE_TYPES.Literal && typeof node.arguments[0].value === "string") {
1492
+ const source = node.arguments[0].value;
1493
+ if (isRelativePath(source) && !resolveModulePath(fileDir, source)) {
1494
+ issues.push({
1495
+ ruleId: "logic/phantom-import",
1496
+ severity: "high",
1497
+ category: "logic",
1498
+ file: context.filePath,
1499
+ startLine: node.loc?.start.line ?? 0,
1500
+ endLine: node.loc?.end.line ?? 0,
1501
+ message: t(
1502
+ `Require "${source}" \u2014 module does not exist.`,
1503
+ `Require "${source}" \u2014 \u6A21\u5757\u4E0D\u5B58\u5728\u3002`
1504
+ ),
1505
+ suggestion: t(
1506
+ "Verify the require path. The AI may have hallucinated this module.",
1507
+ "\u68C0\u67E5 require \u8DEF\u5F84\uFF0CAI \u53EF\u80FD\u7F16\u9020\u4E86\u8FD9\u4E2A\u6A21\u5757\u3002"
1508
+ )
1509
+ });
1510
+ }
1511
+ }
1512
+ });
1513
+ return issues;
1514
+ }
1515
+ };
1516
+ function isRelativePath(source) {
1517
+ return source.startsWith("./") || source.startsWith("../");
1518
+ }
1519
+ function resolveModulePath(dir, importPath) {
1520
+ const resolved = resolve(dir, importPath);
1521
+ if (existsSync(resolved)) return true;
1522
+ const extMap = {
1523
+ ".js": [".ts", ".tsx"],
1524
+ ".jsx": [".tsx"],
1525
+ ".mjs": [".mts"],
1526
+ ".cjs": [".cts"]
1527
+ };
1528
+ for (const [fromExt, toExts] of Object.entries(extMap)) {
1529
+ if (importPath.endsWith(fromExt)) {
1530
+ const base = resolved.slice(0, -fromExt.length);
1531
+ for (const toExt of toExts) {
1532
+ if (existsSync(base + toExt)) return true;
1533
+ }
1534
+ }
1535
+ }
1536
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs", ".json"];
1537
+ for (const ext of extensions) {
1538
+ if (existsSync(resolved + ext)) return true;
1539
+ }
1540
+ for (const ext of extensions) {
1541
+ if (existsSync(resolve(resolved, `index${ext}`))) return true;
1542
+ }
1543
+ return false;
1544
+ }
1545
+
1546
+ // src/rules/builtin/unused-import.ts
1547
+ var unusedImportRule = {
1548
+ id: "logic/unused-import",
1549
+ category: "logic",
1550
+ severity: "low",
1551
+ title: "Unused import",
1552
+ description: "AI-generated code often imports modules or identifiers that are never used in the file.",
1553
+ check(context) {
1554
+ const issues = [];
1555
+ let ast;
1556
+ try {
1557
+ const parsed = parseCode(context.fileContent, context.filePath);
1558
+ ast = parsed.ast;
1559
+ } catch {
1560
+ return issues;
1561
+ }
1562
+ const imports = [];
1563
+ const namespaceImports = /* @__PURE__ */ new Set();
1564
+ for (const node of ast.body) {
1565
+ if (node.type !== AST_NODE_TYPES.ImportDeclaration) continue;
1566
+ const source = String(node.source.value);
1567
+ const isTypeOnlyImport = node.importKind === "type";
1568
+ for (const spec of node.specifiers) {
1569
+ if (spec.type === AST_NODE_TYPES.ImportNamespaceSpecifier) {
1570
+ namespaceImports.add(spec.local.name);
1571
+ continue;
1572
+ }
1573
+ const isTypeOnlySpec = isTypeOnlyImport || spec.type === AST_NODE_TYPES.ImportSpecifier && spec.importKind === "type";
1574
+ imports.push({
1575
+ name: spec.type === AST_NODE_TYPES.ImportSpecifier ? spec.imported.name : "default",
1576
+ local: spec.local.name,
1577
+ line: spec.loc?.start.line ?? 0,
1578
+ isTypeOnly: isTypeOnlySpec,
1579
+ source
1580
+ });
1581
+ }
1582
+ }
1583
+ if (imports.length === 0) return issues;
1584
+ const references = /* @__PURE__ */ new Set();
1585
+ const importNodes = /* @__PURE__ */ new WeakSet();
1586
+ for (const node of ast.body) {
1587
+ if (node.type === AST_NODE_TYPES.ImportDeclaration) {
1588
+ importNodes.add(node);
1589
+ }
1590
+ }
1591
+ walkAST(ast, (node) => {
1592
+ if (importNodes.has(node)) return false;
1593
+ if (node.type === AST_NODE_TYPES.Identifier) {
1594
+ references.add(node.name);
1595
+ }
1596
+ if (node.type === AST_NODE_TYPES.JSXIdentifier) {
1597
+ references.add(node.name);
1598
+ }
1599
+ return;
1600
+ });
1601
+ const typeRefPattern = /\b([A-Z][A-Za-z0-9]*)\b/g;
1602
+ let match;
1603
+ while ((match = typeRefPattern.exec(context.fileContent)) !== null) {
1604
+ references.add(match[1]);
1605
+ }
1606
+ for (const imp of imports) {
1607
+ if (!references.has(imp.local)) {
1608
+ issues.push({
1609
+ ruleId: "logic/unused-import",
1610
+ severity: "low",
1611
+ category: "logic",
1612
+ file: context.filePath,
1613
+ startLine: imp.line,
1614
+ endLine: imp.line,
1615
+ message: t(
1616
+ `Imported "${imp.local}" from "${imp.source}" is never used.`,
1617
+ `\u4ECE "${imp.source}" \u5BFC\u5165\u7684 "${imp.local}" \u4ECE\u672A\u4F7F\u7528\u3002`
1618
+ ),
1619
+ suggestion: t(
1620
+ `Remove the unused import "${imp.local}".`,
1621
+ `\u79FB\u9664\u672A\u4F7F\u7528\u7684\u5BFC\u5165 "${imp.local}"\u3002`
1622
+ )
1623
+ });
1624
+ }
1625
+ }
1626
+ return issues;
1627
+ }
1628
+ };
1629
+
1630
+ // src/rules/builtin/missing-await.ts
1631
+ var missingAwaitRule = {
1632
+ id: "logic/missing-await",
1633
+ category: "logic",
1634
+ severity: "medium",
1635
+ title: "Missing await on async call",
1636
+ description: "AI-generated code often omits `await` when calling async functions, leading to unhandled promises and race conditions.",
1637
+ check(context) {
1638
+ const issues = [];
1639
+ let ast;
1640
+ try {
1641
+ const parsed = parseCode(context.fileContent, context.filePath);
1642
+ ast = parsed.ast;
1643
+ } catch {
1644
+ return issues;
1645
+ }
1646
+ const asyncFuncNames = /* @__PURE__ */ new Set();
1647
+ collectAsyncFunctionNames(ast, asyncFuncNames);
1648
+ walkAST(ast, (node) => {
1649
+ if (!isAsyncFunction(node)) return;
1650
+ const body = getFunctionBody(node);
1651
+ if (!body) return;
1652
+ walkAST(body, (inner, parent) => {
1653
+ if (inner !== body && isAsyncFunction(inner)) return false;
1654
+ if (inner.type !== AST_NODE_TYPES.CallExpression) return;
1655
+ if (parent?.type === AST_NODE_TYPES.AwaitExpression) return;
1656
+ if (parent?.type === AST_NODE_TYPES.ReturnStatement) return;
1657
+ if (isInsidePromiseChain(inner, parent ?? null)) return;
1658
+ if (parent?.type === AST_NODE_TYPES.VariableDeclarator) return;
1659
+ if (parent?.type === AST_NODE_TYPES.AssignmentExpression) return;
1660
+ if (parent?.type === AST_NODE_TYPES.ArrayExpression) return;
1661
+ if (parent?.type === AST_NODE_TYPES.CallExpression && parent !== inner) return;
1662
+ const callName = getCallName(inner);
1663
+ if (!callName) return;
1664
+ if (!asyncFuncNames.has(callName)) return;
1665
+ issues.push({
1666
+ ruleId: "logic/missing-await",
1667
+ severity: "medium",
1668
+ category: "logic",
1669
+ file: context.filePath,
1670
+ startLine: inner.loc?.start.line ?? 0,
1671
+ endLine: inner.loc?.end.line ?? 0,
1672
+ message: t(
1673
+ `Call to async function "${callName}" is missing "await".`,
1674
+ `\u8C03\u7528\u5F02\u6B65\u51FD\u6570 "${callName}" \u65F6\u7F3A\u5C11 "await"\u3002`
1675
+ ),
1676
+ suggestion: t(
1677
+ `Add "await" before the call: await ${callName}(...)`,
1678
+ `\u5728\u8C03\u7528\u524D\u6DFB\u52A0 "await"\uFF1Aawait ${callName}(...)`
1679
+ )
1680
+ });
1681
+ });
1682
+ });
1683
+ return issues;
1684
+ }
1685
+ };
1686
+ function collectAsyncFunctionNames(ast, names) {
1687
+ walkAST(ast, (node) => {
1688
+ if (node.type === AST_NODE_TYPES.FunctionDeclaration && node.async && node.id) {
1689
+ names.add(node.id.name);
1690
+ }
1691
+ if (node.type === AST_NODE_TYPES.VariableDeclarator && node.id.type === AST_NODE_TYPES.Identifier && node.init && (node.init.type === AST_NODE_TYPES.ArrowFunctionExpression || node.init.type === AST_NODE_TYPES.FunctionExpression) && node.init.async) {
1692
+ names.add(node.id.name);
1693
+ }
1694
+ if (node.type === AST_NODE_TYPES.MethodDefinition && node.key.type === AST_NODE_TYPES.Identifier && node.value.async) {
1695
+ names.add(node.key.name);
1696
+ }
1697
+ });
1698
+ }
1699
+ function isAsyncFunction(node) {
1700
+ return node.type === AST_NODE_TYPES.FunctionDeclaration && node.async || node.type === AST_NODE_TYPES.FunctionExpression && node.async || node.type === AST_NODE_TYPES.ArrowFunctionExpression && node.async || node.type === AST_NODE_TYPES.MethodDefinition && node.value.async;
1701
+ }
1702
+ function getFunctionBody(node) {
1703
+ if (node.type === AST_NODE_TYPES.FunctionDeclaration || node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1704
+ return node.body;
1705
+ }
1706
+ if (node.type === AST_NODE_TYPES.MethodDefinition) {
1707
+ return node.value.body;
1708
+ }
1709
+ return null;
1710
+ }
1711
+ function getCallName(node) {
1712
+ const callee = node.callee;
1713
+ if (callee.type === AST_NODE_TYPES.Identifier) {
1714
+ return callee.name;
1715
+ }
1716
+ if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) {
1717
+ return callee.property.name;
1718
+ }
1719
+ return null;
1720
+ }
1721
+ function isInsidePromiseChain(_node, parent) {
1722
+ if (!parent) return false;
1723
+ if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier && (parent.property.name === "then" || parent.property.name === "catch" || parent.property.name === "finally")) {
1724
+ return true;
1725
+ }
1726
+ return false;
1727
+ }
1728
+
1729
+ // src/rules/engine.ts
1730
+ var BUILTIN_RULES = [
1731
+ unnecessaryTryCatchRule,
1732
+ overDefensiveRule,
1733
+ deadLogicRule,
1734
+ unusedVariablesRule,
1735
+ duplicateConditionRule,
1736
+ ...securityRules,
1737
+ emptyCatchRule,
1738
+ identicalBranchesRule,
1739
+ redundantElseRule,
1740
+ consoleInCodeRule,
1741
+ phantomImportRule,
1742
+ unusedImportRule,
1743
+ missingAwaitRule
1744
+ ];
1745
+ var RuleEngine = class {
1746
+ rules;
1747
+ constructor(config) {
1748
+ this.rules = BUILTIN_RULES.filter(
1749
+ (rule) => !config.rules.disabled.includes(rule.id)
1750
+ );
1751
+ }
1752
+ run(context) {
1753
+ const allIssues = [];
1754
+ for (const rule of this.rules) {
1755
+ try {
1756
+ const issues = rule.check(context);
1757
+ allIssues.push(...issues);
1758
+ } catch (_err) {
1759
+ }
1760
+ }
1761
+ return allIssues;
1762
+ }
1763
+ getRules() {
1764
+ return [...this.rules];
1765
+ }
1766
+ listRules() {
1767
+ return BUILTIN_RULES.map((r) => ({
1768
+ id: r.id,
1769
+ category: r.category,
1770
+ severity: r.severity,
1771
+ title: r.title
1772
+ }));
1773
+ }
1774
+ };
1775
+
1776
+ // src/core/scorer.ts
1777
+ var SEVERITY_PENALTY = {
1778
+ high: 15,
1779
+ medium: 8,
1780
+ low: 3,
1781
+ info: 0
1782
+ };
1783
+ function calculateDimensionScore(issues) {
1784
+ let score = 100;
1785
+ for (const issue of issues) {
1786
+ score -= SEVERITY_PENALTY[issue.severity] ?? 0;
1787
+ }
1788
+ return {
1789
+ score: Math.max(0, Math.min(100, score)),
1790
+ issues
1791
+ };
1792
+ }
1793
+ function calculateOverallScore(dimensions, weights) {
1794
+ const score = dimensions.security.score * weights.security + dimensions.logic.score * weights.logic + dimensions.structure.score * weights.structure + dimensions.style.score * weights.style + dimensions.coverage.score * weights.coverage;
1795
+ return Math.round(Math.max(0, Math.min(100, score)));
1796
+ }
1797
+ function getGrade(score) {
1798
+ if (score >= 90) return "HIGH_TRUST";
1799
+ if (score >= 70) return "REVIEW";
1800
+ if (score >= 50) return "LOW_TRUST";
1801
+ return "UNTRUSTED";
1802
+ }
1803
+ function getGradeEmoji(grade) {
1804
+ switch (grade) {
1805
+ case "HIGH_TRUST":
1806
+ return "\u2705";
1807
+ case "REVIEW":
1808
+ return "\u26A0\uFE0F";
1809
+ case "LOW_TRUST":
1810
+ return "\u26A0\uFE0F";
1811
+ case "UNTRUSTED":
1812
+ return "\u274C";
1813
+ }
1814
+ }
1815
+ function getGradeLabel(grade) {
1816
+ const isZh = isZhLocale();
1817
+ if (isZh) {
1818
+ switch (grade) {
1819
+ case "HIGH_TRUST":
1820
+ return "\u9AD8\u4FE1\u4EFB \u2014 \u53EF\u5B89\u5168\u5408\u5E76";
1821
+ case "REVIEW":
1822
+ return "\u5EFA\u8BAE\u5BA1\u67E5";
1823
+ case "LOW_TRUST":
1824
+ return "\u4F4E\u4FE1\u4EFB \u2014 \u9700\u4ED4\u7EC6\u5BA1\u67E5";
1825
+ case "UNTRUSTED":
1826
+ return "\u4E0D\u53EF\u4FE1 \u2014 \u4E0D\u5E94\u5408\u5E76";
1827
+ }
1828
+ }
1829
+ switch (grade) {
1830
+ case "HIGH_TRUST":
1831
+ return "HIGH TRUST \u2014 Safe to merge";
1832
+ case "REVIEW":
1833
+ return "REVIEW RECOMMENDED";
1834
+ case "LOW_TRUST":
1835
+ return "LOW TRUST \u2014 Careful review needed";
1836
+ case "UNTRUSTED":
1837
+ return "UNTRUSTED \u2014 Do not merge without changes";
1838
+ }
1839
+ }
1840
+
1841
+ // src/analyzers/structure.ts
1842
+ var DEFAULT_THRESHOLDS = {
1843
+ maxCyclomaticComplexity: 10,
1844
+ maxCognitiveComplexity: 20,
1845
+ maxFunctionLength: 40,
1846
+ maxNestingDepth: 4,
1847
+ maxParamCount: 5
1848
+ };
1849
+ function analyzeStructure(code, filePath, thresholds = {}) {
1850
+ const t_ = { ...DEFAULT_THRESHOLDS, ...thresholds };
1851
+ const issues = [];
1852
+ let parsed;
1853
+ try {
1854
+ parsed = parseCode(code, filePath);
1855
+ } catch {
1856
+ return { functions: [], issues: [] };
1857
+ }
1858
+ const functions = extractFunctions(parsed);
1859
+ for (const fn of functions) {
1860
+ if (fn.cyclomaticComplexity > t_.maxCyclomaticComplexity) {
1861
+ issues.push({
1862
+ ruleId: "structure/high-cyclomatic-complexity",
1863
+ severity: fn.cyclomaticComplexity > t_.maxCyclomaticComplexity * 2 ? "high" : "medium",
1864
+ category: "structure",
1865
+ file: filePath,
1866
+ startLine: fn.startLine,
1867
+ endLine: fn.endLine,
1868
+ message: t(
1869
+ `Function "${fn.name}" has cyclomatic complexity of ${fn.cyclomaticComplexity} (threshold: ${t_.maxCyclomaticComplexity}).`,
1870
+ `\u51FD\u6570 "${fn.name}" \u7684\u5708\u590D\u6742\u5EA6\u4E3A ${fn.cyclomaticComplexity}\uFF08\u9608\u503C\uFF1A${t_.maxCyclomaticComplexity}\uFF09\u3002`
1871
+ ),
1872
+ suggestion: t(
1873
+ "Break the function into smaller, simpler functions.",
1874
+ "\u5C06\u51FD\u6570\u62C6\u5206\u4E3A\u66F4\u5C0F\u3001\u66F4\u7B80\u5355\u7684\u51FD\u6570\u3002"
1875
+ )
1876
+ });
1877
+ }
1878
+ if (fn.cognitiveComplexity > t_.maxCognitiveComplexity) {
1879
+ issues.push({
1880
+ ruleId: "structure/high-cognitive-complexity",
1881
+ severity: fn.cognitiveComplexity > t_.maxCognitiveComplexity * 2 ? "high" : "medium",
1882
+ category: "structure",
1883
+ file: filePath,
1884
+ startLine: fn.startLine,
1885
+ endLine: fn.endLine,
1886
+ message: t(
1887
+ `Function "${fn.name}" has cognitive complexity of ${fn.cognitiveComplexity} (threshold: ${t_.maxCognitiveComplexity}).`,
1888
+ `\u51FD\u6570 "${fn.name}" \u7684\u8BA4\u77E5\u590D\u6742\u5EA6\u4E3A ${fn.cognitiveComplexity}\uFF08\u9608\u503C\uFF1A${t_.maxCognitiveComplexity}\uFF09\u3002`
1889
+ ),
1890
+ suggestion: t(
1891
+ "Simplify the function by reducing nesting and breaking out helper functions.",
1892
+ "\u901A\u8FC7\u51CF\u5C11\u5D4C\u5957\u548C\u63D0\u53D6\u8F85\u52A9\u51FD\u6570\u6765\u7B80\u5316\u8BE5\u51FD\u6570\u3002"
1893
+ )
1894
+ });
1895
+ }
1896
+ if (fn.lineCount > t_.maxFunctionLength) {
1897
+ issues.push({
1898
+ ruleId: "structure/long-function",
1899
+ severity: fn.lineCount > t_.maxFunctionLength * 2 ? "high" : "medium",
1900
+ category: "structure",
1901
+ file: filePath,
1902
+ startLine: fn.startLine,
1903
+ endLine: fn.endLine,
1904
+ message: t(
1905
+ `Function "${fn.name}" is ${fn.lineCount} lines long (threshold: ${t_.maxFunctionLength}).`,
1906
+ `\u51FD\u6570 "${fn.name}" \u957F\u8FBE ${fn.lineCount} \u884C\uFF08\u9608\u503C\uFF1A${t_.maxFunctionLength}\uFF09\u3002`
1907
+ ),
1908
+ suggestion: t(
1909
+ "Break the function into smaller units with clear responsibilities.",
1910
+ "\u5C06\u51FD\u6570\u62C6\u5206\u4E3A\u804C\u8D23\u6E05\u6670\u7684\u66F4\u5C0F\u5355\u5143\u3002"
1911
+ )
1912
+ });
1913
+ }
1914
+ if (fn.maxNestingDepth > t_.maxNestingDepth) {
1915
+ issues.push({
1916
+ ruleId: "structure/deep-nesting",
1917
+ severity: fn.maxNestingDepth > t_.maxNestingDepth + 2 ? "high" : "medium",
1918
+ category: "structure",
1919
+ file: filePath,
1920
+ startLine: fn.startLine,
1921
+ endLine: fn.endLine,
1922
+ message: t(
1923
+ `Function "${fn.name}" has nesting depth of ${fn.maxNestingDepth} (threshold: ${t_.maxNestingDepth}).`,
1924
+ `\u51FD\u6570 "${fn.name}" \u7684\u5D4C\u5957\u6DF1\u5EA6\u4E3A ${fn.maxNestingDepth}\uFF08\u9608\u503C\uFF1A${t_.maxNestingDepth}\uFF09\u3002`
1925
+ ),
1926
+ suggestion: t(
1927
+ "Use early returns, guard clauses, or extract nested logic into separate functions.",
1928
+ "\u4F7F\u7528\u63D0\u524D\u8FD4\u56DE\u3001\u5B88\u536B\u8BED\u53E5\uFF0C\u6216\u5C06\u5D4C\u5957\u903B\u8F91\u63D0\u53D6\u5230\u5355\u72EC\u7684\u51FD\u6570\u4E2D\u3002"
1929
+ )
1930
+ });
1931
+ }
1932
+ if (fn.paramCount > t_.maxParamCount) {
1933
+ issues.push({
1934
+ ruleId: "structure/too-many-params",
1935
+ severity: "low",
1936
+ category: "structure",
1937
+ file: filePath,
1938
+ startLine: fn.startLine,
1939
+ endLine: fn.endLine,
1940
+ message: t(
1941
+ `Function "${fn.name}" has ${fn.paramCount} parameters (threshold: ${t_.maxParamCount}).`,
1942
+ `\u51FD\u6570 "${fn.name}" \u6709 ${fn.paramCount} \u4E2A\u53C2\u6570\uFF08\u9608\u503C\uFF1A${t_.maxParamCount}\uFF09\u3002`
1943
+ ),
1944
+ suggestion: t(
1945
+ "Consider using an options object to group related parameters.",
1946
+ "\u8003\u8651\u4F7F\u7528\u9009\u9879\u5BF9\u8C61\u6765\u7EC4\u5408\u76F8\u5173\u53C2\u6570\u3002"
1947
+ )
1948
+ });
1949
+ }
1950
+ }
1951
+ return { functions, issues };
1952
+ }
1953
+
1954
+ // src/analyzers/style.ts
1955
+ function analyzeStyle(code, filePath) {
1956
+ const issues = [];
1957
+ const lines = code.split("\n");
1958
+ let commentLines = 0;
1959
+ let codeLines = 0;
1960
+ let inBlockComment = false;
1961
+ for (const line of lines) {
1962
+ const trimmed = line.trim();
1963
+ if (trimmed.length === 0) continue;
1964
+ if (inBlockComment) {
1965
+ commentLines++;
1966
+ if (trimmed.includes("*/")) inBlockComment = false;
1967
+ continue;
1968
+ }
1969
+ if (trimmed.startsWith("/*")) {
1970
+ commentLines++;
1971
+ if (!trimmed.includes("*/")) inBlockComment = true;
1972
+ continue;
1973
+ }
1974
+ if (trimmed.startsWith("//")) {
1975
+ commentLines++;
1976
+ continue;
1977
+ }
1978
+ codeLines++;
1979
+ }
1980
+ const commentDensity = codeLines > 0 ? commentLines / codeLines : 0;
1981
+ let camelCase = 0;
1982
+ let snakeCase = 0;
1983
+ let pascalCase = 0;
1984
+ let totalIdents = 0;
1985
+ try {
1986
+ const parsed = parseCode(code, filePath);
1987
+ collectNamingStyles(parsed.ast, (style) => {
1988
+ totalIdents++;
1989
+ if (style === "camel") camelCase++;
1990
+ else if (style === "snake") snakeCase++;
1991
+ else if (style === "pascal") pascalCase++;
1992
+ });
1993
+ } catch {
1994
+ }
1995
+ const styles = [
1996
+ { name: "camelCase", count: camelCase },
1997
+ { name: "snake_case", count: snakeCase }
1998
+ ].filter((s) => s.count > 0);
1999
+ if (styles.length > 1 && totalIdents >= 5) {
2000
+ const dominant = styles.reduce((a, b) => a.count > b.count ? a : b);
2001
+ const minority = styles.reduce((a, b) => a.count < b.count ? a : b);
2002
+ const ratio = minority.count / totalIdents;
2003
+ if (ratio > 0.15) {
2004
+ issues.push({
2005
+ ruleId: "style/inconsistent-naming",
2006
+ severity: "low",
2007
+ category: "style",
2008
+ file: filePath,
2009
+ startLine: 1,
2010
+ endLine: lines.length,
2011
+ message: t(
2012
+ `Mixed naming styles: ${dominant.count} ${dominant.name} vs ${minority.count} ${minority.name} identifiers.`,
2013
+ `\u547D\u540D\u98CE\u683C\u6DF7\u7528\uFF1A${dominant.count} \u4E2A ${dominant.name} \u4E0E ${minority.count} \u4E2A ${minority.name} \u6807\u8BC6\u7B26\u3002`
2014
+ ),
2015
+ suggestion: t(
2016
+ `Standardize on ${dominant.name} for consistency.`,
2017
+ `\u7EDF\u4E00\u4F7F\u7528 ${dominant.name} \u4EE5\u4FDD\u6301\u4E00\u81F4\u6027\u3002`
2018
+ )
2019
+ });
2020
+ }
2021
+ }
2022
+ return {
2023
+ issues,
2024
+ stats: {
2025
+ camelCaseCount: camelCase,
2026
+ snakeCaseCount: snakeCase,
2027
+ pascalCaseCount: pascalCase,
2028
+ totalIdentifiers: totalIdents,
2029
+ commentLineCount: commentLines,
2030
+ codeLineCount: codeLines,
2031
+ commentDensity
2032
+ }
2033
+ };
2034
+ }
2035
+ function detectNamingStyle(name) {
2036
+ if (name.length <= 1) return "other";
2037
+ if (name.startsWith("_") || name === name.toUpperCase()) return "other";
2038
+ if (name.includes("_")) return "snake";
2039
+ if (/^[A-Z]/.test(name)) return "pascal";
2040
+ if (/^[a-z]/.test(name)) return "camel";
2041
+ return "other";
2042
+ }
2043
+ function collectNamingStyles(root, callback) {
2044
+ walkAST(root, (node) => {
2045
+ if (node.type === AST_NODE_TYPES.VariableDeclarator && node.id.type === AST_NODE_TYPES.Identifier) {
2046
+ const style = detectNamingStyle(node.id.name);
2047
+ if (style !== "other") callback(style);
2048
+ }
2049
+ if (node.type === AST_NODE_TYPES.FunctionDeclaration && node.id) {
2050
+ const style = detectNamingStyle(node.id.name);
2051
+ if (style !== "other") callback(style);
2052
+ }
2053
+ });
2054
+ }
2055
+
2056
+ // src/analyzers/coverage.ts
2057
+ import * as fs from "fs";
2058
+ import * as path from "path";
2059
+ function analyzeCoverage(code, filePath) {
2060
+ const issues = [];
2061
+ const exportedFunctions = [];
2062
+ const testFile = findTestFile(filePath);
2063
+ const hasTestFile = testFile !== null;
2064
+ try {
2065
+ const parsed = parseCode(code, filePath);
2066
+ const functions = extractFunctions(parsed);
2067
+ for (const fn of functions) {
2068
+ if (fn.name !== "<anonymous>" && fn.name !== "<arrow>") {
2069
+ exportedFunctions.push(fn.name);
2070
+ }
2071
+ }
2072
+ if (exportedFunctions.length >= 2 && !hasTestFile) {
2073
+ const basename2 = path.basename(filePath);
2074
+ if (!basename2.includes(".test.") && !basename2.includes(".spec.") && !basename2.startsWith("test")) {
2075
+ issues.push({
2076
+ ruleId: "coverage/missing-test-file",
2077
+ severity: "low",
2078
+ category: "coverage",
2079
+ file: filePath,
2080
+ startLine: 1,
2081
+ endLine: 1,
2082
+ message: t(
2083
+ `File has ${exportedFunctions.length} functions but no corresponding test file found.`,
2084
+ `\u6587\u4EF6\u6709 ${exportedFunctions.length} \u4E2A\u51FD\u6570\uFF0C\u4F46\u672A\u627E\u5230\u5BF9\u5E94\u7684\u6D4B\u8BD5\u6587\u4EF6\u3002`
2085
+ ),
2086
+ suggestion: t(
2087
+ `Create a test file (e.g., ${suggestTestFileName(filePath)}).`,
2088
+ `\u521B\u5EFA\u6D4B\u8BD5\u6587\u4EF6\uFF08\u5982\uFF1A${suggestTestFileName(filePath)}\uFF09\u3002`
2089
+ )
2090
+ });
2091
+ }
2092
+ }
2093
+ } catch {
2094
+ }
2095
+ return { issues, exportedFunctions, hasTestFile };
2096
+ }
2097
+ function findTestFile(filePath) {
2098
+ const dir = path.dirname(filePath);
2099
+ const ext = path.extname(filePath);
2100
+ const base = path.basename(filePath, ext);
2101
+ const candidates = [
2102
+ // 同目录
2103
+ path.join(dir, `${base}.test${ext}`),
2104
+ path.join(dir, `${base}.spec${ext}`),
2105
+ // tests/ 目录(复数)
2106
+ path.join(dir, "..", "tests", `${base}.test${ext}`),
2107
+ path.join(dir, "..", "tests", `${base}.spec${ext}`),
2108
+ path.join(dir, "..", "..", "tests", `${base}.test${ext}`),
2109
+ path.join(dir, "..", "..", "tests", `${base}.spec${ext}`),
2110
+ // test/ 目录(单数)
2111
+ path.join(dir, "..", "test", `${base}.test${ext}`),
2112
+ path.join(dir, "..", "test", `${base}.spec${ext}`),
2113
+ // __tests__/ 目录
2114
+ path.join(dir, "__tests__", `${base}.test${ext}`),
2115
+ path.join(dir, "__tests__", `${base}.spec${ext}`)
2116
+ ];
2117
+ for (const candidate of candidates) {
2118
+ if (fs.existsSync(candidate)) {
2119
+ return candidate;
2120
+ }
2121
+ }
2122
+ return null;
2123
+ }
2124
+ function suggestTestFileName(filePath) {
2125
+ const ext = path.extname(filePath);
2126
+ const base = path.basename(filePath, ext);
2127
+ return `${base}.test${ext}`;
2128
+ }
2129
+
2130
+ // src/core/engine.ts
2131
+ var __filename = fileURLToPath(import.meta.url);
2132
+ var __dirname = dirname3(__filename);
2133
+ var PKG_VERSION = (() => {
2134
+ try {
2135
+ const pkg2 = JSON.parse(readFileSync(resolve2(__dirname, "../../package.json"), "utf-8"));
2136
+ return pkg2.version;
2137
+ } catch {
2138
+ return "0.1.0";
2139
+ }
2140
+ })();
2141
+ var ScanEngine = class {
2142
+ config;
2143
+ diffParser;
2144
+ ruleEngine;
2145
+ constructor(config, workDir) {
2146
+ this.config = config;
2147
+ this.diffParser = new DiffParser(workDir);
2148
+ this.ruleEngine = new RuleEngine(config);
2149
+ }
2150
+ async scan(options) {
2151
+ const diffFiles = await this.getDiffFiles(options);
2152
+ const allIssues = [];
2153
+ for (const diffFile of diffFiles) {
2154
+ if (diffFile.status === "deleted") continue;
2155
+ const filePath = resolve2(diffFile.filePath);
2156
+ let fileContent;
2157
+ try {
2158
+ if (existsSync3(filePath)) {
2159
+ fileContent = await readFile(filePath, "utf-8");
2160
+ } else {
2161
+ const content = await this.diffParser.getFileContent(diffFile.filePath);
2162
+ if (!content) continue;
2163
+ fileContent = content;
2164
+ }
2165
+ } catch {
2166
+ continue;
2167
+ }
2168
+ const addedLines = diffFile.hunks.flatMap((hunk) => {
2169
+ const lines = hunk.content.split("\n");
2170
+ const result = [];
2171
+ let currentLine = hunk.newStart;
2172
+ for (const line of lines) {
2173
+ if (line.startsWith("+")) {
2174
+ result.push({ lineNumber: currentLine, content: line.slice(1) });
2175
+ currentLine++;
2176
+ } else if (line.startsWith("-")) {
2177
+ } else {
2178
+ currentLine++;
2179
+ }
2180
+ }
2181
+ return result;
2182
+ });
2183
+ const issues = this.ruleEngine.run({
2184
+ filePath: diffFile.filePath,
2185
+ fileContent,
2186
+ addedLines
2187
+ });
2188
+ allIssues.push(...issues);
2189
+ if (this.isTsJsFile(diffFile.filePath)) {
2190
+ const structureResult = analyzeStructure(fileContent, diffFile.filePath, {
2191
+ maxCyclomaticComplexity: this.config.thresholds["max-cyclomatic-complexity"],
2192
+ maxCognitiveComplexity: this.config.thresholds["max-cognitive-complexity"],
2193
+ maxFunctionLength: this.config.thresholds["max-function-length"],
2194
+ maxNestingDepth: this.config.thresholds["max-nesting-depth"],
2195
+ maxParamCount: this.config.thresholds["max-params"]
2196
+ });
2197
+ allIssues.push(...structureResult.issues);
2198
+ const styleResult = analyzeStyle(fileContent, diffFile.filePath);
2199
+ allIssues.push(...styleResult.issues);
2200
+ const coverageResult = analyzeCoverage(fileContent, diffFile.filePath);
2201
+ allIssues.push(...coverageResult.issues);
2202
+ }
2203
+ }
2204
+ const dimensions = this.groupByDimension(allIssues);
2205
+ const overallScore = calculateOverallScore(dimensions, this.config.weights);
2206
+ const grade = getGrade(overallScore);
2207
+ const commitHash = await this.diffParser.getCurrentCommitHash();
2208
+ return {
2209
+ version: PKG_VERSION,
2210
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2211
+ commit: commitHash,
2212
+ overall: {
2213
+ score: overallScore,
2214
+ grade,
2215
+ filesScanned: diffFiles.filter((f) => f.status !== "deleted").length,
2216
+ issuesFound: allIssues.length
2217
+ },
2218
+ dimensions,
2219
+ issues: allIssues.sort((a, b) => {
2220
+ const severityOrder = { high: 0, medium: 1, low: 2, info: 3 };
2221
+ return severityOrder[a.severity] - severityOrder[b.severity];
2222
+ })
2223
+ };
2224
+ }
2225
+ async getDiffFiles(options) {
2226
+ if (options.staged) {
2227
+ return this.diffParser.getStagedFiles();
2228
+ }
2229
+ if (options.diff) {
2230
+ return this.diffParser.getDiffFromRef(options.diff);
2231
+ }
2232
+ if (options.files && options.files.length > 0) {
2233
+ return Promise.all(
2234
+ options.files.map(async (filePath) => {
2235
+ let content = "";
2236
+ try {
2237
+ content = await readFile(resolve2(filePath), "utf-8");
2238
+ } catch {
2239
+ }
2240
+ return {
2241
+ filePath,
2242
+ status: "modified",
2243
+ additions: content.split("\n").length,
2244
+ deletions: 0,
2245
+ content,
2246
+ hunks: [
2247
+ {
2248
+ oldStart: 1,
2249
+ oldLines: 0,
2250
+ newStart: 1,
2251
+ newLines: content.split("\n").length,
2252
+ content: content.split("\n").map((l) => "+" + l).join("\n")
2253
+ }
2254
+ ]
2255
+ };
2256
+ })
2257
+ );
2258
+ }
2259
+ return this.diffParser.getChangedFiles();
2260
+ }
2261
+ isTsJsFile(filePath) {
2262
+ return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
2263
+ }
2264
+ groupByDimension(issues) {
2265
+ const categories = [
2266
+ "security",
2267
+ "logic",
2268
+ "structure",
2269
+ "style",
2270
+ "coverage"
2271
+ ];
2272
+ const grouped = {};
2273
+ for (const cat of categories) {
2274
+ const catIssues = issues.filter((i) => i.category === cat);
2275
+ grouped[cat] = calculateDimensionScore(catIssues);
2276
+ }
2277
+ return grouped;
2278
+ }
2279
+ };
2280
+
2281
+ // src/cli/output/terminal.ts
2282
+ import pc from "picocolors";
2283
+ import Table from "cli-table3";
2284
+ var en = {
2285
+ reportTitle: "\u{1F4CA} CodeTrust Report",
2286
+ overallScore: "Overall Trust Score",
2287
+ dimension: "Dimension",
2288
+ score: "Score",
2289
+ details: "Details",
2290
+ noIssues: "No issues",
2291
+ issuesFound: "{{count}} issue(s) found",
2292
+ issuesHeader: "Issues ({{count}}):",
2293
+ noIssuesFound: "No issues found! \u{1F389}",
2294
+ scanned: "Scanned {{count}} file(s)"
2295
+ };
2296
+ var zh = {
2297
+ reportTitle: "\u{1F4CA} CodeTrust \u62A5\u544A",
2298
+ overallScore: "\u603B\u4F53\u4FE1\u4EFB\u8BC4\u5206",
2299
+ dimension: "\u7EF4\u5EA6",
2300
+ score: "\u8BC4\u5206",
2301
+ details: "\u8BE6\u60C5",
2302
+ noIssues: "\u65E0\u95EE\u9898",
2303
+ issuesFound: "\u53D1\u73B0 {{count}} \u4E2A\u95EE\u9898",
2304
+ issuesHeader: "\u95EE\u9898\u5217\u8868 ({{count}}):",
2305
+ noIssuesFound: "\u672A\u53D1\u73B0\u95EE\u9898! \u{1F389}",
2306
+ scanned: "\u626B\u63CF\u4E86 {{count}} \u4E2A\u6587\u4EF6"
2307
+ };
2308
+ function renderTerminalReport(report) {
2309
+ const isZh = isZhLocale();
2310
+ const t2 = isZh ? zh : en;
2311
+ const lines = [];
2312
+ const commitLabel = report.commit ? ` \u2014 commit ${report.commit}` : "";
2313
+ lines.push("");
2314
+ lines.push(pc.bold(`${t2.reportTitle}${commitLabel}`));
2315
+ lines.push(pc.dim("\u2550".repeat(50)));
2316
+ lines.push("");
2317
+ const emoji = getGradeEmoji(report.overall.grade);
2318
+ const label = getGradeLabel(report.overall.grade);
2319
+ const scoreColor = getScoreColor(report.overall.score);
2320
+ lines.push(
2321
+ `${t2.overallScore}: ${scoreColor(pc.bold(String(report.overall.score) + "/100"))} ${emoji} ${pc.bold(label)}`
2322
+ );
2323
+ lines.push("");
2324
+ const table = new Table({
2325
+ head: [
2326
+ pc.bold(t2.dimension),
2327
+ pc.bold(t2.score),
2328
+ pc.bold(t2.details)
2329
+ ],
2330
+ style: { head: [], border: [] },
2331
+ colWidths: [16, 8, 40]
2332
+ });
2333
+ const dimLabels = isZh ? {
2334
+ security: "\u5B89\u5168",
2335
+ logic: "\u903B\u8F91",
2336
+ structure: "\u7ED3\u6784",
2337
+ style: "\u98CE\u683C",
2338
+ coverage: "\u8986\u76D6"
2339
+ } : {
2340
+ security: "Security",
2341
+ logic: "Logic",
2342
+ structure: "Structure",
2343
+ style: "Style",
2344
+ coverage: "Coverage"
2345
+ };
2346
+ const dims = ["security", "logic", "structure", "style", "coverage"];
2347
+ for (const dim of dims) {
2348
+ const d = report.dimensions[dim];
2349
+ const dimEmoji = d.score >= 80 ? "\u2705" : d.score >= 60 ? "\u26A0\uFE0F" : "\u274C";
2350
+ const color = getScoreColor(d.score);
2351
+ const issueCount = d.issues.length;
2352
+ const detail = issueCount === 0 ? pc.green(t2.noIssues) : t2.issuesFound.replace("{{count}}", String(issueCount));
2353
+ table.push([
2354
+ `${dimEmoji} ${dimLabels[dim]}`,
2355
+ color(String(d.score)),
2356
+ detail
2357
+ ]);
2358
+ }
2359
+ lines.push(table.toString());
2360
+ lines.push("");
2361
+ if (report.issues.length > 0) {
2362
+ lines.push(pc.bold(t2.issuesHeader.replace("{{count}}", String(report.issues.length))));
2363
+ lines.push("");
2364
+ for (const issue of report.issues) {
2365
+ lines.push(formatIssue(issue, isZh));
2366
+ }
2367
+ } else {
2368
+ lines.push(pc.green(pc.bold(t2.noIssuesFound)));
2369
+ }
2370
+ lines.push("");
2371
+ lines.push(
2372
+ pc.dim(`${t2.scanned.replace("{{count}}", String(report.overall.filesScanned))} \u2022 ${new Date(report.timestamp).toLocaleString()}`)
2373
+ );
2374
+ lines.push("");
2375
+ return lines.join("\n");
2376
+ }
2377
+ function formatIssue(issue, isZh) {
2378
+ const severityLabel = formatSeverity(issue.severity, isZh);
2379
+ const location = pc.dim(`${issue.file}:${issue.startLine}-${issue.endLine}`);
2380
+ const message = issue.message;
2381
+ const suggestion = issue.suggestion ? `
2382
+ ${pc.dim("\u{1F4A1} " + issue.suggestion)}` : "";
2383
+ return ` ${severityLabel} ${location}
2384
+ ${message}${suggestion}
2385
+ `;
2386
+ }
2387
+ function formatSeverity(severity, isZh) {
2388
+ if (isZh) {
2389
+ switch (severity) {
2390
+ case "high":
2391
+ return pc.red(pc.bold("\u274C \u9AD8 "));
2392
+ case "medium":
2393
+ return pc.yellow(pc.bold("\u26A0\uFE0F \u4E2D"));
2394
+ case "low":
2395
+ return pc.cyan(pc.bold("\u2139\uFE0F \u4F4E "));
2396
+ case "info":
2397
+ return pc.dim("\u{1F4DD} \u63D0\u793A ");
2398
+ default:
2399
+ return severity;
2400
+ }
2401
+ }
2402
+ switch (severity) {
2403
+ case "high":
2404
+ return pc.red(pc.bold("\u274C HIGH "));
2405
+ case "medium":
2406
+ return pc.yellow(pc.bold("\u26A0\uFE0F MEDIUM"));
2407
+ case "low":
2408
+ return pc.cyan(pc.bold("\u2139\uFE0F LOW "));
2409
+ case "info":
2410
+ return pc.dim("\u{1F4DD} INFO ");
2411
+ default:
2412
+ return severity;
2413
+ }
2414
+ }
2415
+ function getScoreColor(score) {
2416
+ if (score >= 90) return pc.green;
2417
+ if (score >= 70) return pc.yellow;
2418
+ if (score >= 50) return pc.magenta;
2419
+ return pc.red;
2420
+ }
2421
+
2422
+ // src/cli/output/json.ts
2423
+ function renderJsonReport(report) {
2424
+ return JSON.stringify(report, null, 2);
2425
+ }
2426
+
2427
+ // src/cli/commands/scan.ts
2428
+ function createScanCommand() {
2429
+ const cmd = new Command("scan").description("Scan code changes for trust issues").argument("[files...]", "Specific files to scan").option("--staged", "Scan only git staged files").option("--diff <ref>", "Scan diff against a git ref (e.g. HEAD~1, origin/main)").option("--format <format>", "Output format: terminal, json", "terminal").option("--min-score <score>", "Minimum trust score threshold", "0").action(async (files, opts) => {
2430
+ try {
2431
+ const config = await loadConfig();
2432
+ const engine = new ScanEngine(config);
2433
+ const scanOptions = {
2434
+ staged: opts.staged,
2435
+ diff: opts.diff,
2436
+ files: files.length > 0 ? files : void 0,
2437
+ format: opts.format,
2438
+ minScore: parseInt(opts.minScore, 10)
2439
+ };
2440
+ const report = await engine.scan(scanOptions);
2441
+ if (opts.format === "json") {
2442
+ console.log(renderJsonReport(report));
2443
+ } else {
2444
+ console.log(renderTerminalReport(report));
2445
+ }
2446
+ if (scanOptions.minScore && scanOptions.minScore > 0 && report.overall.score < scanOptions.minScore) {
2447
+ process.exit(1);
2448
+ }
2449
+ } catch (err) {
2450
+ if (err instanceof Error) {
2451
+ console.error(`Error: ${err.message}`);
2452
+ } else {
2453
+ console.error("An unexpected error occurred");
2454
+ }
2455
+ process.exit(1);
2456
+ }
2457
+ });
2458
+ return cmd;
2459
+ }
2460
+
2461
+ // src/cli/commands/report.ts
2462
+ import { Command as Command2 } from "commander";
2463
+ function createReportCommand() {
2464
+ const cmd = new Command2("report").description("Generate a trust report for recent changes").option("--json", "Output as JSON").option("--diff <ref>", "Diff against a git ref", "HEAD~1").action(async (opts) => {
2465
+ try {
2466
+ const config = await loadConfig();
2467
+ const engine = new ScanEngine(config);
2468
+ const report = await engine.scan({ diff: opts.diff });
2469
+ if (opts.json) {
2470
+ console.log(renderJsonReport(report));
2471
+ } else {
2472
+ console.log(renderTerminalReport(report));
2473
+ }
2474
+ } catch (err) {
2475
+ if (err instanceof Error) {
2476
+ console.error(`Error: ${err.message}`);
2477
+ } else {
2478
+ console.error("An unexpected error occurred");
2479
+ }
2480
+ process.exit(1);
2481
+ }
2482
+ });
2483
+ return cmd;
2484
+ }
2485
+
2486
+ // src/cli/commands/init.ts
2487
+ import { writeFile } from "fs/promises";
2488
+ import { existsSync as existsSync4 } from "fs";
2489
+ import { resolve as resolve3 } from "path";
2490
+ import { Command as Command3 } from "commander";
2491
+ import pc2 from "picocolors";
2492
+ function createInitCommand() {
2493
+ const cmd = new Command3("init").description("Initialize CodeTrust configuration file").action(async () => {
2494
+ const configPath = resolve3(".codetrust.yml");
2495
+ if (existsSync4(configPath)) {
2496
+ console.log(pc2.yellow("\u26A0\uFE0F .codetrust.yml already exists. Skipping."));
2497
+ return;
2498
+ }
2499
+ try {
2500
+ await writeFile(configPath, generateDefaultConfig(), "utf-8");
2501
+ console.log(pc2.green("\u2705 Created .codetrust.yml"));
2502
+ console.log(pc2.dim(" Edit this file to customize scan behavior."));
2503
+ } catch (err) {
2504
+ if (err instanceof Error) {
2505
+ console.error(pc2.red(`Error creating config: ${err.message}`));
2506
+ }
2507
+ process.exit(1);
2508
+ }
2509
+ });
2510
+ return cmd;
2511
+ }
2512
+
2513
+ // src/cli/commands/rules.ts
2514
+ import { Command as Command4 } from "commander";
2515
+ import pc3 from "picocolors";
2516
+ import Table2 from "cli-table3";
2517
+ function createRulesCommand() {
2518
+ const cmd = new Command4("rules").description("Manage analysis rules");
2519
+ cmd.command("list").description("List all available rules").action(async () => {
2520
+ const config = await loadConfig();
2521
+ const ruleEngine = new RuleEngine(config);
2522
+ const rules = ruleEngine.listRules();
2523
+ const table = new Table2({
2524
+ head: [
2525
+ pc3.bold("ID"),
2526
+ pc3.bold("Category"),
2527
+ pc3.bold("Severity"),
2528
+ pc3.bold("Title")
2529
+ ],
2530
+ style: { head: [], border: [] }
2531
+ });
2532
+ for (const rule of rules) {
2533
+ const isDisabled = config.rules.disabled.includes(rule.id);
2534
+ const id = isDisabled ? pc3.strikethrough(pc3.dim(rule.id)) : rule.id;
2535
+ const status = isDisabled ? pc3.dim(" (disabled)") : "";
2536
+ table.push([
2537
+ id + status,
2538
+ rule.category,
2539
+ formatSeverity2(rule.severity),
2540
+ rule.title
2541
+ ]);
2542
+ }
2543
+ console.log("");
2544
+ console.log(pc3.bold("\u{1F4CB} CodeTrust Rules"));
2545
+ console.log("");
2546
+ console.log(table.toString());
2547
+ console.log("");
2548
+ console.log(pc3.dim(`Total: ${rules.length} rules`));
2549
+ });
2550
+ return cmd;
2551
+ }
2552
+ function formatSeverity2(severity) {
2553
+ switch (severity) {
2554
+ case "high":
2555
+ return pc3.red("HIGH");
2556
+ case "medium":
2557
+ return pc3.yellow("MEDIUM");
2558
+ case "low":
2559
+ return pc3.cyan("LOW");
2560
+ case "info":
2561
+ return pc3.dim("INFO");
2562
+ default:
2563
+ return severity;
2564
+ }
2565
+ }
2566
+
2567
+ // src/cli/commands/hook.ts
2568
+ import { writeFile as writeFile2, chmod, mkdir } from "fs/promises";
2569
+ import { existsSync as existsSync5 } from "fs";
2570
+ import { resolve as resolve4, join as join2 } from "path";
2571
+ import { Command as Command5 } from "commander";
2572
+ import pc4 from "picocolors";
2573
+ var HOOK_CONTENT = `#!/bin/sh
2574
+ # CodeTrust pre-commit hook
2575
+ codetrust scan --staged --min-score 70 --format terminal
2576
+ if [ $? -ne 0 ]; then
2577
+ echo ""
2578
+ echo "CodeTrust: Trust score below threshold. Use --no-verify to bypass."
2579
+ exit 1
2580
+ fi
2581
+ `;
2582
+ function createHookCommand() {
2583
+ const cmd = new Command5("hook").description("Manage git hooks");
2584
+ cmd.command("install").description("Install pre-commit hook").action(async () => {
2585
+ const gitDir = resolve4(".git");
2586
+ if (!existsSync5(gitDir)) {
2587
+ console.error(pc4.red("Error: Not a git repository."));
2588
+ process.exit(1);
2589
+ }
2590
+ const hooksDir = join2(gitDir, "hooks");
2591
+ const hookPath = join2(hooksDir, "pre-commit");
2592
+ try {
2593
+ if (!existsSync5(hooksDir)) {
2594
+ await mkdir(hooksDir, { recursive: true });
2595
+ }
2596
+ if (existsSync5(hookPath)) {
2597
+ console.log(pc4.yellow("\u26A0\uFE0F pre-commit hook already exists. Skipping."));
2598
+ console.log(pc4.dim(" Remove .git/hooks/pre-commit to reinstall."));
2599
+ return;
2600
+ }
2601
+ await writeFile2(hookPath, HOOK_CONTENT, "utf-8");
2602
+ await chmod(hookPath, "755");
2603
+ console.log(pc4.green("\u2705 Installed pre-commit hook"));
2604
+ console.log(pc4.dim(" CodeTrust will run on every commit."));
2605
+ console.log(pc4.dim(" Use --no-verify to bypass."));
2606
+ } catch (err) {
2607
+ if (err instanceof Error) {
2608
+ console.error(pc4.red(`Error installing hook: ${err.message}`));
2609
+ }
2610
+ process.exit(1);
2611
+ }
2612
+ });
2613
+ return cmd;
2614
+ }
2615
+
2616
+ // src/cli/index.ts
2617
+ import { readFileSync as readFileSync2 } from "fs";
2618
+ import { fileURLToPath as fileURLToPath2 } from "url";
2619
+ import { dirname as dirname4, resolve as resolve5 } from "path";
2620
+ var __filename2 = fileURLToPath2(import.meta.url);
2621
+ var __dirname2 = dirname4(__filename2);
2622
+ var pkg = JSON.parse(readFileSync2(resolve5(__dirname2, "../../package.json"), "utf-8"));
2623
+ var program = new Command6();
2624
+ program.name("codetrust").description("AI code trust verification tool \u2014 verify AI-generated code with deterministic algorithms").version(pkg.version);
2625
+ program.addCommand(createScanCommand());
2626
+ program.addCommand(createReportCommand());
2627
+ program.addCommand(createInitCommand());
2628
+ program.addCommand(createRulesCommand());
2629
+ program.addCommand(createHookCommand());
2630
+ program.parse();
2631
+ //# sourceMappingURL=index.js.map