@aiready/pattern-detect 0.1.2 → 0.2.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 +214 -3
- package/dist/chunk-AXHGYYYZ.mjs +404 -0
- package/dist/chunk-JKVKOXYR.mjs +407 -0
- package/dist/chunk-OFGMDX66.mjs +402 -0
- package/dist/chunk-QE4E3F7C.mjs +410 -0
- package/dist/chunk-TXWPOVYU.mjs +402 -0
- package/dist/cli.js +265 -65
- package/dist/cli.mjs +52 -32
- package/dist/index.d.mts +20 -3
- package/dist/index.d.ts +20 -3
- package/dist/index.js +214 -34
- package/dist/index.mjs +1 -1
- package/package.json +11 -11
- package/dist/chunk-K5O2HVB5.mjs +0 -114
- package/dist/chunk-RLWJXASG.mjs +0 -227
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ interface DuplicatePattern {
|
|
|
5
5
|
file2: string;
|
|
6
6
|
line1: number;
|
|
7
7
|
line2: number;
|
|
8
|
+
endLine1: number;
|
|
9
|
+
endLine2: number;
|
|
8
10
|
similarity: number;
|
|
9
11
|
snippet: string;
|
|
10
12
|
patternType: PatternType;
|
|
@@ -19,23 +21,38 @@ interface FileContent {
|
|
|
19
21
|
interface DetectionOptions {
|
|
20
22
|
minSimilarity: number;
|
|
21
23
|
minLines: number;
|
|
24
|
+
maxBlocks?: number;
|
|
25
|
+
batchSize?: number;
|
|
26
|
+
approx?: boolean;
|
|
27
|
+
minSharedTokens?: number;
|
|
28
|
+
maxCandidatesPerBlock?: number;
|
|
29
|
+
maxComparisons?: number;
|
|
30
|
+
streamResults?: boolean;
|
|
22
31
|
}
|
|
23
32
|
/**
|
|
24
33
|
* Detect duplicate patterns across files with enhanced analysis
|
|
25
34
|
*/
|
|
26
|
-
declare function detectDuplicatePatterns(files: FileContent[], options: DetectionOptions): DuplicatePattern[]
|
|
35
|
+
declare function detectDuplicatePatterns(files: FileContent[], options: DetectionOptions): Promise<DuplicatePattern[]>;
|
|
27
36
|
|
|
28
37
|
interface PatternDetectOptions extends ScanOptions {
|
|
29
38
|
minSimilarity?: number;
|
|
30
39
|
minLines?: number;
|
|
40
|
+
batchSize?: number;
|
|
41
|
+
approx?: boolean;
|
|
42
|
+
minSharedTokens?: number;
|
|
43
|
+
maxCandidatesPerBlock?: number;
|
|
44
|
+
streamResults?: boolean;
|
|
31
45
|
}
|
|
32
46
|
interface PatternSummary {
|
|
33
47
|
totalPatterns: number;
|
|
34
48
|
totalTokenCost: number;
|
|
35
49
|
patternsByType: Record<PatternType, number>;
|
|
36
50
|
topDuplicates: Array<{
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
files: Array<{
|
|
52
|
+
path: string;
|
|
53
|
+
startLine: number;
|
|
54
|
+
endLine: number;
|
|
55
|
+
}>;
|
|
39
56
|
similarity: number;
|
|
40
57
|
patternType: PatternType;
|
|
41
58
|
tokenCost: number;
|
package/dist/index.js
CHANGED
|
@@ -80,6 +80,7 @@ function extractCodeBlocks(content, minLines) {
|
|
|
80
80
|
blocks.push({
|
|
81
81
|
content: blockContent,
|
|
82
82
|
startLine: blockStart + 1,
|
|
83
|
+
endLine: i + 1,
|
|
83
84
|
patternType: categorizePattern(blockContent),
|
|
84
85
|
linesOfCode
|
|
85
86
|
});
|
|
@@ -95,47 +96,198 @@ function extractCodeBlocks(content, minLines) {
|
|
|
95
96
|
function normalizeCode(code) {
|
|
96
97
|
return code.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/"[^"]*"/g, '"STR"').replace(/'[^']*'/g, "'STR'").replace(/`[^`]*`/g, "`STR`").replace(/\b\d+\b/g, "NUM").replace(/\s+/g, " ").trim();
|
|
97
98
|
}
|
|
98
|
-
function
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
function jaccardSimilarity(tokens1, tokens2) {
|
|
100
|
+
const set1 = new Set(tokens1);
|
|
101
|
+
const set2 = new Set(tokens2);
|
|
102
|
+
let intersection = 0;
|
|
103
|
+
for (const token of set1) {
|
|
104
|
+
if (set2.has(token)) intersection++;
|
|
105
|
+
}
|
|
106
|
+
const union = set1.size + set2.size - intersection;
|
|
107
|
+
return union === 0 ? 0 : intersection / union;
|
|
106
108
|
}
|
|
107
|
-
function detectDuplicatePatterns(files, options) {
|
|
108
|
-
const {
|
|
109
|
+
async function detectDuplicatePatterns(files, options) {
|
|
110
|
+
const {
|
|
111
|
+
minSimilarity,
|
|
112
|
+
minLines,
|
|
113
|
+
batchSize = 100,
|
|
114
|
+
approx = true,
|
|
115
|
+
minSharedTokens = 8,
|
|
116
|
+
maxCandidatesPerBlock = 100,
|
|
117
|
+
streamResults = false
|
|
118
|
+
} = options;
|
|
109
119
|
const duplicates = [];
|
|
120
|
+
const maxComparisons = approx ? Infinity : 5e5;
|
|
110
121
|
const allBlocks = files.flatMap(
|
|
111
122
|
(file) => extractCodeBlocks(file.content, minLines).map((block) => ({
|
|
112
|
-
|
|
123
|
+
content: block.content,
|
|
124
|
+
startLine: block.startLine,
|
|
125
|
+
endLine: block.endLine,
|
|
113
126
|
file: file.file,
|
|
114
127
|
normalized: normalizeCode(block.content),
|
|
115
|
-
|
|
128
|
+
patternType: block.patternType,
|
|
129
|
+
tokenCost: (0, import_core.estimateTokens)(block.content),
|
|
130
|
+
linesOfCode: block.linesOfCode
|
|
116
131
|
}))
|
|
117
132
|
);
|
|
118
133
|
console.log(`Extracted ${allBlocks.length} code blocks for analysis`);
|
|
134
|
+
if (!approx && allBlocks.length > 500) {
|
|
135
|
+
console.log(`\u26A0\uFE0F Using --no-approx mode with ${allBlocks.length} blocks may be slow (O(B\xB2) complexity).`);
|
|
136
|
+
console.log(` Consider using approximate mode (default) for better performance.`);
|
|
137
|
+
}
|
|
138
|
+
const stopwords = /* @__PURE__ */ new Set([
|
|
139
|
+
"return",
|
|
140
|
+
"const",
|
|
141
|
+
"let",
|
|
142
|
+
"var",
|
|
143
|
+
"function",
|
|
144
|
+
"class",
|
|
145
|
+
"new",
|
|
146
|
+
"if",
|
|
147
|
+
"else",
|
|
148
|
+
"for",
|
|
149
|
+
"while",
|
|
150
|
+
"async",
|
|
151
|
+
"await",
|
|
152
|
+
"try",
|
|
153
|
+
"catch",
|
|
154
|
+
"switch",
|
|
155
|
+
"case",
|
|
156
|
+
"default",
|
|
157
|
+
"import",
|
|
158
|
+
"export",
|
|
159
|
+
"from",
|
|
160
|
+
"true",
|
|
161
|
+
"false",
|
|
162
|
+
"null",
|
|
163
|
+
"undefined",
|
|
164
|
+
"this"
|
|
165
|
+
]);
|
|
166
|
+
const tokenize = (norm) => norm.split(/[\s(){}\[\];,\.]+/).filter((t) => t && t.length >= 3 && !stopwords.has(t.toLowerCase()));
|
|
167
|
+
const blockTokens = allBlocks.map((b) => tokenize(b.normalized));
|
|
168
|
+
const invertedIndex = /* @__PURE__ */ new Map();
|
|
169
|
+
if (approx) {
|
|
170
|
+
for (let i = 0; i < blockTokens.length; i++) {
|
|
171
|
+
for (const tok of blockTokens[i]) {
|
|
172
|
+
let arr = invertedIndex.get(tok);
|
|
173
|
+
if (!arr) {
|
|
174
|
+
arr = [];
|
|
175
|
+
invertedIndex.set(tok, arr);
|
|
176
|
+
}
|
|
177
|
+
arr.push(i);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const totalComparisons = approx ? void 0 : allBlocks.length * (allBlocks.length - 1) / 2;
|
|
182
|
+
if (totalComparisons !== void 0) {
|
|
183
|
+
console.log(`Processing ${totalComparisons.toLocaleString()} comparisons in batches...`);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(`Using approximate candidate selection to reduce comparisons...`);
|
|
186
|
+
}
|
|
187
|
+
let comparisonsProcessed = 0;
|
|
188
|
+
let comparisonsBudgetExhausted = false;
|
|
189
|
+
const startTime = Date.now();
|
|
119
190
|
for (let i = 0; i < allBlocks.length; i++) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
linesOfCode: block1.linesOfCode
|
|
136
|
-
});
|
|
191
|
+
if (maxComparisons && comparisonsProcessed >= maxComparisons) {
|
|
192
|
+
comparisonsBudgetExhausted = true;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
if (i % batchSize === 0 && i > 0) {
|
|
196
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
197
|
+
const duplicatesFound = duplicates.length;
|
|
198
|
+
if (totalComparisons !== void 0) {
|
|
199
|
+
const progress = (comparisonsProcessed / totalComparisons * 100).toFixed(1);
|
|
200
|
+
const remaining = totalComparisons - comparisonsProcessed;
|
|
201
|
+
const rate = comparisonsProcessed / parseFloat(elapsed);
|
|
202
|
+
const eta = remaining > 0 ? (remaining / rate).toFixed(0) : 0;
|
|
203
|
+
console.log(` ${progress}% (${comparisonsProcessed.toLocaleString()}/${totalComparisons.toLocaleString()} comparisons, ${elapsed}s elapsed, ~${eta}s remaining, ${duplicatesFound} duplicates)`);
|
|
204
|
+
} else {
|
|
205
|
+
console.log(` Processed ${i.toLocaleString()}/${allBlocks.length} blocks (${elapsed}s elapsed, ${duplicatesFound} duplicates)`);
|
|
137
206
|
}
|
|
207
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
138
208
|
}
|
|
209
|
+
const block1 = allBlocks[i];
|
|
210
|
+
let candidates = null;
|
|
211
|
+
if (approx) {
|
|
212
|
+
const counts = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const tok of blockTokens[i]) {
|
|
214
|
+
const ids = invertedIndex.get(tok);
|
|
215
|
+
if (!ids) continue;
|
|
216
|
+
for (const j of ids) {
|
|
217
|
+
if (j <= i) continue;
|
|
218
|
+
if (allBlocks[j].file === block1.file) continue;
|
|
219
|
+
counts.set(j, (counts.get(j) || 0) + 1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
candidates = Array.from(counts.entries()).filter(([, shared]) => shared >= minSharedTokens).sort((a, b) => b[1] - a[1]).slice(0, maxCandidatesPerBlock).map(([j, shared]) => ({ j, shared }));
|
|
223
|
+
}
|
|
224
|
+
if (approx && candidates) {
|
|
225
|
+
for (const { j } of candidates) {
|
|
226
|
+
if (!approx && maxComparisons !== Infinity && comparisonsProcessed >= maxComparisons) {
|
|
227
|
+
console.log(`\u26A0\uFE0F Comparison safety limit reached (${maxComparisons.toLocaleString()} comparisons in --no-approx mode).`);
|
|
228
|
+
console.log(` This prevents excessive runtime on large repos. Consider using approximate mode (default) or --min-lines to reduce blocks.`);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
comparisonsProcessed++;
|
|
232
|
+
const block2 = allBlocks[j];
|
|
233
|
+
const similarity = jaccardSimilarity(blockTokens[i], blockTokens[j]);
|
|
234
|
+
if (similarity >= minSimilarity) {
|
|
235
|
+
const duplicate = {
|
|
236
|
+
file1: block1.file,
|
|
237
|
+
file2: block2.file,
|
|
238
|
+
line1: block1.startLine,
|
|
239
|
+
line2: block2.startLine,
|
|
240
|
+
endLine1: block1.endLine,
|
|
241
|
+
endLine2: block2.endLine,
|
|
242
|
+
similarity,
|
|
243
|
+
snippet: block1.content.split("\n").slice(0, 5).join("\n") + "\n...",
|
|
244
|
+
patternType: block1.patternType,
|
|
245
|
+
tokenCost: block1.tokenCost + block2.tokenCost,
|
|
246
|
+
linesOfCode: block1.linesOfCode
|
|
247
|
+
};
|
|
248
|
+
duplicates.push(duplicate);
|
|
249
|
+
if (streamResults) {
|
|
250
|
+
console.log(`
|
|
251
|
+
\u2705 Found: ${duplicate.patternType} ${Math.round(similarity * 100)}% similar`);
|
|
252
|
+
console.log(` ${duplicate.file1}:${duplicate.line1}-${duplicate.endLine1} \u21D4 ${duplicate.file2}:${duplicate.line2}-${duplicate.endLine2}`);
|
|
253
|
+
console.log(` Token cost: ${duplicate.tokenCost.toLocaleString()}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
for (let j = i + 1; j < allBlocks.length; j++) {
|
|
259
|
+
if (maxComparisons && comparisonsProcessed >= maxComparisons) break;
|
|
260
|
+
comparisonsProcessed++;
|
|
261
|
+
const block2 = allBlocks[j];
|
|
262
|
+
if (block1.file === block2.file) continue;
|
|
263
|
+
const similarity = jaccardSimilarity(blockTokens[i], blockTokens[j]);
|
|
264
|
+
if (similarity >= minSimilarity) {
|
|
265
|
+
const duplicate = {
|
|
266
|
+
file1: block1.file,
|
|
267
|
+
file2: block2.file,
|
|
268
|
+
line1: block1.startLine,
|
|
269
|
+
line2: block2.startLine,
|
|
270
|
+
endLine1: block1.endLine,
|
|
271
|
+
endLine2: block2.endLine,
|
|
272
|
+
similarity,
|
|
273
|
+
snippet: block1.content.split("\n").slice(0, 5).join("\n") + "\n...",
|
|
274
|
+
patternType: block1.patternType,
|
|
275
|
+
tokenCost: block1.tokenCost + block2.tokenCost,
|
|
276
|
+
linesOfCode: block1.linesOfCode
|
|
277
|
+
};
|
|
278
|
+
duplicates.push(duplicate);
|
|
279
|
+
if (streamResults) {
|
|
280
|
+
console.log(`
|
|
281
|
+
\u2705 Found: ${duplicate.patternType} ${Math.round(similarity * 100)}% similar`);
|
|
282
|
+
console.log(` ${duplicate.file1}:${duplicate.line1}-${duplicate.endLine1} \u21D4 ${duplicate.file2}:${duplicate.line2}-${duplicate.endLine2}`);
|
|
283
|
+
console.log(` Token cost: ${duplicate.tokenCost.toLocaleString()}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (comparisonsBudgetExhausted) {
|
|
290
|
+
console.log(`\u26A0\uFE0F Comparison budget exhausted (${maxComparisons.toLocaleString()} comparisons). Use --max-comparisons to increase.`);
|
|
139
291
|
}
|
|
140
292
|
return duplicates.sort(
|
|
141
293
|
(a, b) => b.similarity - a.similarity || b.tokenCost - a.tokenCost
|
|
@@ -157,7 +309,17 @@ function getRefactoringSuggestion(patternType, similarity) {
|
|
|
157
309
|
return baseMessages[patternType] + urgency;
|
|
158
310
|
}
|
|
159
311
|
async function analyzePatterns(options) {
|
|
160
|
-
const {
|
|
312
|
+
const {
|
|
313
|
+
minSimilarity = 0.4,
|
|
314
|
+
// Jaccard similarity default (40% threshold)
|
|
315
|
+
minLines = 5,
|
|
316
|
+
batchSize = 100,
|
|
317
|
+
approx = true,
|
|
318
|
+
minSharedTokens = 8,
|
|
319
|
+
maxCandidatesPerBlock = 100,
|
|
320
|
+
streamResults = false,
|
|
321
|
+
...scanOptions
|
|
322
|
+
} = options;
|
|
161
323
|
const files = await (0, import_core2.scanFiles)(scanOptions);
|
|
162
324
|
const results = [];
|
|
163
325
|
const fileContents = await Promise.all(
|
|
@@ -166,9 +328,14 @@ async function analyzePatterns(options) {
|
|
|
166
328
|
content: await (0, import_core2.readFileContent)(file)
|
|
167
329
|
}))
|
|
168
330
|
);
|
|
169
|
-
const duplicates = detectDuplicatePatterns(fileContents, {
|
|
331
|
+
const duplicates = await detectDuplicatePatterns(fileContents, {
|
|
170
332
|
minSimilarity,
|
|
171
|
-
minLines
|
|
333
|
+
minLines,
|
|
334
|
+
batchSize,
|
|
335
|
+
approx,
|
|
336
|
+
minSharedTokens,
|
|
337
|
+
maxCandidatesPerBlock,
|
|
338
|
+
streamResults
|
|
172
339
|
});
|
|
173
340
|
for (const file of files) {
|
|
174
341
|
const fileDuplicates = duplicates.filter(
|
|
@@ -231,8 +398,21 @@ function generateSummary(results) {
|
|
|
231
398
|
const typeMatch = issue.message.match(/^(\S+(?:-\S+)*) pattern/);
|
|
232
399
|
const fileMatch = issue.message.match(/similar to (.+?) \(/);
|
|
233
400
|
return {
|
|
234
|
-
|
|
235
|
-
|
|
401
|
+
files: [
|
|
402
|
+
{
|
|
403
|
+
path: issue.location.file,
|
|
404
|
+
startLine: issue.location.line,
|
|
405
|
+
endLine: 0
|
|
406
|
+
// Not available from Issue
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
path: fileMatch?.[1] || "unknown",
|
|
410
|
+
startLine: 0,
|
|
411
|
+
// Not available from Issue
|
|
412
|
+
endLine: 0
|
|
413
|
+
// Not available from Issue
|
|
414
|
+
}
|
|
415
|
+
],
|
|
236
416
|
similarity: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
|
|
237
417
|
patternType: typeMatch?.[1] || "unknown",
|
|
238
418
|
tokenCost: tokenMatch ? parseInt(tokenMatch[1]) : 0
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/pattern-detect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Semantic duplicate pattern detection for AI-generated code - finds similar implementations that waste AI context tokens",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
"import": "./dist/index.mjs"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts",
|
|
20
|
+
"dev": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --watch",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"lint": "eslint src",
|
|
23
|
+
"clean": "rm -rf dist"
|
|
24
|
+
},
|
|
18
25
|
"keywords": [
|
|
19
26
|
"aiready",
|
|
20
27
|
"duplicate-detection",
|
|
@@ -43,9 +50,9 @@
|
|
|
43
50
|
"url": "https://github.com/caopengau/aiready-pattern-detect/issues"
|
|
44
51
|
},
|
|
45
52
|
"dependencies": {
|
|
53
|
+
"@aiready/core": "workspace:*",
|
|
46
54
|
"commander": "^12.1.0",
|
|
47
|
-
"chalk": "^5.3.0"
|
|
48
|
-
"@aiready/core": "0.1.2"
|
|
55
|
+
"chalk": "^5.3.0"
|
|
49
56
|
},
|
|
50
57
|
"devDependencies": {
|
|
51
58
|
"tsup": "^8.3.5",
|
|
@@ -62,12 +69,5 @@
|
|
|
62
69
|
},
|
|
63
70
|
"publishConfig": {
|
|
64
71
|
"access": "public"
|
|
65
|
-
},
|
|
66
|
-
"scripts": {
|
|
67
|
-
"build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts",
|
|
68
|
-
"dev": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --watch",
|
|
69
|
-
"test": "vitest run",
|
|
70
|
-
"lint": "eslint src",
|
|
71
|
-
"clean": "rm -rf dist"
|
|
72
72
|
}
|
|
73
|
-
}
|
|
73
|
+
}
|
package/dist/chunk-K5O2HVB5.mjs
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
import { scanFiles, readFileContent } from "@aiready/core";
|
|
3
|
-
|
|
4
|
-
// src/detector.ts
|
|
5
|
-
import { similarityScore } from "@aiready/core";
|
|
6
|
-
function extractCodeBlocks(content, minLines) {
|
|
7
|
-
const lines = content.split("\n");
|
|
8
|
-
const blocks = [];
|
|
9
|
-
let currentBlock = [];
|
|
10
|
-
let blockStart = 0;
|
|
11
|
-
let braceDepth = 0;
|
|
12
|
-
for (let i = 0; i < lines.length; i++) {
|
|
13
|
-
const line = lines[i];
|
|
14
|
-
for (const char of line) {
|
|
15
|
-
if (char === "{") braceDepth++;
|
|
16
|
-
if (char === "}") braceDepth--;
|
|
17
|
-
}
|
|
18
|
-
currentBlock.push(line);
|
|
19
|
-
if (braceDepth === 0 && currentBlock.length >= minLines) {
|
|
20
|
-
blocks.push({
|
|
21
|
-
content: currentBlock.join("\n"),
|
|
22
|
-
startLine: blockStart + 1
|
|
23
|
-
});
|
|
24
|
-
currentBlock = [];
|
|
25
|
-
blockStart = i + 1;
|
|
26
|
-
} else if (braceDepth === 0) {
|
|
27
|
-
currentBlock = [];
|
|
28
|
-
blockStart = i + 1;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return blocks;
|
|
32
|
-
}
|
|
33
|
-
function normalizeCode(code) {
|
|
34
|
-
return code.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
35
|
-
}
|
|
36
|
-
function detectDuplicatePatterns(files, options) {
|
|
37
|
-
const { minSimilarity, minLines } = options;
|
|
38
|
-
const duplicates = [];
|
|
39
|
-
const allBlocks = files.flatMap(
|
|
40
|
-
(file) => extractCodeBlocks(file.content, minLines).map((block) => ({
|
|
41
|
-
...block,
|
|
42
|
-
file: file.file,
|
|
43
|
-
normalized: normalizeCode(block.content)
|
|
44
|
-
}))
|
|
45
|
-
);
|
|
46
|
-
for (let i = 0; i < allBlocks.length; i++) {
|
|
47
|
-
for (let j = i + 1; j < allBlocks.length; j++) {
|
|
48
|
-
const block1 = allBlocks[i];
|
|
49
|
-
const block2 = allBlocks[j];
|
|
50
|
-
if (block1.file === block2.file) continue;
|
|
51
|
-
const similarity = similarityScore(block1.normalized, block2.normalized);
|
|
52
|
-
if (similarity >= minSimilarity) {
|
|
53
|
-
duplicates.push({
|
|
54
|
-
file1: block1.file,
|
|
55
|
-
file2: block2.file,
|
|
56
|
-
line1: block1.startLine,
|
|
57
|
-
line2: block2.startLine,
|
|
58
|
-
similarity,
|
|
59
|
-
snippet: block1.content.split("\n").slice(0, 3).join("\n") + "..."
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return duplicates.sort((a, b) => b.similarity - a.similarity);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// src/index.ts
|
|
68
|
-
async function analyzePatterns(options) {
|
|
69
|
-
const {
|
|
70
|
-
minSimilarity = 0.85,
|
|
71
|
-
minLines = 5,
|
|
72
|
-
...scanOptions
|
|
73
|
-
} = options;
|
|
74
|
-
const files = await scanFiles(scanOptions);
|
|
75
|
-
const results = [];
|
|
76
|
-
const fileContents = await Promise.all(
|
|
77
|
-
files.map(async (file) => ({
|
|
78
|
-
file,
|
|
79
|
-
content: await readFileContent(file)
|
|
80
|
-
}))
|
|
81
|
-
);
|
|
82
|
-
const duplicates = detectDuplicatePatterns(fileContents, {
|
|
83
|
-
minSimilarity,
|
|
84
|
-
minLines
|
|
85
|
-
});
|
|
86
|
-
for (const file of files) {
|
|
87
|
-
const fileDuplicates = duplicates.filter(
|
|
88
|
-
(dup) => dup.file1 === file || dup.file2 === file
|
|
89
|
-
);
|
|
90
|
-
const issues = fileDuplicates.map((dup) => ({
|
|
91
|
-
type: "duplicate-pattern",
|
|
92
|
-
severity: dup.similarity > 0.95 ? "critical" : "major",
|
|
93
|
-
message: `Similar pattern found in ${dup.file1 === file ? dup.file2 : dup.file1}`,
|
|
94
|
-
location: {
|
|
95
|
-
file,
|
|
96
|
-
line: dup.file1 === file ? dup.line1 : dup.line2
|
|
97
|
-
},
|
|
98
|
-
suggestion: "Consider extracting common logic into a shared utility"
|
|
99
|
-
}));
|
|
100
|
-
results.push({
|
|
101
|
-
fileName: file,
|
|
102
|
-
issues,
|
|
103
|
-
metrics: {
|
|
104
|
-
consistencyScore: 1 - fileDuplicates.length * 0.1
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
return results;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export {
|
|
112
|
-
detectDuplicatePatterns,
|
|
113
|
-
analyzePatterns
|
|
114
|
-
};
|