TypeScript-only similarity analyzer
Private edition of mizchi/similarity.
npx @kongyo2/similarity-ts .Use the following prompt with your AI coding assistant:
Run `npx @kongyo2/similarity-ts .` to detect semantic code similarities.
Execute this command, analyze the duplicate code patterns, and create a refactoring plan.
For Claude Code, copy
.claude/skills/check-similarity-ts
into your project's .claude/skills/ directory. The skill runs the analyzer
on TypeScript files and turns its clusters and pairs into a refactoring plan.
Three or more copies of one declaration are reported once, as a cluster,
instead of as every pair among them: five identical dispose() methods are
one block listing five locations, not ten table rows. The JSON report keeps
every pair in results/byMode and adds the groups as clusters.
The analyzer compares functions, types, and classes structurally. Every function is alpha-renamed before comparison — parameters, locals, inner functions, and the declaration name all get positional canonical names — so consistent renames never hide a duplicate, no matter how many identifiers changed. Free identifiers (imported helpers, globals, property names) keep their names, because which function you call is behavior.
On top of that, the comparison canonicalizes common style alternatives — two snippets a reviewer would call "the same code written differently" score as equal:
- arrow functions ⇔ function declarations ⇔ class methods ⇔ object-literal methods ⇔ arrow-valued class fields, expression bodies ⇔ block bodies
promise.then((v) => …)⇔const v = await promise; …items.forEach((item) => …)⇔for (const item of items) …⇔for (let i = 0; i < items.length; i++) { const item = items[i]; … }- push-accumulator loops ⇔
items.map(...)/items.filter(...) `Hello ${name}`⇔"Hello " + nametotal += n⇔total = total + n⇔i++(in statement position)return c ? a : b⇔if (c) { return a } else { return b }⇔if (c) { return a } return b(guard style)if (!c) { A } else { B }⇔if (c) { B } else { A },!==guards ⇔ flipped===guards, De Morgan (!(a && b)⇔!a || !b)x ? x : y⇔x || y·x == null ? d : x⇔x ?? d·x === null || x === undefined⇔x == null·void 0⇔undefined·Boolean(x)⇔!!xconst { a, b } = obj⇔const a = obj.a; const b = obj.b, including single-use temporaries inlined either wayconst x = c ? a : b⇔let x; if (c) { x = a } else { x = b }- jump-terminated
switch⇔if/else ifchains ⇔ guard ladders Object.assign({}, a, b)⇔{ ...a, ...b }constructor(private readonly repo: Repo) {}⇔constructor(repo: Repo) { this.repo = repo; }(parameter properties are desugared the way the compiler emits them, after anysuper()call)for (; cond; )⇔while (cond), braced ⇔ brace-less bodies, Yoda comparisons (0 === n) ⇔ natural order,arr[arr.length - 1]⇔arr.at(-1), reordered independentconstdeclarations- interfaces ⇔ structurally identical type aliases, reordered members,
renamed-but-identically-typed property sets,
Array<T>⇔T[],x?: T⇔x: T | undefined, reordered unions, renamed generic params,'a'⇔"a"literal types - classes: reordered members, renamed private fields, renamed class type
parameters (
ItemList<T> extends Collection<T>⇔EntryList<U> extends Collection<U>, also when they are declared in another order and used together, as inT | U), constructor parameter properties ⇔ explicit field + assignment (a parameter stored asthis.x = x as Tis still stored), and fully renamed method names when the canonical method bodies match
Every declaration form takes part: function declarations, arrow and
function expressions bound to a variable (also when parenthesized or
wrapped in as/satisfies; a named function expression keeps recursing
through either of its names), the methods and function-valued properties of
an object literal bound to a name or default-exported (handlers.onSave,
nested objects included; a named function expression there recurses
through its own name), export default classes and functions, class
methods and arrow-function class fields, constructors, and declarations
inside namespace/module blocks, which are reported under their
qualified name (Legacy.Options). Bodiless declarations — overload
signatures, declare function, abstract methods — are never reported:
there is no code behind them.
Rewrites that change behavior keep their distinct shapes on purpose:
swapped builtins (.map vs .filter, Math.max vs Math.min), a
different free callee (sendEmail vs sendSms; a method's key binds
nothing in its body, so debug(…) inside a debug() method calls the
outer debug), flipped operators (* vs /, && vs ||, < vs <=),
?? vs ||, x == null vs x === null,
optional chaining vs plain access, for-of vs for-in, an added break,
reordered statements that share data, async vs sync and generator vs
plain contracts (a sync twin is the same code only when it visibly returns
a promise: a Promise<…> return type, a returned Promise.resolve(…), or
a chained .then), then(onFulfilled, onRejected), fall-through switch
cases, Object.assign with a mutated target, string prepend vs append
folds (trail = trail + seg vs trail = seg + trail), boundary indexes
(.at(-1) vs .at(0), .slice(0, n) vs .slice(n)), templates that
stringify adjacent values (`${a}${b}` vs numeric a + b), and on the
type side Promise<ShopUser> vs Promise<ShopOrder> payloads, Map<K, V>
vs Map<V, K> swaps, index signatures vs concrete members,
discriminated-union variants that differ in their kind literal or enum
member (signed numbers, bigints and string literals containing quotes
included), differing extends heritage (type arguments included:
Box<string> is not Box<number>), brand markers (_fooBrand: any),
typeof/type-predicate members naming different things, qualified generic
bases (React.ComponentType<P> vs Preact.ComponentType<P>), one- or
two-member shapes whose only shared trait is a primitive, and aliases of a
single atom (type GLsizei = number, type SchemaVersion = "1" in either
quote style), which pair only when they are also named alike — the same
alias copied into two files. On the class side member annotations compare
the way type mode compares them (literal unions, enum members, qualified
and generic types keep their identity; renamed class type parameters do
not count), a constructor that does real work counts as a member,
constructors storing the same parameters into different fields
(this.x = y; this.y = x) or super(…) slots differ, a static member is
not its instance namesake, an optional member is not a required one, a
different, qualified-vs-absent, or one-sided base class is a contract
difference, an implements clause the other side lacks is one more
difference, and a single shared field is not a duplicate class. Twins that
differ only in data literals (a table name, a status code, a locale
string) are reported — parameterizing them is the refactor.
Accuracy is tracked by a labeled benchmark (bench/cases.ts plus the
extended corpora in bench/cases/) that mirrors the refactoring flow
above: 337 ground-truth pairs across functions, types, and classes —
semantic duplicates a refactoring plan must see, and similarly-shaped
lookalikes it must not flag — evaluated at the default threshold. The
corpus covers whole-function renames, guard/negation/ternary spellings,
loop-form rewrites, destructuring, nullish sugar, literal-vs-behavior
twins, realistic cross-file copy-paste, and — since 0.7.0 — the lookalike
families found by auditing the engine's reports on a snapshot of the
TypeScript compiler repository: bodiless overload signatures,
discriminated-union variants (enum members, string, signed-number and
bigint literals), heritage (one-sided, differing, differently instantiated
or qualified bases), brand markers, tiny renamed shapes, typeof/predicate
members, constructor work, and declaration scopes — namespaces, default
exports, named function expressions — and, since 0.8.0, the families
found on the fallow, tsrs and TypeScript repositories: sync/async API
siblings, single-atom type aliases, qualified generic bases, class members
whose annotations differ, renamed class type parameters, constructor
wiring, static/instance and optional members, object-literal members and
wrapped initializers — and, since 0.8.1, string literals that contain
quotes or use single quotes, object members that recurse through their own
name, methods that delegate to a helper named like themselves, class type
parameters declared in another order, and constructors that narrow a
parameter before storing it.
| Engine | Corpus | Wrong labels | Error rate | Accuracy |
|---|---|---|---|---|
| v0.3.0 | 71 pairs | 15 / 71 | 21.13% | 78.87% |
| v0.4.1 | 71 pairs | 0 / 71 | 0.00% | 100.00% |
| v0.4.1 | 261 pairs | 89 / 261 | 34.10% | 65.90% |
| v0.5.0 | 261 pairs | 7 / 261 | 2.68% | 97.32% |
| v0.6.0 | 261 pairs | 0 / 261 | 0.00% | 100.00% |
| v0.6.0 | 300 pairs | 29 / 300 | 9.67% | 90.33% |
| v0.7.0 | 300 pairs | 0 / 300 | 0.00% | 100.00% |
| v0.7.0 | 325 pairs | 22 / 325 | 6.77% | 93.23% |
| v0.8.0 | 325 pairs | 0 / 325 | 0.00% | 100.00% |
| v0.8.0 | 337 pairs | 12 / 337 | 3.56% | 96.44% |
| v0.8.1 | 337 pairs | 0 / 337 | 0.00% | 100.00% |
v0.7.0 was audited against real code: run over the TypeScript compiler's
API package, its VS Code extension, and 1,482 conformance test files, the
v0.6.0 engine reported every AST-node interface pair that differs only in
its kind discriminant, every overload signature as a 1.000 duplicate of
the next, and thousands of single-field conformance classes as twins,
while never extracting function expressions, arrow-function class fields,
or anything declared inside a namespace. v0.7.0 fixes each family (type
pairs on the 108-file API package drop from 685 to 419, conformance class
pairs from 15,062 to 5,793, and the newly extracted declaration forms
surface their twins), adds the families to the corpus, and keeps the
original 261 pairs at 100% with unchanged margins. Reports are also
deterministic now — type mode used to compare declarations in hash-map
order, which moved borderline scores between runs.
v0.8.0 was audited against three more codebases: the fallow analyzer (its
VS Code extension, tools, npm packages and visualization frontend), tsrs
(its npm package, tools and playground, and the 116 standard-library
declaration files it bundles), and the current TypeScript repository,
whose API package now ships sync and async siblings. The v0.7.0 engine
paired the async API's methods with their sync namesakes, reported every
two aliases of a single atom with equal bodies (the WebGL/WebGPU number
labels of the DOM library, the "1" schema versions of a generated output
contract), rendered class member annotations onto any/unknown, and
never saw the members of an object literal — dispatch tables, handler
objects, test doubles. v0.8.0 fixes each family (type pairs on the tsrs
standard library drop from 4,585 to 2,383, on fallow's VS Code extension
from 855 to 757), adds the families to the corpus, and keeps the original
300 pairs at 100%. Overlap mode, part of every default run, used to give a
different report on each run of the same input and took over five minutes
on the 59-file TypeScript API package; it is deterministic now, and a
default run there takes 56 s instead of 314 s.
Run it yourself with npm run bench:accuracy; the suite in
tests/accuracy-benchmark.test.ts fails CI if any labeled pair is
mislabeled.
| Option | Default | Description |
|---|---|---|
--modes <list> |
functions,types,classes,overlap |
Comma-separated analysis modes |
-t, --threshold <number> |
0.8 |
Similarity threshold (0–1) |
--min-lines <number> |
3 |
Minimum function line count |
--min-tokens <number> |
— | Minimum function size in AST nodes (replaces the line gate; ~50 recommended for noisy code) |
--no-size-penalty |
off | Disable the short-function score penalty |
--same-file-only / --cross-file-only |
off | Restrict pair scope |
--extensions <list> |
ts,tsx,mts,cts |
File extensions to scan |
--exclude <pattern> |
— | Exclude glob (repeatable) |
--types-only <kind> |
all |
Restrict type mode to interface or type |
--no-allow-cross-kind |
off | Disable interface ⇔ type alias matching |
--type-literals |
off | Include anonymous type literals in type mode |
--format <pretty|json> |
pretty |
Output format |
--output <path> |
— | Write the report to a file (parent directories are created) |
--fail-on-warnings |
off | Non-zero exit on analyzer warnings |
--fail-on-duplicates |
off | Non-zero exit when any pair is reported (CI gate) |
Annotate a declaration with a // similarity-ignore comment on the
preceding line to exclude it from the report.
The function comparison implements TSED (Tree Similarity of Edit
Distance): both fragments are parsed to ASTs, an APTED-style tree edit
distance δ with configurable per-operation weights (rename 0.3, delete
1.0, insert 1.0 — tuned against the labeled corpus) is computed, and the
score is normalized as TSED = max(1 − δ / MaxNodes(G1, G2), 0). The
alpha-renaming, refactor canonicalization, behavioral-atom guard, and
size-penalty layers documented above are this project's additions on top
of that metric; the operation-weight sensitivity they exploit is the
paper's RQ3 finding that TSED's penalty weights are influential and
language-dependent. Following that finding, the weights are re-validated
against the labeled corpus on every accuracy release: for 0.8.1 every
setting in rename 0.2–0.5 × delete/insert 0.8–1.0 classifies all 337 pairs
correctly (the closest call is XF-N36 at rename 0.2, 0.797 against the 0.8
threshold, the same as on v0.7.0 and v0.8.0), so the calibrated defaults
stand.
- Yewei Song, Cedric Lothritz, Daniel Tang, Tegawendé F. Bissyandé, and Jacques Klein. 2024. Revisiting Code Similarity Evaluation with Abstract Syntax Tree Edit Distance. In Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics (Volume 2: Short Papers). ACL Anthology 2024.acl-short.3 · arXiv:2404.08817
- Yewei Song, Saad Ezzini, Xunzhu Tang, Cedric Lothritz, Jacques Klein, Tegawendé Bissyandé, Andrey Boytsov, Ulrick Ble, and Anne Goujon. 2023. Enhancing Text-to-SQL Translation for Financial System Design. The paper that introduced the original TSED metric. arXiv:2312.14725
- Mateusz Pawlik and Nikolaus Augsten. 2015. Efficient Computation of the Tree Edit Distance. ACM Transactions on Database Systems 40(1) — the APTED algorithm family used for δ. See also Pawlik and Augsten 2016, Tree edit distance: Robust and memory-efficient, Information Systems 56.