Swift CPD

Duplicated code rarely looks dangerous on the day it’s pasted. It becomes dangerous the day one copy gets a fix and the other doesn’t. swift-cpd finds duplicated logic by its structure, not its text — through renamed variables, changed literals, edited lines and even different implementations of the same behavior — across Swift, Objective-C and C.

Install
brew tap ericodx/homebrew-tools
brew install swift-cpd
4 clone types, from exact copies to the same behavior
3 languages: Swift, Objective-C and C
4 output formats, including inline Xcode warnings
0 dependencies to add to your project

Different words, same code

Every field, literal and threshold below is different. A text diff sees two unrelated functions. swift-cpd sees the same 67 tokens once names and values are set aside — and the day the rules change in one of them, the other is your next bug report.

SignUpValidator.swift
struct SignUpValidator {
    func validate(_ form: Form) -> Bool {
        guard !form.name.isEmpty else { return false }
        guard form.age >= 18 else { return false }
        guard form.email.contains("@") else { return false }
        guard form.country.count == 2 else { return false }
        return true
    }
}
CheckoutValidator.swift
struct CheckoutValidator {
    func validate(_ form: Form) -> Bool {
        guard !form.holder.isEmpty else { return false }
        guard form.amount >= 1 else { return false }
        guard form.card.contains(" ") else { return false }
        guard form.currency.count == 3 else { return false }
        return true
    }
}
Output
$ swift-cpd
Found 1 clone(s) in 2 files (0.00s)

Clone 1 (Type-2, 67 tokens, 9 lines):
  Sources/App/Features/CheckoutValidator.swift:1-9
  Sources/App/Features/SignUpValidator.swift:1-9

Four kinds of duplication

Most copy-paste detectors stop at the first two. The copies that hurt are usually the ones that were edited after the paste — or rewritten from memory.

Type-1 · Exact The same code, token for token. The copy-paste nobody touched since.
Type-2 · Parameterized The same structure with different names and literals — the example above.
Type-3 · Near-miss A copy that has since grown, lost or swapped a few statements. Scored by Greedy String Tiling, 70% similarity by default.
Type-4 · Semantic Different code, same behavior. Compared by control flow, data flow, calls and types, 80% similarity by default.

How it works

Comparing every fragment with every other one doesn’t scale past a small project. Each stage narrows the work for the next, so the expensive comparisons only see pairs that are likely to be clones.

  1. 01 Discover Finds the Swift files in your configured paths — plus .m, .mm, .h, .c and .cpp with --cross-language — and leaves out what your exclude patterns name. Or reads them from a git ref instead of the working tree.
  2. 02 Tokenize SwiftSyntax tokenizes Swift; a C-family tokenizer handles the rest. Files are processed concurrently and cached by content hash, so unchanged files cost nothing on the next run.
  3. 03 Normalize Identifiers, numbers and strings become placeholders. Type names and called functions stay as they are, so code that only looks alike is not reported. Suppressed regions are removed here.
  4. 04 Detect A rolling hash finds exact and parameterized clones in linear time per file. Near-miss and semantic clones go through pre-filters first, so the expensive comparisons only run on likely pairs.
  5. 05 Report Text, JSON, a self-contained HTML page, or warnings Xcode shows in the editor — with the duplication percentage the quality gate checks.

Signal, not noise

A detector that flags every two functions with a guard statement gets turned off within a week. The defaults aim at duplication worth a refactor, and everything is tunable.

Types keep their names Normalization keeps type names and called functions, so Color(r: 0) and GridToken(columns: 2) are not a clone just because they share a shape.
Thresholds you choose 50 tokens and 5 lines by default, and a similarity threshold for each structural type. Lower them to hunt small clones, raise them to see only the big ones.
Filters for the common cases Skip clones that live inside one file, or keep only exact and parameterized clones while you start. Both are a line in the config.
Suppression by comment // swiftcpd:ignore before a declaration leaves its whole body out; before a line, just that line. Nothing to import, nothing in your build.

Built for CI

Duplication you only see in a dashboard keeps growing. Make it a rule instead: a threshold for the whole codebase, or a baseline that lets existing clones be and stops new ones at the door.

Commands
# Fail the build when duplication goes above 3%
swift-cpd --max-duplication 3 Sources/

# Or accept today's clones once, and fail only on new ones
swift-cpd --baseline-generate Sources/
swift-cpd --baseline .swift-cpd-baseline.json Sources/
pre-commit
# .pre-commit-config.yaml — check exactly what is being committed
- repo: https://github.com/ericodx/swift-cpd
  hooks:
    - id: swift-cpd
      args: [--source-ref, ":0"]
Quality gate --max-duplication turns the duplication percentage into a pass or fail. Exit code 1 means clones; 2 and 3 are configuration and analysis errors, so CI can tell them apart.
Baselines for legacy code Record the clones you already have, commit the file, and report only new ones. Combined with the gate, the threshold applies to new clones alone.
pre-commit, done right With --source-ref :0 the hook reads the staged blobs, not a half-stashed working tree, so it reports what the commit will actually contain.
Reports to keep A JSON report with a stable, sorted schema for scripts, and a self-contained HTML report to attach as a build artifact.
CI recipes

Two commands to a report

init writes a small .swift-cpd.yml with the folders it found. The second command is the whole analysis.

Quick start
# Finds your sources and writes .swift-cpd.yml
swift-cpd init

# Run it
swift-cpd

Warnings where you write code

The build plugin runs swift-cpd on every build of an Xcode or SPM target and shows each clone as a warning on its line in the editor. It never fails the build — it just makes the duplication hard to miss.

Add the plugin
// Package.swift
.target(
    name: "MyTarget",
    plugins: [.plugin(name: "SwiftCPDPlugin", package: "swift-cpd")]
)
Build output
Sources/App/Features/CheckoutValidator.swift:1:1: warning: Clone detected (Type-2, 67 tokens, 9 lines) — also in SignUpValidator.swift:1
Sources/App/Features/SignUpValidator.swift:1:1: warning: Clone detected (Type-2, 67 tokens, 9 lines) — also in CheckoutValidator.swift:1

Also included

Across languages Swift and Objective-C tokens map to one vocabulary, so a Swift rewrite of an Objective-C class can be found next to the original.
Any git ref Analyze HEAD, a branch, a sha or the index instead of the working tree. The cache is keyed by the resolved sha.
init that finds your code Picks Sources/ when it exists, or every top-level folder with Swift files — skipping build and dependency folders.

Free and open source

MIT-licensed, no account, no telemetry. It runs on your machine and your CI runners, and your code never leaves them. Issues, ideas and pull requests are welcome.

↑ Back to top