dry-ts
Version:
Find candidate duplicate TypeScript code by comparing normalized AST structure.
96 lines (95 loc) • 3.19 kB
JavaScript
// Fingerprint ids are 53-bit structural hashes, so identical subtrees produce the
// same id with no shared state. That keeps id assignment deterministic across
// worker threads; the collision probability is negligible for candidate finding.
export class FingerprintInterner {
tagHashes = new Map();
idFor(tag, childIds) {
const tagHash = this.tagHash(tag);
let h1 = tagHash >>> 0;
let h2 = (tagHash ^ 0x9e3779b9) >>> 0;
for (const childId of childIds) {
const lo = childId >>> 0;
const hi = Math.floor(childId / 0x100000000) >>> 0;
h1 = mix(h1, lo, 0xcc9e2d51, 0x1b873593);
h1 = mix(h1, hi, 0xcc9e2d51, 0x1b873593);
h2 = mix(h2, lo, 0x85ebca6b, 0xc2b2ae35);
h2 = mix(h2, hi, 0x85ebca6b, 0xc2b2ae35);
}
h1 = finalize(h1 ^ childIds.length);
h2 = finalize(h2 ^ childIds.length);
return (h2 & 0x1fffff) * 0x100000000 + (h1 >>> 0);
}
tagHash(tag) {
const cached = this.tagHashes.get(tag);
if (cached !== undefined) {
return cached;
}
let hash = 0x811c9dc5;
for (let i = 0; i < tag.length; i += 1) {
hash ^= tag.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);
}
hash >>>= 0;
this.tagHashes.set(tag, hash);
return hash;
}
}
function mix(h, k, c1, c2) {
k = Math.imul(k, c1);
k = ((k << 15) | (k >>> 17)) >>> 0;
k = Math.imul(k, c2);
h ^= k;
h = ((h << 13) | (h >>> 19)) >>> 0;
return (Math.imul(h, 5) + 0xe6546b64) >>> 0;
}
function finalize(h) {
h ^= h >>> 16;
h = Math.imul(h, 0x85ebca6b);
h ^= h >>> 13;
h = Math.imul(h, 0xc2b2ae35);
h ^= h >>> 16;
return h >>> 0;
}
export class NormalizedNode {
tag;
children;
count;
cachedFor;
cachedId = 0;
constructor(tag, children) {
this.tag = tag;
this.children = children;
this.count = 1 + children.reduce((sum, child) => sum + child.count, 0);
}
nodeCount() {
return this.count;
}
fingerprints(interner) {
const result = new Set();
this.collectFingerprints(interner, result);
const sorted = Float64Array.from(result);
sorted.sort();
return sorted;
}
collectFingerprints(interner, result) {
// Nested candidates re-walk shared subtrees; the single-slot cache keeps the
// hash computation to once per unique node for the most recently used
// interner (alternating interners recompute — fine, scans use one interner).
if (this.cachedFor === interner) {
for (const child of this.children) {
child.collectFingerprints(interner, result);
}
result.add(this.cachedId);
return this.cachedId;
}
const childIds = [];
for (const child of this.children) {
childIds.push(child.collectFingerprints(interner, result));
}
const id = interner.idFor(this.tag, childIds);
this.cachedFor = interner;
this.cachedId = id;
result.add(id);
return id;
}
}