omp-plugin-duplicate-detector 0.2.0 → 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.
@@ -971,6 +971,7 @@ var require_ignore = __commonJS(function(exports, module) {
971
971
 
972
972
  // src/detector-worker.ts
973
973
  import * as crypto3 from "crypto";
974
+ import * as fsSync3 from "fs";
974
975
  import * as fs3 from "fs/promises";
975
976
  import * as path5 from "path";
976
977
 
@@ -13074,6 +13075,7 @@ class DiskCacheManager {
13074
13075
  #getStmt = null;
13075
13076
  #saveStmt = null;
13076
13077
  #deleteStmt = null;
13078
+ #deleteByRelPathStmt = null;
13077
13079
  #totalSizeStmt = null;
13078
13080
  #oldestShardsStmt = null;
13079
13081
  #deleteAllStmt = null;
@@ -13135,6 +13137,7 @@ class DiskCacheManager {
13135
13137
  mtime = excluded.mtime
13136
13138
  `);
13137
13139
  this.#deleteStmt = db.prepare("DELETE FROM shards WHERE rel_path = ?1 AND content_hash = ?2");
13140
+ this.#deleteByRelPathStmt = db.prepare("DELETE FROM shards WHERE rel_path = ?1");
13138
13141
  this.#totalSizeStmt = db.prepare("SELECT COALESCE(SUM(LENGTH(payload)), 0) as total FROM shards");
13139
13142
  this.#oldestShardsStmt = db.prepare("SELECT rowid, LENGTH(payload) as size FROM shards ORDER BY mtime ASC LIMIT ?1");
13140
13143
  this.#deleteAllStmt = db.prepare("DELETE FROM shards");
@@ -13189,6 +13192,15 @@ class DiskCacheManager {
13189
13192
  this.#deleteStmt.run(normalizedRelPath, contentHash);
13190
13193
  } catch {}
13191
13194
  }
13195
+ async deleteByRelPath(relPath) {
13196
+ try {
13197
+ const db = this.#getDb();
13198
+ if (!db || !this.#deleteByRelPathStmt)
13199
+ return;
13200
+ const normalizedRelPath = relPath.replace(/\\/g, "/");
13201
+ this.#deleteByRelPathStmt.run(normalizedRelPath);
13202
+ } catch {}
13203
+ }
13192
13204
  async saveShards(items) {
13193
13205
  if (items.length === 0)
13194
13206
  return;
@@ -13273,6 +13285,7 @@ class DiskCacheManager {
13273
13285
  this.#getStmt = null;
13274
13286
  this.#saveStmt = null;
13275
13287
  this.#deleteStmt = null;
13288
+ this.#deleteByRelPathStmt = null;
13276
13289
  this.#totalSizeStmt = null;
13277
13290
  this.#oldestShardsStmt = null;
13278
13291
  this.#deleteAllStmt = null;
@@ -13302,11 +13315,18 @@ import * as fs2 from "fs/promises";
13302
13315
  import * as path4 from "path";
13303
13316
  function canonicalizePath(targetPath) {
13304
13317
  const resolved = path4.resolve(targetPath);
13305
- try {
13306
- if (fsSync2.existsSync(resolved)) {
13307
- return fsSync2.realpathSync(resolved);
13308
- }
13309
- } catch {}
13318
+ let current = resolved;
13319
+ let suffix = "";
13320
+ while (current && current !== path4.dirname(current)) {
13321
+ try {
13322
+ if (fsSync2.existsSync(current)) {
13323
+ const real = fsSync2.realpathSync(current);
13324
+ return suffix ? path4.join(real, suffix) : real;
13325
+ }
13326
+ } catch {}
13327
+ suffix = suffix ? path4.join(path4.basename(current), suffix) : path4.basename(current);
13328
+ current = path4.dirname(current);
13329
+ }
13310
13330
  return resolved;
13311
13331
  }
13312
13332
  function isOmpWorktreePath(targetPath) {
@@ -13481,6 +13501,56 @@ function areOptionsEqual(a, b) {
13481
13501
  const bFormats = b.formatsExts ? JSON.stringify(b.formatsExts) : "";
13482
13502
  return aFormats === bFormats;
13483
13503
  }
13504
+ function filterAndEvictStaleClones(clones, activeFilePath) {
13505
+ if (!currentRootDir || !fsSync3.existsSync(currentRootDir)) {
13506
+ return clones;
13507
+ }
13508
+ const validClones = [];
13509
+ const evictedSources = new Set;
13510
+ const activeRes = activeFilePath ? path5.resolve(activeFilePath) : null;
13511
+ const activeCan = activeFilePath ? canonicalizePath(activeFilePath) : null;
13512
+ const isActive = (src) => {
13513
+ if (!activeFilePath)
13514
+ return false;
13515
+ if (src === activeFilePath)
13516
+ return true;
13517
+ if (activeRes && path5.resolve(src) === activeRes)
13518
+ return true;
13519
+ if (activeCan && canonicalizePath(src) === activeCan)
13520
+ return true;
13521
+ return false;
13522
+ };
13523
+ const isStaleSource = (src) => {
13524
+ if (isActive(src))
13525
+ return false;
13526
+ if (evictedSources.has(src))
13527
+ return true;
13528
+ const can = canonicalizePath(path5.isAbsolute(src) ? src : path5.resolve(currentRootDir, src));
13529
+ const rel = path5.relative(currentRootDir, can);
13530
+ const isInside = !rel.startsWith("..") && !path5.isAbsolute(rel);
13531
+ if (isInside && !fsSync3.existsSync(can)) {
13532
+ evictedSources.add(src);
13533
+ currentIndex.removeSource(src);
13534
+ watchedRevisions.delete(src);
13535
+ watchedRevisions.delete(can);
13536
+ watchedRevisions.delete(path5.resolve(src));
13537
+ if (currentDiskCache) {
13538
+ currentDiskCache.deleteByRelPath(rel).catch(() => {});
13539
+ }
13540
+ return true;
13541
+ }
13542
+ return false;
13543
+ };
13544
+ for (const clone2 of clones) {
13545
+ const srcA = clone2.duplicationA.sourceId;
13546
+ const srcB = clone2.duplicationB.sourceId;
13547
+ if (isStaleSource(srcA) || isStaleSource(srcB)) {
13548
+ continue;
13549
+ }
13550
+ validClones.push(clone2);
13551
+ }
13552
+ return validClones;
13553
+ }
13484
13554
  function notifyLateFindings(clones) {
13485
13555
  for (const clone2 of clones) {
13486
13556
  const srcA = clone2.duplicationA.sourceId;
@@ -13888,16 +13958,18 @@ async function handleWorkerRequest(msg) {
13888
13958
  }
13889
13959
  case "checkSnippet": {
13890
13960
  const { filePath, content, format } = msg.payload;
13891
- const clones = currentIndex.checkSnippet(filePath, content, format);
13961
+ const rawClones = currentIndex.checkSnippet(filePath, content, format);
13962
+ const clones = filterAndEvictStaleClones(rawClones, filePath);
13892
13963
  self.postMessage(createSuccessResponse(msg.id, clones));
13893
13964
  break;
13894
13965
  }
13895
13966
  case "checkAndUpdate": {
13896
13967
  const { filePath, content, format, revision = 1 } = msg.payload;
13897
- const clones = currentIndex.updateSource(filePath, content, format);
13968
+ const rawClones = currentIndex.updateSource(filePath, content, format);
13898
13969
  const canonicalFilePath = canonicalizePath(filePath);
13899
13970
  const resolvedPath = path5.resolve(filePath);
13900
13971
  cacheSourceShard(filePath, content);
13972
+ const clones = filterAndEvictStaleClones(rawClones, filePath);
13901
13973
  const fileClones = clones.filter((c) => c.duplicationA.sourceId === filePath || c.duplicationB.sourceId === filePath || path5.resolve(c.duplicationA.sourceId) === resolvedPath || path5.resolve(c.duplicationB.sourceId) === resolvedPath || canonicalizePath(c.duplicationA.sourceId) === canonicalFilePath || canonicalizePath(c.duplicationB.sourceId) === canonicalFilePath);
13902
13974
  const watchEntry = {
13903
13975
  revision,
@@ -13914,14 +13986,19 @@ async function handleWorkerRequest(msg) {
13914
13986
  }
13915
13987
  case "updateFile": {
13916
13988
  const { filePath, content, format } = msg.payload;
13917
- const clones = currentIndex.updateSource(filePath, content, format);
13989
+ const rawClones = currentIndex.updateSource(filePath, content, format);
13918
13990
  cacheSourceShard(filePath, content);
13991
+ const clones = filterAndEvictStaleClones(rawClones, filePath);
13919
13992
  self.postMessage(createSuccessResponse(msg.id, { clones }));
13920
13993
  break;
13921
13994
  }
13922
13995
  case "removeFile": {
13923
13996
  const { filePath } = msg.payload;
13924
13997
  currentIndex.removeSource(filePath);
13998
+ if (currentDiskCache) {
13999
+ const relPath = path5.isAbsolute(filePath) && currentRootDir ? path5.relative(currentRootDir, filePath) : filePath;
14000
+ currentDiskCache.deleteByRelPath(relPath).catch(() => {});
14001
+ }
13925
14002
  self.postMessage(createSuccessResponse(msg.id, { removed: true }));
13926
14003
  break;
13927
14004
  }
@@ -13937,6 +14014,10 @@ async function handleWorkerRequest(msg) {
13937
14014
  } else {
13938
14015
  if (!getSupportedCodeFormat(fileEntry.filePath, currentOptions?.formatsExts)) {
13939
14016
  currentIndex.removeSource(fileEntry.filePath);
14017
+ if (currentDiskCache) {
14018
+ const relPath = path5.isAbsolute(fileEntry.filePath) && currentRootDir ? path5.relative(currentRootDir, fileEntry.filePath) : fileEntry.filePath;
14019
+ currentDiskCache.deleteByRelPath(relPath).catch(() => {});
14020
+ }
13940
14021
  continue;
13941
14022
  }
13942
14023
  const stat2 = await fs3.stat(fileEntry.filePath);
@@ -13951,6 +14032,10 @@ async function handleWorkerRequest(msg) {
13951
14032
  }
13952
14033
  } catch {
13953
14034
  currentIndex.removeSource(fileEntry.filePath);
14035
+ if (currentDiskCache) {
14036
+ const relPath = path5.isAbsolute(fileEntry.filePath) && currentRootDir ? path5.relative(currentRootDir, fileEntry.filePath) : fileEntry.filePath;
14037
+ currentDiskCache.deleteByRelPath(relPath).catch(() => {});
14038
+ }
13954
14039
  }
13955
14040
  }
13956
14041
  self.postMessage(createSuccessResponse(msg.id, { reconciledCount }));
@@ -13999,7 +14084,7 @@ async function handleWorkerRequest(msg) {
13999
14084
  clones = currentIndex.getClones();
14000
14085
  }
14001
14086
  } else {
14002
- clones = currentIndex.getClones();
14087
+ clones = filterAndEvictStaleClones(currentIndex.getClones());
14003
14088
  }
14004
14089
  self.postMessage(createSuccessResponse(msg.id, clones));
14005
14090
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-plugin-duplicate-detector",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Code clone and duplication detector plugin for oh-my-pi powered by jscpd",
5
5
  "type": "module",
6
6
  "files": [
@@ -5,6 +5,7 @@
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";
@@ -88,6 +89,70 @@ function areOptionsEqual(a?: WorkspaceOptions, b?: WorkspaceOptions): boolean {
88
89
  const bFormats = b.formatsExts ? JSON.stringify(b.formatsExts) : "";
89
90
  return aFormats === bFormats;
90
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
+ }
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;
@@ -96,7 +161,6 @@ function notifyLateFindings(clones: IClone[]): void {
96
161
  const resB = path.resolve(srcB);
97
162
  const canA = canonicalizePath(srcA);
98
163
  const canB = canonicalizePath(srcB);
99
-
100
164
  const isWatchedA =
101
165
  watchedRevisions.has(srcA) ||
102
166
  watchedRevisions.has(resA) ||
@@ -659,19 +723,22 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
659
723
 
660
724
  case "checkSnippet": {
661
725
  const { filePath, content, format } = msg.payload;
662
- const clones = currentIndex.checkSnippet(filePath, content, format);
726
+ const rawClones = currentIndex.checkSnippet(filePath, content, format);
727
+ const clones = filterAndEvictStaleClones(rawClones, filePath);
663
728
  self.postMessage(createSuccessResponse(msg.id, clones));
664
729
  break;
665
730
  }
666
731
 
667
732
  case "checkAndUpdate": {
668
733
  const { filePath, content, format, revision = 1 } = msg.payload;
669
- const clones = currentIndex.updateSource(filePath, content, format);
734
+ const rawClones = currentIndex.updateSource(filePath, content, format);
670
735
  const canonicalFilePath = canonicalizePath(filePath);
671
736
  const resolvedPath = path.resolve(filePath);
672
737
 
673
738
  cacheSourceShard(filePath, content);
674
739
 
740
+ const clones = filterAndEvictStaleClones(rawClones, filePath);
741
+
675
742
  const fileClones = clones.filter(
676
743
  (c) =>
677
744
  c.duplicationA.sourceId === filePath ||
@@ -701,8 +768,9 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
701
768
 
702
769
  case "updateFile": {
703
770
  const { filePath, content, format } = msg.payload;
704
- const clones = currentIndex.updateSource(filePath, content, format);
771
+ const rawClones = currentIndex.updateSource(filePath, content, format);
705
772
  cacheSourceShard(filePath, content);
773
+ const clones = filterAndEvictStaleClones(rawClones, filePath);
706
774
  self.postMessage(createSuccessResponse(msg.id, { clones }));
707
775
  break;
708
776
  }
@@ -710,6 +778,13 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
710
778
  case "removeFile": {
711
779
  const { filePath } = msg.payload;
712
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
+ }
713
788
  self.postMessage(createSuccessResponse(msg.id, { removed: true }));
714
789
  break;
715
790
  }
@@ -732,6 +807,13 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
732
807
  )
733
808
  ) {
734
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
+ }
735
817
  continue;
736
818
  }
737
819
  const stat = await fs.stat(fileEntry.filePath);
@@ -745,8 +827,15 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
745
827
  }
746
828
  }
747
829
  } catch {
748
- // 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
749
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
+ }
750
839
  }
751
840
  }
752
841
 
@@ -803,7 +892,7 @@ async function handleWorkerRequest(msg: WorkerRequestMessage): Promise<void> {
803
892
  clones = currentIndex.getClones();
804
893
  }
805
894
  } else {
806
- clones = currentIndex.getClones();
895
+ clones = filterAndEvictStaleClones(currentIndex.getClones());
807
896
  }
808
897
 
809
898
  self.postMessage(createSuccessResponse(msg.id, clones));
package/src/disk-cache.ts CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  import type { WorkspaceOptions } from "./worker-protocol";
22
22
 
23
23
  const DEFAULT_MAX_CACHE_BYTES = 250 * 1024 * 1024; // 250 MB
24
- export const TOKENIZER_CACHE_VERSION = "4.0";
24
+ const TOKENIZER_CACHE_VERSION = "4.0";
25
25
 
26
26
  export interface DiskCacheOptions {
27
27
  /** Root directory of the workspace */
@@ -389,6 +389,7 @@ export class DiskCacheManager {
389
389
  #getStmt: Statement | null = null;
390
390
  #saveStmt: Statement | null = null;
391
391
  #deleteStmt: Statement | null = null;
392
+ #deleteByRelPathStmt: Statement | null = null;
392
393
  #totalSizeStmt: Statement | null = null;
393
394
  #oldestShardsStmt: Statement | null = null;
394
395
  #deleteAllStmt: Statement | null = null;
@@ -473,6 +474,9 @@ export class DiskCacheManager {
473
474
  this.#deleteStmt = db.prepare(
474
475
  "DELETE FROM shards WHERE rel_path = ?1 AND content_hash = ?2",
475
476
  );
477
+ this.#deleteByRelPathStmt = db.prepare(
478
+ "DELETE FROM shards WHERE rel_path = ?1",
479
+ );
476
480
  this.#totalSizeStmt = db.prepare(
477
481
  "SELECT COALESCE(SUM(LENGTH(payload)), 0) as total FROM shards",
478
482
  );
@@ -587,6 +591,19 @@ export class DiskCacheManager {
587
591
  // Fail open
588
592
  }
589
593
  }
594
+ /**
595
+ * Deletes all cached shards for a given relPath regardless of contentHash.
596
+ */
597
+ async deleteByRelPath(relPath: string): Promise<void> {
598
+ try {
599
+ const db = this.#getDb();
600
+ if (!db || !this.#deleteByRelPathStmt) return;
601
+ const normalizedRelPath = relPath.replace(/\\/g, "/");
602
+ this.#deleteByRelPathStmt.run(normalizedRelPath);
603
+ } catch {
604
+ // Fail open
605
+ }
606
+ }
590
607
 
591
608
  /**
592
609
  * Atomically saves a batch of pre-tokenized shards inside a single SQLite transaction.
@@ -721,6 +738,7 @@ export class DiskCacheManager {
721
738
  this.#getStmt = null;
722
739
  this.#saveStmt = null;
723
740
  this.#deleteStmt = null;
741
+ this.#deleteByRelPathStmt = null;
724
742
  this.#totalSizeStmt = null;
725
743
  this.#oldestShardsStmt = null;
726
744
  this.#deleteAllStmt = null;
package/src/index.ts CHANGED
@@ -394,7 +394,7 @@ export default function duplicateDetectorExtension(pi: ExtensionAPI): void {
394
394
  }
395
395
  }
396
396
  const { stdout: statusOut } = await execGit(
397
- ["status", "--porcelain", "-z", "--untracked-files=no", "--", "."],
397
+ ["status", "--porcelain", "-z", "-uall", "--", "."],
398
398
  cwd,
399
399
  ).catch(() => ({ stdout: "" }));
400
400
 
@@ -26,12 +26,21 @@ export interface RepositoryContext {
26
26
  */
27
27
  export function canonicalizePath(targetPath: string): string {
28
28
  const resolved = path.resolve(targetPath);
29
- try {
30
- if (fsSync.existsSync(resolved)) {
31
- return fsSync.realpathSync(resolved);
29
+ let current = resolved;
30
+ let suffix = "";
31
+ while (current && current !== path.dirname(current)) {
32
+ try {
33
+ if (fsSync.existsSync(current)) {
34
+ const real = fsSync.realpathSync(current);
35
+ return suffix ? path.join(real, suffix) : real;
36
+ }
37
+ } catch {
38
+ // Fall back to walking up
32
39
  }
33
- } catch {
34
- // Fall back to resolved
40
+ suffix = suffix
41
+ ? path.join(path.basename(current), suffix)
42
+ : path.basename(current);
43
+ current = path.dirname(current);
35
44
  }
36
45
  return resolved;
37
46
  }