97 """Winnowed fingerprints of a token list, as a set of hashes.
99 The k-gram hashes are produced with a rolling polynomial hash, then winnowed with a
100 monotonic deque so the whole pass is linear rather than O(n*w).
105 ids = [hash(t) & 0xFFFFFFFF
for t
in tokens]
107 high = pow(BASE, k - 1, MOD)
110 h = (h * BASE + ids[i]) % MOD
112 for i
in range(k, n):
113 h = ((h - ids[i - k] * high) * BASE + ids[i]) % MOD
120 for i, hv
in enumerate(hashes):
121 while dq
and hashes[dq[-1]] >= hv:
124 while dq[0] <= i - w:
127 picked.add(hashes[dq[0]])
131def scan(root, k, w, normalise):
132 """Fingerprint every production file under root."""
133 root = os.path.abspath(root)
134 per_file, corpus = {}, set()
136 for dirpath, dirnames, filenames
in os.walk(root):
137 rel_dir =
"/" + os.path.relpath(dirpath, root).replace(os.sep,
"/") +
"/"
141 for name
in sorted(filenames):
142 if not name.lower().endswith(SOURCE_SUFFIXES):
144 full = os.path.join(dirpath, name)
145 rel = os.path.relpath(full, root).replace(os.sep,
"/")
147 with open(full, encoding=
"utf-8", errors=
"replace")
as fh:
152 tokens_total += len(toks)
157 return per_file, corpus, tokens_total
160def compare(name_a, a_files, a_corpus, name_b, b_files, b_corpus, a_tokens, b_tokens):
161 shared = a_corpus & b_corpus
163 for rel, fp
in a_files.items():
166 hit = len(fp & b_corpus)
167 per_file.append({
"file": rel,
"fingerprints": len(fp),
168 "shared": hit,
"share": round(100.0 * hit / len(fp), 1)})
169 per_file.sort(key=
lambda r: (-r[
"share"], -r[
"fingerprints"]))
170 buckets = defaultdict(int)
173 buckets[
">=90%"] += 1
174 elif r[
"share"] >= 50:
175 buckets[
"50-90%"] += 1
176 elif r[
"share"] >= 10:
177 buckets[
"10-50%"] += 1
181 "a": name_a,
"b": name_b,
182 "a_files": len(a_files),
"b_files": len(b_files),
183 "a_tokens": a_tokens,
"b_tokens": b_tokens,
184 "a_fingerprints": len(a_corpus),
"b_fingerprints": len(b_corpus),
185 "shared_fingerprints": len(shared),
186 "share_of_a": round(100.0 * len(shared) /
max(1, len(a_corpus)), 1),
187 "share_of_b": round(100.0 * len(shared) /
max(1, len(b_corpus)), 1),
188 "buckets": dict(buckets),
189 "most_shared": per_file[:25],
190 "least_shared": [r
for r
in per_file
if r[
"fingerprints"] >= 40][-25:],
195 ap = argparse.ArgumentParser(description=__doc__,
196 formatter_class=argparse.RawDescriptionHelpFormatter)
197 ap.add_argument(
"--a", required=
True, help=
"first source tree")
198 ap.add_argument(
"--b", required=
True, help=
"second source tree")
199 ap.add_argument(
"--name-a", default=
"a")
200 ap.add_argument(
"--name-b", default=
"b")
201 ap.add_argument(
"-k", type=int, default=20, help=
"k-gram size in tokens")
202 ap.add_argument(
"-w", type=int, default=12, help=
"winnowing window")
203 ap.add_argument(
"-o",
"--out", default=
"clones.json")
204 args = ap.parse_args()
206 report = {
"k": args.k,
"w": args.w,
207 "guaranteed_match_length": args.k + args.w - 1}
208 for mode
in (
"strict",
"normalised"):
209 norm = mode ==
"normalised"
210 sys.stderr.write(
"clone_detect: scanning %s (%s)\n" % (args.name_a, mode))
211 af, ac, at =
scan(args.a, args.k, args.w, norm)
212 sys.stderr.write(
"clone_detect: scanning %s (%s)\n" % (args.name_b, mode))
213 bf, bc, bt =
scan(args.b, args.k, args.w, norm)
214 report[mode] =
compare(args.name_a, af, ac, args.name_b, bf, bc, at, bt)
216 sys.stderr.write(
"clone_detect: %s - %.1f%% of %s shared with %s\n"
217 % (mode, r[
"share_of_a"], args.name_a, args.name_b))
219 with open(args.out,
"w", encoding=
"utf-8")
as fh:
220 json.dump(report, fh, indent=1)
221 sys.stderr.write(
"clone_detect: wrote %s\n" % args.out)
compare(name_a, a_files, a_corpus, name_b, b_files, b_corpus, a_tokens, b_tokens)