@darrenjcoxon/vibeoptimise 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +81 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/orchestrator.d.ts +16 -0
- package/dist/orchestrator.js +201 -0
- package/dist/reporter.d.ts +15 -0
- package/dist/reporter.js +140 -0
- package/dist/scanners/bundlephobia.d.ts +19 -0
- package/dist/scanners/bundlephobia.js +189 -0
- package/dist/scanners/css-efficiency.d.ts +15 -0
- package/dist/scanners/css-efficiency.js +210 -0
- package/dist/scanners/depcheck.d.ts +17 -0
- package/dist/scanners/depcheck.js +133 -0
- package/dist/scanners/eslint-perf.d.ts +23 -0
- package/dist/scanners/eslint-perf.js +209 -0
- package/dist/scanners/index.d.ts +26 -0
- package/dist/scanners/index.js +53 -0
- package/dist/scanners/jscpd.d.ts +17 -0
- package/dist/scanners/jscpd.js +150 -0
- package/dist/scanners/knip.d.ts +17 -0
- package/dist/scanners/knip.js +248 -0
- package/dist/scanners/madge.d.ts +19 -0
- package/dist/scanners/madge.js +162 -0
- package/dist/scanners/query-efficiency.d.ts +20 -0
- package/dist/scanners/query-efficiency.js +232 -0
- package/dist/scanners/regex-safety.d.ts +21 -0
- package/dist/scanners/regex-safety.js +221 -0
- package/dist/scanners/source-map-explorer.d.ts +21 -0
- package/dist/scanners/source-map-explorer.js +177 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +35 -0
- package/package.json +65 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Performance Scanner — Import Hygiene & Inefficient Patterns
|
|
3
|
+
*
|
|
4
|
+
* Runs ESLint with performance-focused rules to detect:
|
|
5
|
+
* - Import cycles (eslint-plugin-import)
|
|
6
|
+
* - Unused imports
|
|
7
|
+
* - Barrel file anti-patterns
|
|
8
|
+
* - Inefficient array/object patterns
|
|
9
|
+
*
|
|
10
|
+
* Tool: eslint (npm)
|
|
11
|
+
* Output: JSON formatter
|
|
12
|
+
*/
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
export class EslintPerfScanner {
|
|
17
|
+
name = 'ESLint Perf';
|
|
18
|
+
description = 'Performance-focused linting — import cycles, unused imports, inefficient patterns';
|
|
19
|
+
category = 'code-quality';
|
|
20
|
+
async isAvailable() {
|
|
21
|
+
try {
|
|
22
|
+
execSync('npx eslint --version', { stdio: 'pipe', timeout: 15000 });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async scan(targetDir, options) {
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
const findings = [];
|
|
32
|
+
try {
|
|
33
|
+
// Check if eslint is available in the project
|
|
34
|
+
const hasEslint = existsSync(join(targetDir, 'node_modules', '.bin', 'eslint'))
|
|
35
|
+
|| existsSync(join(targetDir, 'node_modules', 'eslint'));
|
|
36
|
+
if (!hasEslint) {
|
|
37
|
+
// Try with npx instead
|
|
38
|
+
return this.scanWithCustomRules(targetDir, options, start);
|
|
39
|
+
}
|
|
40
|
+
// Run eslint with JSON output
|
|
41
|
+
const extensions = '--ext .js,.jsx,.ts,.tsx,.mjs';
|
|
42
|
+
const ignorePattern = '--ignore-pattern "node_modules" --ignore-pattern "dist" --ignore-pattern "build" --ignore-pattern ".next"';
|
|
43
|
+
try {
|
|
44
|
+
const cmd = `npx eslint "${targetDir}" ${extensions} ${ignorePattern} -f json --no-error-on-unmatched-pattern 2>/dev/null`;
|
|
45
|
+
const output = execSync(cmd, {
|
|
46
|
+
cwd: targetDir,
|
|
47
|
+
stdio: 'pipe',
|
|
48
|
+
timeout: 120000,
|
|
49
|
+
}).toString();
|
|
50
|
+
if (output.trim()) {
|
|
51
|
+
const results = JSON.parse(output);
|
|
52
|
+
this.processEslintResults(results, findings);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// ESLint exits non-zero when it finds errors
|
|
57
|
+
const output = err.stdout?.toString() || '';
|
|
58
|
+
if (output.trim()) {
|
|
59
|
+
try {
|
|
60
|
+
const results = JSON.parse(output);
|
|
61
|
+
this.processEslintResults(results, findings);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// JSON parse failed — ESLint might have errored
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Additionally, scan for common anti-patterns
|
|
69
|
+
this.scanForAntiPatterns(targetDir, findings);
|
|
70
|
+
return {
|
|
71
|
+
scanner: this.name,
|
|
72
|
+
findings,
|
|
73
|
+
summary: findings.length === 0
|
|
74
|
+
? '✅ No performance-impacting lint issues found'
|
|
75
|
+
: `Found ${findings.length} performance-related lint issues`,
|
|
76
|
+
duration: Date.now() - start,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
scanner: this.name,
|
|
82
|
+
findings: [],
|
|
83
|
+
summary: `Error: ${err.message}`,
|
|
84
|
+
duration: Date.now() - start,
|
|
85
|
+
error: err.message,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async scanWithCustomRules(targetDir, options, start) {
|
|
90
|
+
const findings = [];
|
|
91
|
+
// Even without project ESLint, scan for anti-patterns
|
|
92
|
+
this.scanForAntiPatterns(targetDir, findings);
|
|
93
|
+
return {
|
|
94
|
+
scanner: this.name,
|
|
95
|
+
findings,
|
|
96
|
+
summary: findings.length === 0
|
|
97
|
+
? '✅ No performance anti-patterns detected'
|
|
98
|
+
: `Found ${findings.length} performance anti-patterns`,
|
|
99
|
+
duration: Date.now() - start,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
processEslintResults(results, findings) {
|
|
103
|
+
if (!Array.isArray(results))
|
|
104
|
+
return;
|
|
105
|
+
// Performance-relevant rule IDs
|
|
106
|
+
const perfRules = new Set([
|
|
107
|
+
'import/no-cycle',
|
|
108
|
+
'import/no-duplicates',
|
|
109
|
+
'import/no-unused-modules',
|
|
110
|
+
'no-unused-vars',
|
|
111
|
+
'@typescript-eslint/no-unused-vars',
|
|
112
|
+
'unused-imports/no-unused-imports',
|
|
113
|
+
'no-console',
|
|
114
|
+
'no-debugger',
|
|
115
|
+
'prefer-const',
|
|
116
|
+
'no-var',
|
|
117
|
+
]);
|
|
118
|
+
for (const fileResult of results) {
|
|
119
|
+
if (!fileResult.messages || !Array.isArray(fileResult.messages))
|
|
120
|
+
continue;
|
|
121
|
+
for (const msg of fileResult.messages) {
|
|
122
|
+
if (!msg.ruleId)
|
|
123
|
+
continue;
|
|
124
|
+
// Only include performance-relevant rules
|
|
125
|
+
const isPerf = perfRules.has(msg.ruleId) ||
|
|
126
|
+
msg.ruleId.startsWith('import/') ||
|
|
127
|
+
msg.ruleId.includes('unused');
|
|
128
|
+
if (!isPerf)
|
|
129
|
+
continue;
|
|
130
|
+
let severity = 'low';
|
|
131
|
+
if (msg.ruleId === 'import/no-cycle')
|
|
132
|
+
severity = 'high';
|
|
133
|
+
else if (msg.ruleId.includes('unused'))
|
|
134
|
+
severity = 'medium';
|
|
135
|
+
findings.push({
|
|
136
|
+
id: `eslint-${msg.ruleId}-${fileResult.filePath}-${msg.line}`,
|
|
137
|
+
scanner: this.name,
|
|
138
|
+
category: msg.ruleId.startsWith('import/') ? 'import-hygiene' : 'code-quality',
|
|
139
|
+
severity,
|
|
140
|
+
title: `${msg.ruleId}: ${msg.message}`,
|
|
141
|
+
description: msg.message,
|
|
142
|
+
file: fileResult.filePath,
|
|
143
|
+
line: msg.line,
|
|
144
|
+
column: msg.column,
|
|
145
|
+
suggestion: msg.fix
|
|
146
|
+
? 'Auto-fixable: run `eslint --fix` to resolve this automatically.'
|
|
147
|
+
: `Address the ESLint rule "${msg.ruleId}" violation.`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
scanForAntiPatterns(targetDir, findings) {
|
|
153
|
+
try {
|
|
154
|
+
// Check for barrel files that re-export everything (causes bundle bloat)
|
|
155
|
+
const barrelCheck = execSync(`find "${targetDir}" -name "index.ts" -o -name "index.js" | head -20`, { stdio: 'pipe', timeout: 10000 }).toString().trim();
|
|
156
|
+
if (barrelCheck) {
|
|
157
|
+
const barrelFiles = barrelCheck.split('\n').filter(f => !f.includes('node_modules') && !f.includes('dist') && !f.includes('.next'));
|
|
158
|
+
for (const barrelFile of barrelFiles) {
|
|
159
|
+
try {
|
|
160
|
+
const content = execSync(`cat "${barrelFile}"`, { stdio: 'pipe' }).toString();
|
|
161
|
+
const exportAllCount = (content.match(/export \* from/g) || []).length;
|
|
162
|
+
if (exportAllCount > 5) {
|
|
163
|
+
findings.push({
|
|
164
|
+
id: `eslint-barrel-${barrelFile}`,
|
|
165
|
+
scanner: this.name,
|
|
166
|
+
category: 'bundle-size',
|
|
167
|
+
severity: 'medium',
|
|
168
|
+
title: `Barrel file with ${exportAllCount} wildcard re-exports`,
|
|
169
|
+
description: `"${barrelFile}" uses ${exportAllCount} \`export * from\` statements. This can defeat tree-shaking and pull in unused code.`,
|
|
170
|
+
file: barrelFile,
|
|
171
|
+
suggestion: `Replace \`export * from\` with named exports to enable tree-shaking:\n\`export { SpecificThing } from './module'\`\n\nOnly re-export what consumers actually need.`,
|
|
172
|
+
estimatedImpact: 'Better tree-shaking, smaller bundles',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Skip files we can't read
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Check for console.log statements in source (not test) files
|
|
182
|
+
try {
|
|
183
|
+
const consoleLogs = execSync(`grep -rn "console\\.log" "${targetDir}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" | grep -v node_modules | grep -v dist | grep -v ".test." | grep -v ".spec." | grep -v "__tests__" | head -20`, { stdio: 'pipe', timeout: 10000 }).toString().trim();
|
|
184
|
+
if (consoleLogs) {
|
|
185
|
+
const logLines = consoleLogs.split('\n').filter(Boolean);
|
|
186
|
+
if (logLines.length > 3) {
|
|
187
|
+
findings.push({
|
|
188
|
+
id: `eslint-console-logs`,
|
|
189
|
+
scanner: this.name,
|
|
190
|
+
category: 'code-quality',
|
|
191
|
+
severity: 'low',
|
|
192
|
+
title: `${logLines.length}+ console.log statements in production code`,
|
|
193
|
+
description: `Found ${logLines.length}+ console.log statements outside test files. These add unnecessary overhead and noise.`,
|
|
194
|
+
file: logLines[0]?.split(':')[0] || 'multiple files',
|
|
195
|
+
suggestion: `Remove console.log statements from production code. Use a proper logger (like pino or winston) that can be configured per environment, or use console.log only in development with a build-time strip.`,
|
|
196
|
+
estimatedImpact: 'Cleaner output, slight performance improvement',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// grep found nothing — that's good
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Anti-pattern scanning is best-effort
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner Registry
|
|
3
|
+
*
|
|
4
|
+
* All 10 VibeOptimise scanners, registered in priority order.
|
|
5
|
+
*/
|
|
6
|
+
export { KnipScanner } from './knip.js';
|
|
7
|
+
export { JscpdScanner } from './jscpd.js';
|
|
8
|
+
export { MadgeScanner } from './madge.js';
|
|
9
|
+
export { DepcheckScanner } from './depcheck.js';
|
|
10
|
+
export { BundlePhobiaScanner } from './bundlephobia.js';
|
|
11
|
+
export { EslintPerfScanner } from './eslint-perf.js';
|
|
12
|
+
export { SourceMapExplorerScanner } from './source-map-explorer.js';
|
|
13
|
+
export { RegexSafetyScanner } from './regex-safety.js';
|
|
14
|
+
export { CssEfficiencyScanner } from './css-efficiency.js';
|
|
15
|
+
export { QueryEfficiencyScanner } from './query-efficiency.js';
|
|
16
|
+
import { Scanner } from '../types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Get all scanners in priority order:
|
|
19
|
+
* 1. Performance-critical (circular deps, regex safety)
|
|
20
|
+
* 2. Dead code removal (knip, depcheck)
|
|
21
|
+
* 3. Duplication (jscpd)
|
|
22
|
+
* 4. Bundle & dependency analysis
|
|
23
|
+
* 5. Code quality & CSS
|
|
24
|
+
* 6. Query efficiency
|
|
25
|
+
*/
|
|
26
|
+
export declare function getAllScanners(): Scanner[];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner Registry
|
|
3
|
+
*
|
|
4
|
+
* All 10 VibeOptimise scanners, registered in priority order.
|
|
5
|
+
*/
|
|
6
|
+
export { KnipScanner } from './knip.js';
|
|
7
|
+
export { JscpdScanner } from './jscpd.js';
|
|
8
|
+
export { MadgeScanner } from './madge.js';
|
|
9
|
+
export { DepcheckScanner } from './depcheck.js';
|
|
10
|
+
export { BundlePhobiaScanner } from './bundlephobia.js';
|
|
11
|
+
export { EslintPerfScanner } from './eslint-perf.js';
|
|
12
|
+
export { SourceMapExplorerScanner } from './source-map-explorer.js';
|
|
13
|
+
export { RegexSafetyScanner } from './regex-safety.js';
|
|
14
|
+
export { CssEfficiencyScanner } from './css-efficiency.js';
|
|
15
|
+
export { QueryEfficiencyScanner } from './query-efficiency.js';
|
|
16
|
+
import { KnipScanner } from './knip.js';
|
|
17
|
+
import { JscpdScanner } from './jscpd.js';
|
|
18
|
+
import { MadgeScanner } from './madge.js';
|
|
19
|
+
import { DepcheckScanner } from './depcheck.js';
|
|
20
|
+
import { BundlePhobiaScanner } from './bundlephobia.js';
|
|
21
|
+
import { EslintPerfScanner } from './eslint-perf.js';
|
|
22
|
+
import { SourceMapExplorerScanner } from './source-map-explorer.js';
|
|
23
|
+
import { RegexSafetyScanner } from './regex-safety.js';
|
|
24
|
+
import { CssEfficiencyScanner } from './css-efficiency.js';
|
|
25
|
+
import { QueryEfficiencyScanner } from './query-efficiency.js';
|
|
26
|
+
/**
|
|
27
|
+
* Get all scanners in priority order:
|
|
28
|
+
* 1. Performance-critical (circular deps, regex safety)
|
|
29
|
+
* 2. Dead code removal (knip, depcheck)
|
|
30
|
+
* 3. Duplication (jscpd)
|
|
31
|
+
* 4. Bundle & dependency analysis
|
|
32
|
+
* 5. Code quality & CSS
|
|
33
|
+
* 6. Query efficiency
|
|
34
|
+
*/
|
|
35
|
+
export function getAllScanners() {
|
|
36
|
+
return [
|
|
37
|
+
// Tier 1: Performance-critical
|
|
38
|
+
new MadgeScanner(),
|
|
39
|
+
new RegexSafetyScanner(),
|
|
40
|
+
// Tier 2: Dead weight removal
|
|
41
|
+
new KnipScanner(),
|
|
42
|
+
new DepcheckScanner(),
|
|
43
|
+
// Tier 3: Duplication
|
|
44
|
+
new JscpdScanner(),
|
|
45
|
+
// Tier 4: Bundle & dependency health
|
|
46
|
+
new BundlePhobiaScanner(),
|
|
47
|
+
new SourceMapExplorerScanner(),
|
|
48
|
+
new EslintPerfScanner(),
|
|
49
|
+
// Tier 5: Asset & query efficiency
|
|
50
|
+
new CssEfficiencyScanner(),
|
|
51
|
+
new QueryEfficiencyScanner(),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSCPD Scanner — Code Duplication Detection
|
|
3
|
+
*
|
|
4
|
+
* Finds copy/pasted code blocks across the entire codebase.
|
|
5
|
+
* Uses Rabin-Karp algorithm, supports 150+ languages.
|
|
6
|
+
*
|
|
7
|
+
* Tool: jscpd (npm)
|
|
8
|
+
* Output: JSON reporter
|
|
9
|
+
*/
|
|
10
|
+
import { Scanner, ScannerResult, ScanOptions } from '../types.js';
|
|
11
|
+
export declare class JscpdScanner implements Scanner {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
category: "duplication";
|
|
15
|
+
isAvailable(): Promise<boolean>;
|
|
16
|
+
scan(targetDir: string, options: ScanOptions): Promise<ScannerResult>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSCPD Scanner — Code Duplication Detection
|
|
3
|
+
*
|
|
4
|
+
* Finds copy/pasted code blocks across the entire codebase.
|
|
5
|
+
* Uses Rabin-Karp algorithm, supports 150+ languages.
|
|
6
|
+
*
|
|
7
|
+
* Tool: jscpd (npm)
|
|
8
|
+
* Output: JSON reporter
|
|
9
|
+
*/
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
export class JscpdScanner {
|
|
14
|
+
name = 'JSCPD';
|
|
15
|
+
description = 'Copy/paste detector — finds duplicated code blocks across the codebase';
|
|
16
|
+
category = 'duplication';
|
|
17
|
+
async isAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
execSync('npx jscpd --version', { stdio: 'pipe', timeout: 15000 });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async scan(targetDir, options) {
|
|
27
|
+
const start = Date.now();
|
|
28
|
+
const findings = [];
|
|
29
|
+
try {
|
|
30
|
+
const reportDir = join(targetDir, '.vibeoptimise-tmp');
|
|
31
|
+
mkdirSync(reportDir, { recursive: true });
|
|
32
|
+
const excludePatterns = [
|
|
33
|
+
'**/node_modules/**',
|
|
34
|
+
'**/dist/**',
|
|
35
|
+
'**/build/**',
|
|
36
|
+
'**/.next/**',
|
|
37
|
+
'**/coverage/**',
|
|
38
|
+
'**/*.min.js',
|
|
39
|
+
'**/*.map',
|
|
40
|
+
'**/package-lock.json',
|
|
41
|
+
'**/yarn.lock',
|
|
42
|
+
'**/pnpm-lock.yaml',
|
|
43
|
+
...(options.exclude || []),
|
|
44
|
+
].map(p => `"${p}"`).join(',');
|
|
45
|
+
const cmd = [
|
|
46
|
+
'npx jscpd',
|
|
47
|
+
`"${targetDir}"`,
|
|
48
|
+
'--min-tokens 50',
|
|
49
|
+
'--min-lines 5',
|
|
50
|
+
'--reporters json',
|
|
51
|
+
`--output "${reportDir}"`,
|
|
52
|
+
`--ignore ${excludePatterns}`,
|
|
53
|
+
'--silent',
|
|
54
|
+
'--formats "javascript,typescript,jsx,tsx,css,scss"',
|
|
55
|
+
].join(' ');
|
|
56
|
+
try {
|
|
57
|
+
execSync(cmd, {
|
|
58
|
+
cwd: targetDir,
|
|
59
|
+
stdio: 'pipe',
|
|
60
|
+
timeout: 120000,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// jscpd may exit non-zero when duplication exceeds threshold
|
|
65
|
+
}
|
|
66
|
+
// Read the JSON report
|
|
67
|
+
const reportPath = join(reportDir, 'jscpd-report.json');
|
|
68
|
+
if (!existsSync(reportPath)) {
|
|
69
|
+
// Clean up
|
|
70
|
+
try {
|
|
71
|
+
execSync(`rm -rf "${reportDir}"`, { stdio: 'pipe' });
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
return {
|
|
75
|
+
scanner: this.name,
|
|
76
|
+
findings: [],
|
|
77
|
+
summary: '✅ No code duplication detected',
|
|
78
|
+
duration: Date.now() - start,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const report = JSON.parse(readFileSync(reportPath, 'utf-8'));
|
|
82
|
+
// Clean up temp directory
|
|
83
|
+
try {
|
|
84
|
+
execSync(`rm -rf "${reportDir}"`, { stdio: 'pipe' });
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
if (report.duplicates && Array.isArray(report.duplicates)) {
|
|
88
|
+
for (const clone of report.duplicates) {
|
|
89
|
+
const firstFile = clone.firstFile?.name || 'unknown';
|
|
90
|
+
const secondFile = clone.secondFile?.name || 'unknown';
|
|
91
|
+
const lines = clone.lines || 0;
|
|
92
|
+
const tokens = clone.tokens || 0;
|
|
93
|
+
const firstStart = clone.firstFile?.startLoc?.line || 0;
|
|
94
|
+
const firstEnd = clone.firstFile?.endLoc?.line || 0;
|
|
95
|
+
const secondStart = clone.secondFile?.startLoc?.line || 0;
|
|
96
|
+
const secondEnd = clone.secondFile?.endLoc?.line || 0;
|
|
97
|
+
// Determine severity based on duplication size
|
|
98
|
+
let severity = 'low';
|
|
99
|
+
if (lines > 50)
|
|
100
|
+
severity = 'high';
|
|
101
|
+
else if (lines > 20)
|
|
102
|
+
severity = 'medium';
|
|
103
|
+
else if (lines > 10)
|
|
104
|
+
severity = 'low';
|
|
105
|
+
const sameFile = firstFile === secondFile;
|
|
106
|
+
findings.push({
|
|
107
|
+
id: `jscpd-${firstFile}-${firstStart}-${secondFile}-${secondStart}`,
|
|
108
|
+
scanner: this.name,
|
|
109
|
+
category: 'duplication',
|
|
110
|
+
severity,
|
|
111
|
+
title: `${lines} duplicated lines (${tokens} tokens)`,
|
|
112
|
+
description: sameFile
|
|
113
|
+
? `Duplicated code block within "${firstFile}" — lines ${firstStart}-${firstEnd} and ${secondStart}-${secondEnd} are identical.`
|
|
114
|
+
: `Duplicated code between "${firstFile}" (lines ${firstStart}-${firstEnd}) and "${secondFile}" (lines ${secondStart}-${secondEnd}).`,
|
|
115
|
+
file: firstFile,
|
|
116
|
+
line: firstStart,
|
|
117
|
+
endLine: firstEnd,
|
|
118
|
+
suggestion: sameFile
|
|
119
|
+
? `Extract the duplicated code (lines ${firstStart}-${firstEnd} and ${secondStart}-${secondEnd}) into a shared function within this file.`
|
|
120
|
+
: `Extract the shared logic into a common utility module and import it from both "${firstFile}" and "${secondFile}".`,
|
|
121
|
+
estimatedImpact: `~${lines} lines can be consolidated`,
|
|
122
|
+
relatedFiles: sameFile ? undefined : [secondFile],
|
|
123
|
+
metadata: { tokens, secondFileStart: secondStart, secondFileEnd: secondEnd },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Build summary from statistics
|
|
128
|
+
const stats = report.statistics;
|
|
129
|
+
const totalDuplication = stats?.total?.percentage ? `${stats.total.percentage.toFixed(1)}%` : 'unknown';
|
|
130
|
+
const totalClones = findings.length;
|
|
131
|
+
return {
|
|
132
|
+
scanner: this.name,
|
|
133
|
+
findings,
|
|
134
|
+
summary: totalClones === 0
|
|
135
|
+
? '✅ No significant code duplication found'
|
|
136
|
+
: `Found ${totalClones} duplicated blocks (${totalDuplication} total duplication)`,
|
|
137
|
+
duration: Date.now() - start,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
return {
|
|
142
|
+
scanner: this.name,
|
|
143
|
+
findings: [],
|
|
144
|
+
summary: `Error: ${err.message}`,
|
|
145
|
+
duration: Date.now() - start,
|
|
146
|
+
error: err.message,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knip Scanner — Dead Code Detection
|
|
3
|
+
*
|
|
4
|
+
* Finds unused files, dependencies, exports, types, and class members.
|
|
5
|
+
* The most comprehensive dead code scanner for JS/TS projects.
|
|
6
|
+
*
|
|
7
|
+
* Tool: knip (npm)
|
|
8
|
+
* Output: JSON via --reporter json
|
|
9
|
+
*/
|
|
10
|
+
import { Scanner, ScannerResult, ScanOptions } from '../types.js';
|
|
11
|
+
export declare class KnipScanner implements Scanner {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
category: "dead-code";
|
|
15
|
+
isAvailable(): Promise<boolean>;
|
|
16
|
+
scan(targetDir: string, options: ScanOptions): Promise<ScannerResult>;
|
|
17
|
+
}
|