omp-plugin-duplicate-detector 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,144 @@
1
+ import type { IClone } from "@jscpd/core";
2
+ import { toDisplayPath } from "./tui-notification";
3
+
4
+ /**
5
+ * Unique identifier for a duplicate match.
6
+ */
7
+ export function cloneIdentity(clone: IClone): string {
8
+ const a = clone.duplicationA;
9
+ const b = clone.duplicationB;
10
+ // Use source file + line coordinates + length
11
+ const len = a.end.line - a.start.line + 1;
12
+ return `${b.sourceId}:${b.start.line}-${b.end.line}::${a.sourceId}::${len}`;
13
+ }
14
+
15
+ /**
16
+ * Extract source code lines between start and end lines (1-indexed).
17
+ */
18
+ function extractLineRange(
19
+ content: string,
20
+ startLine: number,
21
+ endLine: number,
22
+ ): string {
23
+ const lines = content.split(/\r?\n/);
24
+ const start = Math.max(0, startLine - 1);
25
+ const end = Math.min(lines.length, endLine);
26
+ return lines.slice(start, end).join("\n");
27
+ }
28
+
29
+ /**
30
+ * Tracks surfaced duplicates per file to prevent repetitive warnings across multi-step edits.
31
+ */
32
+ export class DuplicateLedger {
33
+ readonly #seen = new Map<string, Set<string>>();
34
+
35
+ /**
36
+ * Filter a list of clones, returning only those not yet seen for the target file.
37
+ * Updates the ledger with newly seen identities.
38
+ */
39
+ filterFreshClones(filePath: string, clones: IClone[]): IClone[] {
40
+ const previous = this.#seen.get(filePath);
41
+ const fresh: IClone[] = [];
42
+ const currentIdentities = new Set<string>();
43
+
44
+ for (const clone of clones) {
45
+ const id = cloneIdentity(clone);
46
+ currentIdentities.add(id);
47
+ if (!previous?.has(id)) {
48
+ fresh.push(clone);
49
+ }
50
+ }
51
+
52
+ if (currentIdentities.size === 0) {
53
+ this.#seen.delete(filePath);
54
+ } else {
55
+ this.#seen.set(filePath, currentIdentities);
56
+ }
57
+
58
+ return fresh;
59
+ }
60
+
61
+ /**
62
+ * Format an in-band <system-reminder> XML block for detected clones.
63
+ */
64
+ formatReminder(
65
+ clones: IClone[],
66
+ filePath: string,
67
+ targetFileContent?: string,
68
+ basePath?: string,
69
+ options?: {
70
+ maxClones?: number;
71
+ maxSnippetLines?: number;
72
+ artifactId?: string;
73
+ },
74
+ ): string {
75
+ if (clones.length === 0) return "";
76
+ const displayTargetFile = toDisplayPath(filePath, basePath);
77
+ let reminder = `<system-reminder reason="code_duplication" file="${displayTargetFile}">\n`;
78
+ reminder += `Warning: Duplicated code detected in '${displayTargetFile}'. Consider refactoring into a shared helper or reusing existing logic.\n\n`;
79
+
80
+ const maxClones = options?.maxClones ?? 4;
81
+ const count =
82
+ typeof maxClones === "number" && maxClones > 0
83
+ ? Math.min(clones.length, maxClones)
84
+ : clones.length;
85
+
86
+ const maxSnippetLines = options?.maxSnippetLines ?? 8;
87
+
88
+ for (let i = 0; i < count; i++) {
89
+ const clone = clones[i]!;
90
+ const a = clone.duplicationA;
91
+ const b = clone.duplicationB;
92
+ const linesCount = a.end.line - a.start.line + 1;
93
+ const srcA = toDisplayPath(a.sourceId, basePath);
94
+ const srcB = toDisplayPath(b.sourceId, basePath);
95
+
96
+ reminder += `### Duplicate #${i + 1} (${linesCount} lines, format: ${clone.format})\n`;
97
+ reminder += `- Current change: \`${srcA}:${a.start.line}-${a.end.line}\` (lines ${a.start.line} to ${a.end.line})\n`;
98
+ reminder += `- Pre-existing copy: \`${srcB}:${b.start.line}-${b.end.line}\` (lines ${b.start.line} to ${b.end.line})\n`;
99
+ const rawSnippet =
100
+ a.fragment ||
101
+ (targetFileContent
102
+ ? extractLineRange(targetFileContent, a.start.line, a.end.line)
103
+ : "");
104
+ const snippet = rawSnippet.trim();
105
+ if (snippet) {
106
+ let displaySnippet = snippet;
107
+ if (typeof maxSnippetLines === "number" && maxSnippetLines > 0) {
108
+ const lines = snippet.split(/\r?\n/);
109
+ if (lines.length > maxSnippetLines) {
110
+ displaySnippet = `${lines.slice(0, maxSnippetLines).join("\n")}\n// ... +${lines.length - maxSnippetLines} more duplicate lines`;
111
+ }
112
+ }
113
+ reminder += `\n\`\`\`${clone.format}\n${displaySnippet}\n\`\`\`\n`;
114
+ }
115
+ reminder += `\n`;
116
+ }
117
+
118
+ const omittedCount = clones.length - count;
119
+ if (omittedCount > 0 || options?.artifactId) {
120
+ if (omittedCount > 0) {
121
+ reminder += `*... and ${omittedCount} more duplicate block${omittedCount === 1 ? "" : "s"} in this file.*\n`;
122
+ }
123
+ if (options?.artifactId) {
124
+ reminder += `*Read \`artifact://${options.artifactId}\` for complete duplicate report with all ${clones.length} duplicate blocks.*\n`;
125
+ reminder += `\n[raw output: artifact://${options.artifactId}]\n`;
126
+ }
127
+ reminder += "\n";
128
+ }
129
+
130
+ reminder += `</system-reminder>\n`;
131
+ return reminder;
132
+ }
133
+
134
+ /**
135
+ * Reset ledger tracking for a specific file or the entire session.
136
+ */
137
+ clear(filePath?: string): void {
138
+ if (filePath) {
139
+ this.#seen.delete(filePath);
140
+ } else {
141
+ this.#seen.clear();
142
+ }
143
+ }
144
+ }