omp-plugin-duplicate-detector 0.1.1 → 0.2.1
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 +1 -1
- package/dist/detector-worker.js +507 -276
- package/package.json +1 -1
- package/src/coordinator.ts +7 -3
- package/src/detector-worker.ts +156 -28
- package/src/disk-cache.ts +383 -314
- package/src/index.ts +134 -7
- package/src/project-state.ts +10 -4
- package/src/repo-context.ts +167 -0
- package/src/source-aware-index.ts +14 -1
package/package.json
CHANGED
package/src/coordinator.ts
CHANGED
|
@@ -473,15 +473,19 @@ export class DuplicateDetectorCoordinator extends EventEmitter<CoordinatorEvents
|
|
|
473
473
|
/**
|
|
474
474
|
* Reconcile multiple modified or removed files with the index.
|
|
475
475
|
*/
|
|
476
|
-
async reconcile(
|
|
476
|
+
async reconcile(
|
|
477
|
+
files: ReconcileFileEntry[],
|
|
478
|
+
): Promise<{ reconciledCount: number }> {
|
|
477
479
|
try {
|
|
478
480
|
const payload: ReconcilePayload = { files };
|
|
479
|
-
await this.#sendRequest<{ reconciledCount: number }>(
|
|
481
|
+
return await this.#sendRequest<{ reconciledCount: number }>(
|
|
480
482
|
"reconcile",
|
|
481
483
|
payload,
|
|
482
484
|
);
|
|
483
485
|
} catch (err) {
|
|
484
|
-
|
|
486
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
487
|
+
this.emit("error", error);
|
|
488
|
+
throw error;
|
|
485
489
|
}
|
|
486
490
|
}
|
|
487
491
|
|
package/src/detector-worker.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as crypto from "node:crypto";
|
|
8
|
+
import * as fsSync from "node:fs";
|
|
8
9
|
import * as fs from "node:fs/promises";
|
|
9
10
|
import * as path from "node:path";
|
|
10
11
|
import type { IClone } from "@jscpd/core";
|
|
11
|
-
import { DiskCacheManager } from "./disk-cache";
|
|
12
|
+
import { cleanupLegacyCacheFiles, DiskCacheManager } from "./disk-cache";
|
|
12
13
|
import {
|
|
13
14
|
type BaselineStatus,
|
|
14
15
|
createIgnoreFilter,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
MAX_INDEXED_FILES,
|
|
23
24
|
MAX_TOTAL_SOURCE_BYTES,
|
|
24
25
|
} from "./jscpd-engine";
|
|
26
|
+
import { canonicalizePath, resolveRepositoryContext } from "./repo-context";
|
|
25
27
|
import {
|
|
26
28
|
type SerializedSourceShard,
|
|
27
29
|
SourceAwareCloneIndex,
|
|
@@ -87,24 +89,100 @@ function areOptionsEqual(a?: WorkspaceOptions, b?: WorkspaceOptions): boolean {
|
|
|
87
89
|
const bFormats = b.formatsExts ? JSON.stringify(b.formatsExts) : "";
|
|
88
90
|
return aFormats === bFormats;
|
|
89
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Verifies that source files involved in detected clones exist on disk.
|
|
94
|
+
* If an indexed repository source has been deleted or moved, it is evicted from currentIndex,
|
|
95
|
+
* disk cache, and watchedRevisions, and the stale clone is filtered out.
|
|
96
|
+
*
|
|
97
|
+
* activeFilePath: the file currently being queried or updated in memory (exempt from disk check).
|
|
98
|
+
*/
|
|
99
|
+
function filterAndEvictStaleClones(
|
|
100
|
+
clones: IClone[],
|
|
101
|
+
activeFilePath?: string,
|
|
102
|
+
): IClone[] {
|
|
103
|
+
if (!currentRootDir || !fsSync.existsSync(currentRootDir)) {
|
|
104
|
+
return clones;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const validClones: IClone[] = [];
|
|
108
|
+
const evictedSources = new Set<string>();
|
|
109
|
+
const activeRes = activeFilePath ? path.resolve(activeFilePath) : null;
|
|
110
|
+
const activeCan = activeFilePath ? canonicalizePath(activeFilePath) : null;
|
|
111
|
+
|
|
112
|
+
const isActive = (src: string): boolean => {
|
|
113
|
+
if (!activeFilePath) return false;
|
|
114
|
+
if (src === activeFilePath) return true;
|
|
115
|
+
if (activeRes && path.resolve(src) === activeRes) return true;
|
|
116
|
+
if (activeCan && canonicalizePath(src) === activeCan) return true;
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const isStaleSource = (src: string): boolean => {
|
|
121
|
+
if (isActive(src)) return false;
|
|
122
|
+
if (evictedSources.has(src)) return true;
|
|
123
|
+
|
|
124
|
+
const can = canonicalizePath(
|
|
125
|
+
path.isAbsolute(src) ? src : path.resolve(currentRootDir, src),
|
|
126
|
+
);
|
|
127
|
+
const rel = path.relative(currentRootDir, can);
|
|
128
|
+
const isInside = !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
129
|
+
if (isInside && !fsSync.existsSync(can)) {
|
|
130
|
+
evictedSources.add(src);
|
|
131
|
+
currentIndex.removeSource(src);
|
|
132
|
+
watchedRevisions.delete(src);
|
|
133
|
+
watchedRevisions.delete(can);
|
|
134
|
+
watchedRevisions.delete(path.resolve(src));
|
|
135
|
+
if (currentDiskCache) {
|
|
136
|
+
currentDiskCache.deleteByRelPath(rel).catch(() => {});
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
for (const clone of clones) {
|
|
144
|
+
const srcA = clone.duplicationA.sourceId;
|
|
145
|
+
const srcB = clone.duplicationB.sourceId;
|
|
146
|
+
|
|
147
|
+
if (isStaleSource(srcA) || isStaleSource(srcB)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
validClones.push(clone);
|
|
152
|
+
}
|
|
90
153
|
|
|
154
|
+
return validClones;
|
|
155
|
+
}
|
|
91
156
|
function notifyLateFindings(clones: IClone[]): void {
|
|
92
157
|
for (const clone of clones) {
|
|
93
158
|
const srcA = clone.duplicationA.sourceId;
|
|
94
159
|
const srcB = clone.duplicationB.sourceId;
|
|
95
160
|
const resA = path.resolve(srcA);
|
|
96
161
|
const resB = path.resolve(srcB);
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
162
|
+
const canA = canonicalizePath(srcA);
|
|
163
|
+
const canB = canonicalizePath(srcB);
|
|
164
|
+
const isWatchedA =
|
|
165
|
+
watchedRevisions.has(srcA) ||
|
|
166
|
+
watchedRevisions.has(resA) ||
|
|
167
|
+
watchedRevisions.has(canA);
|
|
168
|
+
const isWatchedB =
|
|
169
|
+
watchedRevisions.has(srcB) ||
|
|
170
|
+
watchedRevisions.has(resB) ||
|
|
171
|
+
watchedRevisions.has(canB);
|
|
100
172
|
|
|
101
173
|
if (isWatchedA || isWatchedB) {
|
|
102
174
|
if (isWatchedA) {
|
|
103
|
-
const entry =
|
|
175
|
+
const entry =
|
|
176
|
+
watchedRevisions.get(srcA) ??
|
|
177
|
+
watchedRevisions.get(resA) ??
|
|
178
|
+
watchedRevisions.get(canA);
|
|
104
179
|
if (entry) entry.lastKnownCloneCount++;
|
|
105
180
|
}
|
|
106
181
|
if (isWatchedB) {
|
|
107
|
-
const entry =
|
|
182
|
+
const entry =
|
|
183
|
+
watchedRevisions.get(srcB) ??
|
|
184
|
+
watchedRevisions.get(resB) ??
|
|
185
|
+
watchedRevisions.get(canB);
|
|
108
186
|
if (entry) entry.lastKnownCloneCount++;
|
|
109
187
|
}
|
|
110
188
|
self.postMessage(createLateFindingEvent(clone));
|
|
@@ -307,6 +385,10 @@ async function runBaselineIndexing(
|
|
|
307
385
|
if (signal.aborted) return { indexedCount, status: "cancelled" };
|
|
308
386
|
|
|
309
387
|
// Ingest items into index and notify late findings
|
|
388
|
+
const shardsToSave: Array<{
|
|
389
|
+
shard: SerializedSourceShard;
|
|
390
|
+
relPath: string;
|
|
391
|
+
}> = [];
|
|
310
392
|
for (const item of fileItems) {
|
|
311
393
|
if (!item) continue;
|
|
312
394
|
|
|
@@ -323,9 +405,10 @@ async function runBaselineIndexing(
|
|
|
323
405
|
newClones = currentIndex.hydrateSourceShard(item.cachedShard);
|
|
324
406
|
if (item.isNewlyTokenized) {
|
|
325
407
|
if (currentDiskCache && item.contentHash && item.relPath) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
.
|
|
408
|
+
shardsToSave.push({
|
|
409
|
+
shard: item.cachedShard,
|
|
410
|
+
relPath: item.relPath,
|
|
411
|
+
});
|
|
329
412
|
}
|
|
330
413
|
} else {
|
|
331
414
|
cachedCount++;
|
|
@@ -338,7 +421,7 @@ async function runBaselineIndexing(
|
|
|
338
421
|
item.contentHash,
|
|
339
422
|
);
|
|
340
423
|
if (shard) {
|
|
341
|
-
|
|
424
|
+
shardsToSave.push({ shard, relPath: item.relPath });
|
|
342
425
|
}
|
|
343
426
|
}
|
|
344
427
|
}
|
|
@@ -350,6 +433,10 @@ async function runBaselineIndexing(
|
|
|
350
433
|
}
|
|
351
434
|
}
|
|
352
435
|
|
|
436
|
+
if (shardsToSave.length > 0 && currentDiskCache) {
|
|
437
|
+
await currentDiskCache.saveShards(shardsToSave);
|
|
438
|
+
}
|
|
439
|
+
|
|
353
440
|
const processedCount = Math.min(i + batch.length, totalFiles);
|
|
354
441
|
const percentage =
|
|
355
442
|
totalFiles > 0 ? Math.round((processedCount / totalFiles) * 100) : 100;
|
|
@@ -543,11 +630,12 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
543
630
|
switch (msg.type) {
|
|
544
631
|
case "openWorkspace": {
|
|
545
632
|
const { rootDir, options } = msg.payload;
|
|
633
|
+
const canonicalRootDir = canonicalizePath(rootDir);
|
|
546
634
|
|
|
547
|
-
// If
|
|
635
|
+
// If canonicalRootDir === currentRootDir and options match and baseline is already complete or indexing,
|
|
548
636
|
// avoid total reset; trigger an incremental git status check instead.
|
|
549
637
|
if (
|
|
550
|
-
currentRootDir === rootDir &&
|
|
638
|
+
(currentRootDir === canonicalRootDir || currentRootDir === rootDir) &&
|
|
551
639
|
areOptionsEqual(currentOptions, options) &&
|
|
552
640
|
(isBaselineComplete || isBaselineIndexing)
|
|
553
641
|
) {
|
|
@@ -557,14 +645,14 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
557
645
|
}
|
|
558
646
|
activeAbortController = new AbortController();
|
|
559
647
|
const recResult = await runIncrementalGitReconciliation(
|
|
560
|
-
|
|
648
|
+
currentRootDir,
|
|
561
649
|
options,
|
|
562
650
|
activeAbortController.signal,
|
|
563
651
|
);
|
|
564
652
|
self.postMessage(
|
|
565
653
|
createSuccessResponse(msg.id, {
|
|
566
654
|
started: true,
|
|
567
|
-
rootDir,
|
|
655
|
+
rootDir: currentRootDir,
|
|
568
656
|
reused: true,
|
|
569
657
|
indexedCount: recResult.indexedCount,
|
|
570
658
|
status: recResult.status,
|
|
@@ -574,7 +662,7 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
574
662
|
self.postMessage(
|
|
575
663
|
createSuccessResponse(msg.id, {
|
|
576
664
|
started: true,
|
|
577
|
-
rootDir,
|
|
665
|
+
rootDir: currentRootDir,
|
|
578
666
|
reused: true,
|
|
579
667
|
indexedCount: currentIndex.stats().sourceCount,
|
|
580
668
|
status: "complete" as BaselineStatus,
|
|
@@ -594,11 +682,17 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
594
682
|
currentDiskCache = null;
|
|
595
683
|
}
|
|
596
684
|
|
|
597
|
-
|
|
685
|
+
const repoContext = await resolveRepositoryContext(
|
|
686
|
+
rootDir,
|
|
687
|
+
activeAbortController.signal,
|
|
688
|
+
);
|
|
689
|
+
const effectiveRoot = repoContext.workspaceRoot;
|
|
690
|
+
currentRootDir = effectiveRoot;
|
|
598
691
|
currentOptions = options;
|
|
599
692
|
currentIndex = new SourceAwareCloneIndex(options);
|
|
600
693
|
currentDiskCache = new DiskCacheManager({
|
|
601
|
-
rootDir,
|
|
694
|
+
rootDir: effectiveRoot,
|
|
695
|
+
repositoryKey: repoContext.repositoryKey,
|
|
602
696
|
cacheDir: options?.cacheDir,
|
|
603
697
|
config: options,
|
|
604
698
|
maxBytes: options?.maxCacheBytes,
|
|
@@ -607,41 +701,52 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
607
701
|
isBaselineIndexing = true;
|
|
608
702
|
isBaselineComplete = false;
|
|
609
703
|
|
|
704
|
+
// Clean up legacy pre-v4 caches in background
|
|
705
|
+
cleanupLegacyCacheFiles(options?.cacheDir).catch(() => {});
|
|
706
|
+
|
|
610
707
|
self.postMessage(
|
|
611
708
|
createSuccessResponse(msg.id, {
|
|
612
709
|
started: true,
|
|
613
|
-
rootDir,
|
|
710
|
+
rootDir: effectiveRoot,
|
|
614
711
|
reused: false,
|
|
615
712
|
}),
|
|
616
713
|
);
|
|
617
714
|
|
|
618
715
|
// Run background indexing task (posts complete event when finished)
|
|
619
|
-
runBaselineIndexing(
|
|
620
|
-
|
|
621
|
-
|
|
716
|
+
runBaselineIndexing(
|
|
717
|
+
effectiveRoot,
|
|
718
|
+
options,
|
|
719
|
+
activeAbortController.signal,
|
|
720
|
+
).catch(() => {});
|
|
622
721
|
break;
|
|
623
722
|
}
|
|
624
723
|
|
|
625
724
|
case "checkSnippet": {
|
|
626
725
|
const { filePath, content, format } = msg.payload;
|
|
627
|
-
const
|
|
726
|
+
const rawClones = currentIndex.checkSnippet(filePath, content, format);
|
|
727
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
628
728
|
self.postMessage(createSuccessResponse(msg.id, clones));
|
|
629
729
|
break;
|
|
630
730
|
}
|
|
631
731
|
|
|
632
732
|
case "checkAndUpdate": {
|
|
633
733
|
const { filePath, content, format, revision = 1 } = msg.payload;
|
|
634
|
-
const
|
|
734
|
+
const rawClones = currentIndex.updateSource(filePath, content, format);
|
|
735
|
+
const canonicalFilePath = canonicalizePath(filePath);
|
|
635
736
|
const resolvedPath = path.resolve(filePath);
|
|
636
737
|
|
|
637
738
|
cacheSourceShard(filePath, content);
|
|
638
739
|
|
|
740
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
741
|
+
|
|
639
742
|
const fileClones = clones.filter(
|
|
640
743
|
(c) =>
|
|
641
744
|
c.duplicationA.sourceId === filePath ||
|
|
642
745
|
c.duplicationB.sourceId === filePath ||
|
|
643
746
|
path.resolve(c.duplicationA.sourceId) === resolvedPath ||
|
|
644
|
-
path.resolve(c.duplicationB.sourceId) === resolvedPath
|
|
747
|
+
path.resolve(c.duplicationB.sourceId) === resolvedPath ||
|
|
748
|
+
canonicalizePath(c.duplicationA.sourceId) === canonicalFilePath ||
|
|
749
|
+
canonicalizePath(c.duplicationB.sourceId) === canonicalFilePath,
|
|
645
750
|
);
|
|
646
751
|
|
|
647
752
|
const watchEntry = {
|
|
@@ -650,10 +755,11 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
650
755
|
};
|
|
651
756
|
watchedRevisions.set(filePath, watchEntry);
|
|
652
757
|
watchedRevisions.set(resolvedPath, watchEntry);
|
|
758
|
+
watchedRevisions.set(canonicalFilePath, watchEntry);
|
|
653
759
|
|
|
654
760
|
self.postMessage(
|
|
655
761
|
createSuccessResponse(msg.id, {
|
|
656
|
-
clones,
|
|
762
|
+
clones: fileClones,
|
|
657
763
|
isComplete: isBaselineComplete,
|
|
658
764
|
}),
|
|
659
765
|
);
|
|
@@ -662,8 +768,9 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
662
768
|
|
|
663
769
|
case "updateFile": {
|
|
664
770
|
const { filePath, content, format } = msg.payload;
|
|
665
|
-
const
|
|
771
|
+
const rawClones = currentIndex.updateSource(filePath, content, format);
|
|
666
772
|
cacheSourceShard(filePath, content);
|
|
773
|
+
const clones = filterAndEvictStaleClones(rawClones, filePath);
|
|
667
774
|
self.postMessage(createSuccessResponse(msg.id, { clones }));
|
|
668
775
|
break;
|
|
669
776
|
}
|
|
@@ -671,6 +778,13 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
671
778
|
case "removeFile": {
|
|
672
779
|
const { filePath } = msg.payload;
|
|
673
780
|
currentIndex.removeSource(filePath);
|
|
781
|
+
if (currentDiskCache) {
|
|
782
|
+
const relPath =
|
|
783
|
+
path.isAbsolute(filePath) && currentRootDir
|
|
784
|
+
? path.relative(currentRootDir, filePath)
|
|
785
|
+
: filePath;
|
|
786
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
787
|
+
}
|
|
674
788
|
self.postMessage(createSuccessResponse(msg.id, { removed: true }));
|
|
675
789
|
break;
|
|
676
790
|
}
|
|
@@ -693,6 +807,13 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
693
807
|
)
|
|
694
808
|
) {
|
|
695
809
|
currentIndex.removeSource(fileEntry.filePath);
|
|
810
|
+
if (currentDiskCache) {
|
|
811
|
+
const relPath =
|
|
812
|
+
path.isAbsolute(fileEntry.filePath) && currentRootDir
|
|
813
|
+
? path.relative(currentRootDir, fileEntry.filePath)
|
|
814
|
+
: fileEntry.filePath;
|
|
815
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
816
|
+
}
|
|
696
817
|
continue;
|
|
697
818
|
}
|
|
698
819
|
const stat = await fs.stat(fileEntry.filePath);
|
|
@@ -706,8 +827,15 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
706
827
|
}
|
|
707
828
|
}
|
|
708
829
|
} catch {
|
|
709
|
-
// If file cannot be read or no longer exists, remove from index
|
|
830
|
+
// If file cannot be read or no longer exists, remove from index and disk cache
|
|
710
831
|
currentIndex.removeSource(fileEntry.filePath);
|
|
832
|
+
if (currentDiskCache) {
|
|
833
|
+
const relPath =
|
|
834
|
+
path.isAbsolute(fileEntry.filePath) && currentRootDir
|
|
835
|
+
? path.relative(currentRootDir, fileEntry.filePath)
|
|
836
|
+
: fileEntry.filePath;
|
|
837
|
+
currentDiskCache.deleteByRelPath(relPath).catch(() => {});
|
|
838
|
+
}
|
|
711
839
|
}
|
|
712
840
|
}
|
|
713
841
|
|
|
@@ -764,7 +892,7 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
|
|
|
764
892
|
clones = currentIndex.getClones();
|
|
765
893
|
}
|
|
766
894
|
} else {
|
|
767
|
-
clones = currentIndex.getClones();
|
|
895
|
+
clones = filterAndEvictStaleClones(currentIndex.getClones());
|
|
768
896
|
}
|
|
769
897
|
|
|
770
898
|
self.postMessage(createSuccessResponse(msg.id, clones));
|