@vibecodeqa/cli 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -63
- package/dist/check-meta.d.ts +1 -0
- package/dist/check-meta.js +34 -2
- package/dist/cli.js +35 -10
- package/dist/detect.js +24 -2
- package/dist/fs-utils.d.ts +4 -0
- package/dist/fs-utils.js +12 -6
- package/dist/report/html.d.ts +17 -10
- package/dist/report/html.js +106 -73
- package/dist/report/pages.d.ts +2 -1
- package/dist/report/pages.js +88 -82
- package/dist/report/sarif.d.ts +3 -0
- package/dist/report/sarif.js +67 -0
- package/dist/report/styles.d.ts +1 -1
- package/dist/report/styles.js +82 -36
- package/dist/runners/architecture.d.ts +2 -0
- package/dist/runners/architecture.js +232 -20
- package/dist/runners/code-coherence.d.ts +17 -0
- package/dist/runners/code-coherence.js +39 -0
- package/dist/runners/complexity.js +7 -37
- package/dist/runners/confusion.js +3 -31
- package/dist/runners/context.js +9 -40
- package/dist/runners/dependencies.js +28 -0
- package/dist/runners/doc-coherence.d.ts +14 -0
- package/dist/runners/doc-coherence.js +48 -0
- package/dist/runners/docs.js +7 -32
- package/dist/runners/duplication.js +9 -37
- package/dist/runners/lint.js +17 -0
- package/dist/runners/performance.d.ts +10 -0
- package/dist/runners/performance.js +174 -0
- package/dist/runners/react.js +15 -10
- package/dist/runners/secrets.js +8 -29
- package/dist/runners/security.js +15 -38
- package/dist/runners/standards.js +3 -36
- package/dist/runners/structure.js +35 -55
- package/dist/runners/testing.js +2 -36
- package/dist/runners/type-safety.d.ts +1 -1
- package/dist/runners/type-safety.js +19 -37
- package/dist/runners/types-check.d.ts +1 -1
- package/dist/runners/types-check.js +38 -20
- package/dist/types.d.ts +5 -5
- package/package.json +11 -10
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Code Coherence — detects internal contradictions and inconsistencies in the codebase.
|
|
2
|
+
*
|
|
3
|
+
* Premium feature (powered by LLM). Analyzes the codebase for patterns where
|
|
4
|
+
* different parts of the code contradict each other:
|
|
5
|
+
* - Function A validates input X, but function B that calls A re-validates X differently
|
|
6
|
+
* - Type says field is required, but all usages treat it as optional
|
|
7
|
+
* - Error handling is strict in module A but permissive in module B for the same operations
|
|
8
|
+
* - Naming conventions differ across modules (camelCase vs snake_case for same concepts)
|
|
9
|
+
* - Two implementations of the same algorithm with different behavior
|
|
10
|
+
* - Config declares a feature flag but no code reads it
|
|
11
|
+
* - Dead branches: conditions that can never be true given the types
|
|
12
|
+
* - Contradictory defaults (module A defaults timeout to 5s, module B to 30s)
|
|
13
|
+
*
|
|
14
|
+
* Currently returns a "coming soon" placeholder.
|
|
15
|
+
*/
|
|
16
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
17
|
+
export function runCodeCoherence(cwd) {
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
// Gather basic stats even in placeholder mode
|
|
20
|
+
const files = getProductionFiles(cwd);
|
|
21
|
+
const totalExports = files.reduce((s, f) => s + (f.content.match(/\bexport\s+/g) || []).length, 0);
|
|
22
|
+
const totalFunctions = files.reduce((s, f) => s + (f.content.match(/\bfunction\s+\w+/g) || []).length, 0);
|
|
23
|
+
return {
|
|
24
|
+
name: "code-coherence",
|
|
25
|
+
score: 0,
|
|
26
|
+
grade: "F",
|
|
27
|
+
details: {
|
|
28
|
+
premium: true,
|
|
29
|
+
comingSoon: true,
|
|
30
|
+
reason: "LLM-powered analysis — coming soon",
|
|
31
|
+
filesAnalyzed: files.length,
|
|
32
|
+
totalExports,
|
|
33
|
+
totalFunctions,
|
|
34
|
+
description: "Detects internal contradictions: inconsistent validation, conflicting defaults, naming drift, dead config flags, and behavioral mismatches across modules.",
|
|
35
|
+
},
|
|
36
|
+
issues: [],
|
|
37
|
+
duration: Date.now() - start,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Complexity analysis — counts lines, functions, and cognitive complexity via AST-free heuristics. */
|
|
2
|
-
import {
|
|
3
|
-
import { extname, join } from "node:path";
|
|
2
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
4
3
|
import { gradeFromScore } from "../types.js";
|
|
5
4
|
const MAX_FUNCTION_LINES = 60;
|
|
6
5
|
const MAX_COMPLEXITY = 15;
|
|
@@ -8,27 +7,15 @@ export function runComplexity(cwd) {
|
|
|
8
7
|
const start = Date.now();
|
|
9
8
|
const issues = [];
|
|
10
9
|
const functions = [];
|
|
11
|
-
|
|
12
|
-
const srcDirs = ["src", "web/src"];
|
|
13
|
-
const files = [];
|
|
14
|
-
for (const dir of srcDirs) {
|
|
15
|
-
try {
|
|
16
|
-
collectFiles(join(cwd, dir), files);
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
/* dir doesn't exist */
|
|
20
|
-
}
|
|
21
|
-
}
|
|
10
|
+
const sourceFiles = getProductionFiles(cwd);
|
|
22
11
|
let totalLines = 0;
|
|
23
|
-
const totalFiles =
|
|
12
|
+
const totalFiles = sourceFiles.length;
|
|
24
13
|
let longFunctions = 0;
|
|
25
14
|
let complexFunctions = 0;
|
|
26
|
-
for (const
|
|
27
|
-
|
|
28
|
-
const lines = content.split("\n");
|
|
29
|
-
totalLines += lines.length;
|
|
15
|
+
for (const sf of sourceFiles) {
|
|
16
|
+
totalLines += sf.lines;
|
|
30
17
|
// Simple heuristic: find function boundaries and measure complexity
|
|
31
|
-
const funcs = extractFunctions(content,
|
|
18
|
+
const funcs = extractFunctions(sf.content, sf.path);
|
|
32
19
|
for (const f of funcs) {
|
|
33
20
|
functions.push(f);
|
|
34
21
|
if (f.lines > MAX_FUNCTION_LINES) {
|
|
@@ -68,23 +55,6 @@ export function runComplexity(cwd) {
|
|
|
68
55
|
duration: Date.now() - start,
|
|
69
56
|
};
|
|
70
57
|
}
|
|
71
|
-
function collectFiles(dir, out) {
|
|
72
|
-
for (const entry of readdirSync(dir)) {
|
|
73
|
-
if (entry === "node_modules" || entry === "dist" || entry === ".git")
|
|
74
|
-
continue;
|
|
75
|
-
const full = join(dir, entry);
|
|
76
|
-
const stat = statSync(full);
|
|
77
|
-
if (stat.isDirectory()) {
|
|
78
|
-
collectFiles(full, out);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
const ext = extname(entry);
|
|
82
|
-
if ((ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") && !entry.includes(".test.") && !entry.includes(".spec.")) {
|
|
83
|
-
out.push(full);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
58
|
/** Simple heuristic function extraction — not a full AST parser but good enough for metrics. */
|
|
89
59
|
function extractFunctions(content, filePath) {
|
|
90
60
|
const funcs = [];
|
|
@@ -138,7 +108,7 @@ function measureComplexity(code) {
|
|
|
138
108
|
for (const line of lines) {
|
|
139
109
|
const trimmed = line.trim();
|
|
140
110
|
// +1 for each branch/loop keyword
|
|
141
|
-
if (/\b(if|else if|else|switch|for|while|do|catch
|
|
111
|
+
if (/\b(if|else if|else|switch|for|while|do|catch)\b/.test(trimmed) || /&&|\|\|/.test(trimmed)) {
|
|
142
112
|
complexity++;
|
|
143
113
|
}
|
|
144
114
|
// +1 for ternary
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* 3. Export name collisions (same name exported from multiple files)
|
|
13
13
|
* 4. Ambiguous abbreviations (auth, config, ctx, etc. outside conventional scope)
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import { basename, extname, join } from "node:path";
|
|
15
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
17
16
|
import { gradeFromScore } from "../types.js";
|
|
18
17
|
// ── Pattern dictionaries ──
|
|
19
18
|
const SYNONYM_PAIRS = [
|
|
@@ -97,16 +96,8 @@ const AMBIGUOUS_ABBREVS = {
|
|
|
97
96
|
export function runConfusion(cwd) {
|
|
98
97
|
const start = Date.now();
|
|
99
98
|
const issues = [];
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
for (const dir of dirs) {
|
|
103
|
-
try {
|
|
104
|
-
collectFiles(join(cwd, dir), cwd, files);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
/* dir doesn't exist */
|
|
108
|
-
}
|
|
109
|
-
}
|
|
99
|
+
const sourceFiles = getProductionFiles(cwd);
|
|
100
|
+
const files = sourceFiles.map((sf) => ({ path: sf.path, base: sf.base, content: sf.content, exports: extractExports(sf.content) }));
|
|
110
101
|
if (files.length === 0) {
|
|
111
102
|
return {
|
|
112
103
|
name: "confusion",
|
|
@@ -263,22 +254,3 @@ function extractExports(content) {
|
|
|
263
254
|
}
|
|
264
255
|
return exports;
|
|
265
256
|
}
|
|
266
|
-
function collectFiles(dir, cwd, out) {
|
|
267
|
-
for (const entry of readdirSync(dir)) {
|
|
268
|
-
if (entry === "node_modules" || entry === "dist" || entry === ".git")
|
|
269
|
-
continue;
|
|
270
|
-
const full = join(dir, entry);
|
|
271
|
-
if (statSync(full).isDirectory()) {
|
|
272
|
-
collectFiles(full, cwd, out);
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
const ext = extname(entry);
|
|
276
|
-
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
|
|
277
|
-
const content = readFileSync(full, "utf-8");
|
|
278
|
-
const relPath = full.replace(`${cwd}/`, "");
|
|
279
|
-
const base = basename(entry, ext);
|
|
280
|
-
out.push({ path: relPath, base, content, exports: extractExports(content) });
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
package/dist/runners/context.js
CHANGED
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
* 3. File self-containment — ratio of local symbols to imported symbols
|
|
12
12
|
* 4. Circular dependencies — import cycles that confuse navigation
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import { extname, join } from "node:path";
|
|
14
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
16
15
|
import { gradeFromScore } from "../types.js";
|
|
17
16
|
const MAX_FILE_TOKENS = 4000; // ~400 lines; beyond this LLMs lose mid-context info
|
|
18
17
|
const MAX_IMPORTS = 15; // files importing >15 modules are hard to reason about
|
|
@@ -21,16 +20,13 @@ export function runContext(cwd) {
|
|
|
21
20
|
const start = Date.now();
|
|
22
21
|
const issues = [];
|
|
23
22
|
// Collect source files with imports
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/* dir doesn't exist */
|
|
32
|
-
}
|
|
33
|
-
}
|
|
23
|
+
const sourceFiles = getProductionFiles(cwd);
|
|
24
|
+
const files = sourceFiles.map((sf) => ({
|
|
25
|
+
path: sf.path,
|
|
26
|
+
content: sf.content,
|
|
27
|
+
imports: parseImports(sf.content),
|
|
28
|
+
tokens: Math.round(sf.content.length / CHARS_PER_TOKEN),
|
|
29
|
+
}));
|
|
34
30
|
if (files.length === 0) {
|
|
35
31
|
return {
|
|
36
32
|
name: "context",
|
|
@@ -157,14 +153,8 @@ function resolveImport(fromPath, importPath) {
|
|
|
157
153
|
parts.pop();
|
|
158
154
|
resolved = [...parts, importPath.slice(3)].join("/");
|
|
159
155
|
}
|
|
160
|
-
// Strip extension
|
|
156
|
+
// Strip known extension if present, then default to .ts
|
|
161
157
|
resolved = resolved.replace(/\.(js|ts|tsx|jsx)$/, "");
|
|
162
|
-
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
163
|
-
for (const ext of extensions) {
|
|
164
|
-
if (resolved.endsWith(ext.replace(".", "")))
|
|
165
|
-
return resolved;
|
|
166
|
-
}
|
|
167
|
-
// Return with .ts as default assumption
|
|
168
158
|
return `${resolved}.ts`;
|
|
169
159
|
}
|
|
170
160
|
// ── Cycle detection (DFS) ──
|
|
@@ -201,24 +191,3 @@ function findCycles(graph) {
|
|
|
201
191
|
}
|
|
202
192
|
return cycles;
|
|
203
193
|
}
|
|
204
|
-
// ── File collection ──
|
|
205
|
-
function collectFiles(dir, cwd, out) {
|
|
206
|
-
for (const entry of readdirSync(dir)) {
|
|
207
|
-
if (entry === "node_modules" || entry === "dist" || entry === ".git")
|
|
208
|
-
continue;
|
|
209
|
-
const full = join(dir, entry);
|
|
210
|
-
if (statSync(full).isDirectory()) {
|
|
211
|
-
collectFiles(full, cwd, out);
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
const ext = extname(entry);
|
|
215
|
-
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
|
|
216
|
-
const content = readFileSync(full, "utf-8");
|
|
217
|
-
const relPath = full.replace(`${cwd}/`, "");
|
|
218
|
-
const imports = parseImports(content);
|
|
219
|
-
const tokens = Math.round(content.length / CHARS_PER_TOKEN);
|
|
220
|
-
out.push({ path: relPath, content, imports, tokens });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -5,6 +5,34 @@ export function runDependencies(cwd, stack) {
|
|
|
5
5
|
const start = Date.now();
|
|
6
6
|
const issues = [];
|
|
7
7
|
const pm = stack.packageManager;
|
|
8
|
+
// Dart/Flutter: skip npm audit, just check pubspec for outdated
|
|
9
|
+
if (pm === "pub") {
|
|
10
|
+
const outdatedResult = run("dart pub outdated --json 2>/dev/null || true", cwd);
|
|
11
|
+
let outdatedCount = 0;
|
|
12
|
+
let majorOutdated = 0;
|
|
13
|
+
try {
|
|
14
|
+
const data = JSON.parse(outdatedResult.stdout);
|
|
15
|
+
for (const pkg of data.packages || []) {
|
|
16
|
+
if (pkg.current?.version && pkg.latest?.version && pkg.current.version !== pkg.latest.version) {
|
|
17
|
+
outdatedCount++;
|
|
18
|
+
if (pkg.current.version.split(".")[0] !== pkg.latest.version.split(".")[0])
|
|
19
|
+
majorOutdated++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch { /* parse failed */ }
|
|
24
|
+
if (majorOutdated > 0)
|
|
25
|
+
issues.push({ severity: "warning", message: `${majorOutdated} packages behind by a major version` });
|
|
26
|
+
const score = Math.max(0, Math.min(100, 100 - majorOutdated));
|
|
27
|
+
return {
|
|
28
|
+
name: "dependencies",
|
|
29
|
+
score,
|
|
30
|
+
grade: gradeFromScore(score),
|
|
31
|
+
details: { vulnerabilities: { critical: 0, high: 0, moderate: 0, low: 0 }, outdated: outdatedCount, majorOutdated },
|
|
32
|
+
issues,
|
|
33
|
+
duration: Date.now() - start,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
8
36
|
// Vulnerability audit
|
|
9
37
|
const auditCmd = pm === "pnpm" ? "pnpm audit --json" : pm === "yarn" ? "yarn audit --json" : "npm audit --json";
|
|
10
38
|
const auditResult = run(`${auditCmd} 2>/dev/null || true`, cwd);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Doc Coherence — detects contradictions between documentation and code.
|
|
2
|
+
*
|
|
3
|
+
* Premium feature (powered by LLM). Scans README, CLAUDE.md, JSDoc, and inline
|
|
4
|
+
* comments for claims that contradict the actual code:
|
|
5
|
+
* - README says "supports X" but feature X was removed
|
|
6
|
+
* - JSDoc says "@param required" but param has a default
|
|
7
|
+
* - Comment says "never throws" but function has throw statements
|
|
8
|
+
* - CHANGELOG references files that no longer exist
|
|
9
|
+
* - API docs describe endpoints/functions that were renamed or deleted
|
|
10
|
+
*
|
|
11
|
+
* Currently returns a "coming soon" placeholder.
|
|
12
|
+
*/
|
|
13
|
+
import type { CheckResult } from "../types.js";
|
|
14
|
+
export declare function runDocCoherence(cwd: string): CheckResult;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Doc Coherence — detects contradictions between documentation and code.
|
|
2
|
+
*
|
|
3
|
+
* Premium feature (powered by LLM). Scans README, CLAUDE.md, JSDoc, and inline
|
|
4
|
+
* comments for claims that contradict the actual code:
|
|
5
|
+
* - README says "supports X" but feature X was removed
|
|
6
|
+
* - JSDoc says "@param required" but param has a default
|
|
7
|
+
* - Comment says "never throws" but function has throw statements
|
|
8
|
+
* - CHANGELOG references files that no longer exist
|
|
9
|
+
* - API docs describe endpoints/functions that were renamed or deleted
|
|
10
|
+
*
|
|
11
|
+
* Currently returns a "coming soon" placeholder.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
16
|
+
export function runDocCoherence(cwd) {
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
// Detect if docs exist (useful info even in placeholder mode)
|
|
19
|
+
const docFiles = [];
|
|
20
|
+
const candidates = ["README.md", "CLAUDE.md", "ARCHITECTURE.md", "CONTRIBUTING.md", "CHANGELOG.md", "API.md", "docs/README.md"];
|
|
21
|
+
for (const f of candidates) {
|
|
22
|
+
if (existsSync(join(cwd, f)))
|
|
23
|
+
docFiles.push(f);
|
|
24
|
+
}
|
|
25
|
+
let hasJSDoc = false;
|
|
26
|
+
try {
|
|
27
|
+
const files = getProductionFiles(cwd);
|
|
28
|
+
hasJSDoc = files.some((f) => /\/\*\*/.test(f.content));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// no source files
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
name: "doc-coherence",
|
|
35
|
+
score: 0,
|
|
36
|
+
grade: "F",
|
|
37
|
+
details: {
|
|
38
|
+
premium: true,
|
|
39
|
+
comingSoon: true,
|
|
40
|
+
reason: "LLM-powered analysis — coming soon",
|
|
41
|
+
docFiles,
|
|
42
|
+
hasJSDoc,
|
|
43
|
+
description: "Detects contradictions between documentation and code. Finds stale README claims, incorrect JSDoc, outdated API docs, and misleading comments.",
|
|
44
|
+
},
|
|
45
|
+
issues: [],
|
|
46
|
+
duration: Date.now() - start,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/runners/docs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Documentation check — README, JSDoc, code comments. */
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
4
5
|
import { gradeFromScore } from "../types.js";
|
|
5
6
|
export function runDocs(cwd) {
|
|
6
7
|
const start = Date.now();
|
|
@@ -34,21 +35,11 @@ export function runDocs(cwd) {
|
|
|
34
35
|
issues.push({ severity: "warning", message: "README has very little content", rule: "readme-sparse" });
|
|
35
36
|
}
|
|
36
37
|
// Check exported function documentation
|
|
37
|
-
const
|
|
38
|
-
const dirs = ["src", "web/src"];
|
|
39
|
-
for (const dir of dirs) {
|
|
40
|
-
try {
|
|
41
|
-
collectFiles(join(cwd, dir), files);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
/* dir doesn't exist */
|
|
45
|
-
}
|
|
46
|
-
}
|
|
38
|
+
const sourceFiles = getProductionFiles(cwd);
|
|
47
39
|
let totalExports = 0;
|
|
48
40
|
let documentedExports = 0;
|
|
49
|
-
for (const
|
|
50
|
-
const
|
|
51
|
-
const lines = content.split("\n");
|
|
41
|
+
for (const sf of sourceFiles) {
|
|
42
|
+
const lines = sf.content.split("\n");
|
|
52
43
|
for (let i = 0; i < lines.length; i++) {
|
|
53
44
|
const line = lines[i].trim();
|
|
54
45
|
if (line.startsWith("export function ") ||
|
|
@@ -84,7 +75,7 @@ export function runDocs(cwd) {
|
|
|
84
75
|
score,
|
|
85
76
|
grade: gradeFromScore(score),
|
|
86
77
|
details: {
|
|
87
|
-
readmeLines: existsSync(
|
|
78
|
+
readmeLines: existsSync(join(cwd, "README.md")) ? readFileSync(join(cwd, "README.md"), "utf-8").split("\n").length : 0,
|
|
88
79
|
totalExports,
|
|
89
80
|
documentedExports,
|
|
90
81
|
documentedPct: totalExports > 0 ? `${Math.round((documentedExports / totalExports) * 100)}%` : "n/a",
|
|
@@ -93,19 +84,3 @@ export function runDocs(cwd) {
|
|
|
93
84
|
duration: Date.now() - start,
|
|
94
85
|
};
|
|
95
86
|
}
|
|
96
|
-
function collectFiles(dir, out) {
|
|
97
|
-
for (const entry of readdirSync(dir)) {
|
|
98
|
-
if (entry === "node_modules" || entry === "dist")
|
|
99
|
-
continue;
|
|
100
|
-
const full = join(dir, entry);
|
|
101
|
-
if (statSync(full).isDirectory()) {
|
|
102
|
-
collectFiles(full, out);
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
const ext = extname(entry);
|
|
106
|
-
if ((ext === ".ts" || ext === ".tsx") && !entry.includes(".test.") && !entry.includes(".spec.")) {
|
|
107
|
-
out.push(full);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
/** Code duplication detection — finds copy-pasted blocks. */
|
|
2
|
-
import {
|
|
3
|
-
import { extname, join } from "node:path";
|
|
2
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
4
3
|
import { gradeFromScore } from "../types.js";
|
|
5
4
|
const MIN_LINES = 6; // minimum duplicate block size
|
|
6
5
|
const MIN_TOKENS = 50; // minimum token count for a duplicate
|
|
7
6
|
export function runDuplication(cwd) {
|
|
8
7
|
const start = Date.now();
|
|
9
8
|
const issues = [];
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
for (const dir of dirs) {
|
|
13
|
-
try {
|
|
14
|
-
collectFiles(join(cwd, dir), files);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
/* dir doesn't exist */
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
if (files.length < 2) {
|
|
9
|
+
const sourceFiles = getProductionFiles(cwd);
|
|
10
|
+
if (sourceFiles.length < 2) {
|
|
21
11
|
return {
|
|
22
12
|
name: "duplication",
|
|
23
13
|
score: 100,
|
|
24
14
|
grade: "A",
|
|
25
|
-
details: { filesScanned:
|
|
15
|
+
details: { filesScanned: sourceFiles.length, duplicates: 0 },
|
|
26
16
|
issues: [],
|
|
27
17
|
duration: Date.now() - start,
|
|
28
18
|
};
|
|
@@ -31,23 +21,21 @@ export function runDuplication(cwd) {
|
|
|
31
21
|
// Build a map of normalized line hashes → locations
|
|
32
22
|
const lineMap = new Map();
|
|
33
23
|
let totalSourceLines = 0;
|
|
34
|
-
for (const
|
|
35
|
-
const
|
|
36
|
-
const relPath = file.replace(`${cwd}/`, "");
|
|
37
|
-
const lines = content.split("\n");
|
|
24
|
+
for (const sf of sourceFiles) {
|
|
25
|
+
const lines = sf.content.split("\n");
|
|
38
26
|
totalSourceLines += lines.length;
|
|
39
27
|
for (let i = 0; i <= lines.length - MIN_LINES; i++) {
|
|
40
28
|
const block = lines
|
|
41
29
|
.slice(i, i + MIN_LINES)
|
|
42
30
|
.map((l) => l.trim())
|
|
43
|
-
.filter((l) => l.length > 0 && !l.startsWith("//") && !l.startsWith("*") && l !== "{" && l !== "}" && l !== "");
|
|
31
|
+
.filter((l) => l.length > 0 && !l.startsWith("//") && !l.startsWith("*") && !l.startsWith("import ") && !l.startsWith("export {") && l !== "{" && l !== "}" && l !== "");
|
|
44
32
|
if (block.length < MIN_LINES - 2)
|
|
45
33
|
continue; // too many empty/trivial lines
|
|
46
34
|
const key = block.join("\n");
|
|
47
35
|
if (key.length < MIN_TOKENS)
|
|
48
36
|
continue;
|
|
49
37
|
const locs = lineMap.get(key) || [];
|
|
50
|
-
locs.push({ file:
|
|
38
|
+
locs.push({ file: sf.path, line: i + 1 });
|
|
51
39
|
lineMap.set(key, locs);
|
|
52
40
|
}
|
|
53
41
|
}
|
|
@@ -86,24 +74,8 @@ export function runDuplication(cwd) {
|
|
|
86
74
|
name: "duplication",
|
|
87
75
|
score,
|
|
88
76
|
grade: gradeFromScore(score),
|
|
89
|
-
details: { filesScanned:
|
|
77
|
+
details: { filesScanned: sourceFiles.length, totalSourceLines, duplicateBlocks: duplicates.length, duplicationPct: `${dupPct}%` },
|
|
90
78
|
issues,
|
|
91
79
|
duration: Date.now() - start,
|
|
92
80
|
};
|
|
93
81
|
}
|
|
94
|
-
function collectFiles(dir, out) {
|
|
95
|
-
for (const entry of readdirSync(dir)) {
|
|
96
|
-
if (entry === "node_modules" || entry === "dist" || entry === ".git")
|
|
97
|
-
continue;
|
|
98
|
-
const full = join(dir, entry);
|
|
99
|
-
if (statSync(full).isDirectory()) {
|
|
100
|
-
collectFiles(full, out);
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
const ext = extname(entry);
|
|
104
|
-
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
|
|
105
|
-
out.push(full);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
package/dist/runners/lint.js
CHANGED
|
@@ -49,6 +49,23 @@ export function runLint(cwd, stack) {
|
|
|
49
49
|
/* eslint output parse failed */
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
else if (stack.linter === "dart_analyze") {
|
|
53
|
+
const { stdout } = run("dart analyze --format=machine 2>/dev/null || true", cwd);
|
|
54
|
+
// machine format: SEVERITY|TYPE|CODE|PATH|LINE|COL|LEN|MESSAGE
|
|
55
|
+
for (const line of stdout.split("\n")) {
|
|
56
|
+
const parts = line.split("|");
|
|
57
|
+
if (parts.length < 8)
|
|
58
|
+
continue;
|
|
59
|
+
const severity = parts[0] === "ERROR" ? "error" : parts[0] === "WARNING" ? "warning" : "info";
|
|
60
|
+
issues.push({
|
|
61
|
+
severity,
|
|
62
|
+
message: parts[7],
|
|
63
|
+
file: parts[3],
|
|
64
|
+
line: parseInt(parts[4], 10) || undefined,
|
|
65
|
+
rule: parts[2],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
52
69
|
else {
|
|
53
70
|
return {
|
|
54
71
|
name: "lint",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Performance check — barrel imports, dynamic import opportunities, large bundles.
|
|
2
|
+
*
|
|
3
|
+
* Sub-checks:
|
|
4
|
+
* 1. Barrel import smell — index.ts re-exports that defeat tree-shaking
|
|
5
|
+
* 2. Heavy dependencies — bundled packages known to bloat output
|
|
6
|
+
* 3. Dynamic import opportunities — large imports that could be lazy-loaded
|
|
7
|
+
* 4. CSS-in-JS overhead — detects runtime CSS solutions vs zero-runtime alternatives
|
|
8
|
+
*/
|
|
9
|
+
import type { CheckResult } from "../types.js";
|
|
10
|
+
export declare function runPerformance(cwd: string): CheckResult;
|