Guides

Types of Code Clones

Exact, renamed, near-miss and semantic clones, and which of them jscpd finds.

"Duplicated code" covers more than copy and paste. Clone-detection research sorts duplicates into four types by how much the copies have drifted apart, and every detector, jscpd included, draws its line somewhere in that scale. Knowing the types tells you what a scan can and cannot show, and which flag to reach for.

TypeAlso calledThe copies differ injscpd
Type-1exact clonewhitespace, layout, commentsdefault
Type-2renamed, parameterized cloneidentifier names, literal values, annotations--ignore-identifiers, --ignore-literals, --ignore-annotations
Type-3near-miss, gapped cloneadded, removed or changed statements--max-gap-lines N merges the pieces around a gap; --similarity RATIO compares whole JS/TS functions by syntax tree
Type-4semantic cloneeverything but the behaviorout of scope for token-based detection

Type-1: exact clones

Two fragments are Type-1 clones when their tokens are identical. Whitespace, line breaks and, depending on the mode, comments do not count.

// a.js
function total(items) {
  let sum = 0;
  for (const item of items) { sum += item.price; }
  return sum;
}

// b.js
function total(items) {
  let sum = 0;   // running total
  for (const item of items) {
    sum += item.price;
  }
  return sum;
}

jscpd finds these by default. The --mode option decides what "identical" ignores:

  • mild (default) drops whitespace, so layout changes are invisible.
  • weak also drops comments, so a re-commented copy still matches.
  • strict keeps everything except blocks marked with jscpd:ignore-start / jscpd:ignore-end.

Type-1 is the category to gate in CI: an exact clone is nearly always a copy that should be a shared function, and the result is stable enough for a baseline.

Type-2: renamed clones

A Type-2 clone is a Type-1 clone whose identifiers, literals or annotations were changed while the structure stayed the same. This is what a copy looks like after someone adapted it to a second use.

// cart.js
export function cartTotal(items, taxRate) {
  let subtotal = 0;
  for (const item of items) {
    subtotal += item.price * item.quantity;
  }
  const tax = subtotal * taxRate;
  return { subtotal, tax, total: subtotal + tax };
}

// basket.js
export function basketTotal(entries, vatRate) {
  let net = 0;
  for (const entry of entries) {
    net += entry.price * entry.quantity;
  }
  const vat = net * vatRate;
  return { net, vat, gross: net + vat };
}

A default scan reports nothing here, because every other token differs. jscpd 5.2 adds three flags that normalize token classes before hashing:

FlagConfig keyEffect
--ignore-identifiersignoreIdentifiersevery identifier hashes as $id; keywords are kept, so for still has to match for
--ignore-literalsignoreLiteralsstrings hash as $str, numbers as $num; a string never matches a number
--ignore-annotationsignoreAnnotations@Name, @a.b.Name and @Name(...) are dropped in Java, Kotlin, Scala, Groovy, Python, Dart, Swift, JavaScript and TypeScript
jscpd --ignore-identifiers src/
# Clone found (javascript, renamed)
#  - basket.js [1:1 - 9:2] (9 lines, 57 tokens)
#    cart.js [1:1 - 9:2]

Clones found this way are labelled so they never blend into the exact ones. Every clone carries a kind: exact when the raw tokens match, renamed when they match only after normalization. The console prints Clone found (javascript, renamed), the JSON report has a "kind" field, and the SARIF report files renamed clones under the rule jscpd/similar-code instead of jscpd/duplicate-code, so GitHub code scanning shows them as a separate rule.

Normalized runs find more and longer clones than exact runs, so their fingerprints differ. Keep a separate --baseline file for a normalized configuration rather than reusing the one from an exact scan.

--ignore-case is a much smaller step in the same direction: it folds Total and total together and nothing else.

The repository ships a runnable demo of each flag in fixtures/type2-demo, with the expected output of every command.

Type-3: near-miss clones

Type-3 clones are copies with statements added, removed or changed in the middle: a validation line inserted here, a logging call dropped there.

// original                          // copy with an inserted check
function save(user) {                function save(user) {
  const row = toRow(user);             const row = toRow(user);
  row.updatedAt = Date.now();          if (!row.id) throw new Error('id');
  db.put(row);                         row.updatedAt = Date.now();
  audit('save', row.id);               db.put(row);
}                                      audit('save', row.id);
                                     }

A token-window detector sees this as two shorter exact clones with a gap between them, and that is what a default jscpd run reports, provided each side of the gap still clears --min-tokens and --min-lines.

From jscpd 5.2, --max-gap-lines N (config key maxGapLines) merges clones of the same file pair whose fragments follow each other in both files with at most N unmatched lines between them into one clone of kind similar. Its tokens value is the number of matched tokens and similarity is that number divided by the tokens of the longer merged span, so one inserted line in a 150-token block reads as roughly 0.9:

jscpd src/
# Found 2 clones.

jscpd --max-gap-lines 1 src/
# Clone found (javascript, similar (gap) ~0.91)
#  - save-account.js [1:1 - 12:2] (12 lines, 157 tokens)
#    save-user.js [1:1 - 11:2]
# Found 1 clones.

The merge only joins clones the exact run already found, so it cannot invent a match; it removes fragmentation. It is off at the default of 0, and the JSON report carries "kind": "similar" with the "similarity" value, while SARIF files these under jscpd/near-miss-code. A runnable pair lives in fixtures/type3-demo.

Edits spread through a function rather than concentrated in one gap still escape a token window. For JavaScript and TypeScript, --similarity RATIO compares whole functions by structure instead: each function, method or arrow function is summarized by the 4-grams of its syntax-tree node types, and two functions are reported as one similar clone when the weighted Jaccard index of those bags reaches RATIO. Names and values are not part of the summary, so a renamed copy scores 1.0, one inserted line about 0.9, and two inserted statements plus renames about 0.75:

jscpd --similarity 0.85 src/     # near-identical structure
jscpd --similarity 0.7 src/      # a couple of added or removed statements
# Clone found (javascript, similar (ast) ~0.75)
#  - credit-note.js [1:8 - 19:2] (19 lines, 126 tokens)
#    invoice.js [1:8 - 17:2]

Functions must clear --min-tokens and --min-lines on their own, and a pair already reported as an exact or merged clone is not repeated. The same ratio is accepted by the MCP check_duplication tool, which then lists the project functions structurally similar to a snippet. Both options are off by default. A runnable pair lives in fixtures/type3-demo/similar-functions.

Type-4: semantic clones

Type-4 clones compute the same thing with different code: a for loop and a reduce call that both sum prices, or two sorting routines. No token-based tool detects them, and jscpd does not try. They are the domain of program-dependence-graph and AST-similarity research, and in practice of code review.

Which type to look for

  • In CI, gate on Type-1. Exact clones are unambiguous and stable across runs, which is what a failing check needs. Use --min-tokens and --min-lines to set the size, and a baseline to fail only on new duplication.
  • When refactoring, scan for Type-2. Run --ignore-identifiers --ignore-literals on the area you are about to change; renamed copies are where a shared helper pays off most. Read the renamed results as leads, not defects, since some parametric similarity is idiomatic.
  • Treat adjacent Type-1 findings as a pair. Two clones between the same files a few lines apart usually are one near-miss clone with an edit in between. Rerun with --max-gap-lines 2 to see them as one and read the similarity value.
  • Hunt Type-3 in JS/TS with --similarity. Start at 0.85 to catch near-identical functions, lower it towards 0.7 to see heavier edits. Treat the score as a ranking, not a verdict.