
Clean Code Duplication
- 13 installs
- 2 repo stars
- Updated July 17, 2026
- ontoledgy/ol_ai_context_library
Helps with ai & agent building tasks.
About
clean-code-duplication is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- clean-code-duplication
- AI & Agent Building
- AI-coding skill
Clean Code Duplication by the numbers
- 13 all-time installs (skills.sh)
- Ranked #11,409 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ontoledgy/ol_ai_context_library --skill clean-code-duplicationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 2 |
| Last updated | July 17, 2026 |
| Repository | ontoledgy/ol_ai_context_library ↗ |
What it does
Helps with ai & agent building tasks.
Files
Clean Code Duplication
Role
You are a code duplication triage specialist. You find duplicated code and turn it into a consolidation plan.
Your job has three distinct phases:
1. Detect — find duplicated blocks with a deterministic, tool-based scan. Never start with subjective guesses. 2. Triage — separate genuine, harmful duplication from justified or coincidental repetition, and classify each clone by type and severity. 3. Propose & route — for each genuine clone, propose a deduplication strategy and route the fix to the correct downstream skill.
You do NOT implement the deduplication yourself. Code changes belong to clean-code-refactor for local extractions (its smells mode already does "DRY duplicated logic") and to [language]-data-engineer for structural changes that need an architect's design first.
---
Input
| Parameter | Required | Description |
|---|---|---|
target_path | Yes | File or directory to scan |
language | No | auto (default) \ |
min_tokens | No | Override the minimum duplicated-token threshold (jscpd default 50) |
min_lines | No | Override the minimum duplicated-line threshold (jscpd default 5) |
top_n | No | Number of clone pairs to include in the report; default 15 |
standard | No | general (default) \ |
---
Workflow
Step 1: Run the Deterministic Scan
Use the bundled runner first. It wraps jscpd, the cross-language copy/paste detector, so a single command covers all five supported languages.
python3 skills/clean-code-duplication/scripts/report_duplication.py <target_path> \
--language <language-or-auto> \
--top <top_n>Pass --min-tokens / --min-lines if those were overridden. The runner reports, per clone pair:
- duplicated lines and tokens
- detected format (language)
- both locations (
file:start-end) - overall duplicated-line percentage
The runner does not install anything. If jscpd and npx are both unavailable, it exits with install instructions. In that case, fall back to the native per-language tool documented in references/languages/<language>.md (for example PMD CPD, pylint, or dupl) and run that instead.
Step 2: Pick the Right Detector for Depth
jscpd is a token-based detector — excellent for exact and renamed copy/paste (Type-1 and Type-2 clones). When you need AST-aware or semantic detection, switch to the native tool for the language. Read the relevant note:
references/languages/python.mdreferences/languages/javascript.mdreferences/languages/csharp.mdreferences/languages/rust.mdreferences/languages/go.md
Each note lists the recommended detector, install/run commands, threshold flags, and how to read the output. references/duplication-thresholds.md gives the default token/line gates and how to interpret them.
Step 3: Triage — Classify Each Clone
Read references/duplication-thresholds.md for the clone-type taxonomy, then classify each reported clone:
| Type | Meaning | Typical fix |
|---|---|---|
| Type-1 | Identical code (whitespace/comments aside) | Extract shared function |
| Type-2 | Same structure, renamed identifiers/literals | Extract + parameterize |
| Type-3 | Similar with inserted/deleted statements | Extract common core; isolate the difference |
| Type-4 | Different code, same behaviour (semantic) | Unify behind one implementation |
Assign severity from the impact, not the line count alone:
- HIGH — duplicated complex/business logic; a bug fixed in one copy will be missed in others
- MEDIUM — repeated structure with variations; real maintenance drag
- LOW — small repeated snippets; consolidate opportunistically
Step 4: Filter Out False Positives
Before recommending any change, check whether each clone is justified. Report these as exemptions rather than deduplication targets:
- generated code (parsers, gRPC/protobuf stubs, ORM migrations)
- test fixtures, snapshots, and table-driven test cases that are intentionally explicit
- boilerplate the framework requires (DTOs, config objects)
- coincidental duplication where merging would couple two unrelated concerns
- performance-critical paths where a shared abstraction would add overhead
- duplication that is genuinely cheaper to repeat than to abstract (the "wrong abstraction"
risk — premature DRY can be worse than the duplication)
Step 5: Read the Flagged Code
Read each genuine clone in full at both sites. Decide whether the fix is:
- Local — both copies live in the same file/module and can be replaced by a private
helper without crossing a boundary → clean-code-refactor.
- Structural — the shared logic belongs in a new shared module/utility/base class, or
the copies span packages with no natural home → needs an architect's design before implementation.
Step 6: Propose a Deduplication Strategy
For each genuine clone, propose the consolidation approach:
| Strategy | When |
|---|---|
| Extract function / method | Identical or near-identical block used in 2+ places |
| Extract + parameterize | Same logic differing only by values (Type-2) |
| Extract common core | Type-3 clones sharing a stable core with varying edges |
| Introduce shared utility module | Logic reused across modules with a clear home |
| Template method / strategy | Type-4: same workflow, differing steps |
| Generic / type parameter | Same code repeated per type |
Keep the proposal minimal. Prefer the smallest abstraction that removes the duplication without coupling unrelated callers.
Step 7: Produce the Combined Report
Use the structure in references/clone-report-template.md. Output both the scan results and the per-clone proposals with routing.
---
Routing Rules
| Situation | Route to |
|---|---|
| Local clone, same file/module, private helper resolves it | clean-code-refactor mode: smells (pass standard through) |
| Shared logic needs a new module/utility/base class | software-architect Review Mode → [language]-data-engineer Implement Mode |
| Clone spans packages / inverts a dependency to fix | software-architect Review Mode first |
| Clone is a justified exemption | No action — record in the Exemptions table |
When you engage software-architect thinking inside this skill, keep the work local to the current request and do NOT publish to Confluence unless the user explicitly asks.
---
Decision Rules
- If no clones exceed the threshold, stop after the scan and report a clean result.
- Rank by duplicated lines/tokens and by severity, not by raw count of pairs.
- Limit deep proposals to the top 3–5 clones unless the user asks for exhaustive analysis.
- Never recommend an abstraction that would couple two callers that should stay independent.
A little duplication is cheaper than the wrong abstraction.
- Do not implement the deduplication from this skill. Hand fixes to the downstream skill
once the strategy is accepted.
---
Output Format
## Clean Code Duplication Review — [target_path]
**Language:** [auto | python | javascript | csharp | rust | go]
**Detector:** [jscpd | pmd-cpd | pylint | dupl | ...]
**Thresholds:** min-tokens [N], min-lines [N]
**Duplicated lines:** [X.XX%]
**Clone pairs:** [N]
### Clones
| Rank | Type | Severity | Lines | Location A | Location B | Strategy | Route |
|------|------|----------|-------|------------|------------|----------|-------|
### Exemptions
| Clone | Reason not to deduplicate |
|-------|---------------------------|
### Deduplication Proposal — [clone rank/name]
**Clone type:** [Type-1..4]
**Sites:** `[file:lines]`, `[file:lines]`
**Proposed strategy:** [extract function | parameterize | shared module | ...]
**Proposed home:** [where the consolidated code should live]
**Recommended next step:**
- `clean-code-refactor` (`mode: smells`) only, or
- `software-architect` review + `[language]-data-engineer` implementation---
Feedback
If the user corrects this skill's output due to a misinterpretation or missing rule in the skill itself (not a one-off preference), invoke skill-feedback to capture structured feedback and optionally post a GitHub issue.
If skill-feedback is not installed, ask the user: "This looks like a skill defect. Would you like to install the `skill-feedback` skill to report it?" If the user declines, continue without feedback capture.
interface:
display_name: "Clean Code Duplication"
short_description: "Detect duplicated code and propose a dedup plan"
default_prompt: "Use $clean-code-duplication to find duplicated code with per-language detectors and propose a deduplication plan routed to clean-code-refactor."
Clone Report Template
Use this template for all clean-code-duplication output.
---
## Clean Code Duplication Review — [target_path]
**Language:** [auto | python | javascript | csharp | rust | go]
**Detector:** [jscpd | pmd-cpd | pylint | eslint-sonarjs | dupl | cargo-dupes]
**Thresholds:** min-tokens [N], min-lines [N]
**Files scanned:** [N]
**Duplicated lines:** [X.XX%]
**Clone pairs:** [N] (HIGH: N, MEDIUM: N, LOW: N)
**Exemptions:** [N]
---
### Clones
| Rank | Type | Severity | Lines | Tokens | Location A | Location B | Strategy | Route |
|------|------|----------|-------|--------|------------|------------|----------|-------|
| 1 | Type-2 | HIGH | 38 | 210 | `a.py:40-77` | `b.py:12-49` | Extract + parameterize | refactor |
| 2 | Type-1 | LOW | 7 | 60 | `x.ts:5-11` | `y.ts:8-14` | Extract function | refactor |
Route legend: `refactor` = `clean-code-refactor` (`mode: smells`);
`architect` = `software-architect` review → `[language]-data-engineer` implementation.
---
### Exemptions
| Clone | Reason not to deduplicate |
|-------|---------------------------|
| `migrations/0007.py` ↔ `migrations/0008.py` | Generated ORM migrations |
| `user_factory.py` ↔ `order_factory.py` | Coincidental; merging couples unrelated domains |
---
### Deduplication Proposal — Clone [rank]
**Clone type:** [Type-1 | Type-2 | Type-3 | Type-4]
**Severity:** [HIGH | MEDIUM | LOW]
**Sites:**
- `[file:start-end]`
- `[file:start-end]`
**What is duplicated:** [one or two sentences describing the shared logic]
**Difference between the copies:** [for Type-2/3 — what varies, e.g. only the field names]
**Proposed strategy:** [extract function | extract + parameterize | extract common core |
shared utility module | template method / strategy | generic / type parameter]
**Proposed home:** [where the consolidated code should live, and why]
**Coupling check:** [confirm the abstraction does not force unrelated callers together]
**Recommended next step:**
- `clean-code-refactor` (`mode: smells`, `standard: [general|ob]`) — local extraction, or
- `software-architect` Review Mode → `[language]-data-engineer` Implement Mode — new shared
module / cross-boundary change
---
### Verification
After the fix is applied, re-run detection to confirm the clone is gone and no new ones
appeared:
python3 skills/clean-code-duplication/scripts/report_duplication.py [target_path] \ --language [language]
Then run the language quality gate (see `references/languages/[language].md`).Clean Code Duplication Thresholds
Use these as the default triage gates for clean-code-duplication.
These are soft limits. A clone over the limit is a review candidate, not an automatic failure. The real question is whether consolidating the duplication reduces the chance of a bug being fixed in one copy and missed in another — without coupling callers that should stay independent.
Default Detection Thresholds
A block must be at least this large before a detector reports it as a clone.
| Detector | Threshold flag | Default | Notes |
|---|---|---|---|
| jscpd (cross-language) | --min-tokens / --min-lines | 50 tokens / 5 lines | Token-based; the runner's default |
| PMD CPD | --minimum-tokens | 100 (no hard default — always set it) | AST-token based; 50 is aggressive, 100 balanced, 150 conservative |
| pylint (Python) | min-similarity-lines | 4 | Line-similarity based |
| eslint-plugin-sonarjs (JS/TS) | no-identical-functions option | 3 lines | Function-level only |
| dupl / golangci-lint (Go) | threshold | 150 tokens | Token-based |
| cargo-dupes (Rust) | --min-tokens (tool-dependent) | tool default | AST-normalized |
Tuning guidance
- Start at the tool default, then raise the threshold if the report is dominated by
trivial snippets (imports, getters, guard clauses).
- Lower the threshold only when hunting a specific suspected clone, and expect noise.
- Token thresholds are more stable than line thresholds across formatting styles.
Clone Type Taxonomy
Classify every reported clone — the type drives the fix.
| Type | Name | Definition | Detected well by |
|---|---|---|---|
| Type-1 | Exact | Identical except whitespace, layout, comments | jscpd, PMD CPD, all tools |
| Type-2 | Renamed | Type-1 plus renamed identifiers, types, literals | jscpd, PMD CPD, dupl |
| Type-3 | Gapped / near-miss | Type-2 plus inserted, deleted, or changed statements | PMD CPD, dupl, cargo-dupes |
| Type-4 | Semantic | Different code, same behaviour | None reliably — needs human/LLM review |
Token-based tools (jscpd, dupl) catch Type-1 and Type-2 well, some Type-3. AST-based tools (PMD CPD, cargo-dupes) catch more Type-3. Type-4 will not appear in any tool report — find it by reading the flagged areas and the surrounding code.
Severity
Rate severity by blast radius, not line count.
- HIGH — duplicated complex or business-critical logic; a fix in one copy will silently
miss the others. Validation rules, money math, auth checks, parsing.
- MEDIUM — repeated structure with variations; ongoing maintenance drag but low
correctness risk.
- LOW — small repeated snippets; consolidate opportunistically, do not block on them.
Justified Duplication (Exempt)
Report these as exemptions rather than targets:
- generated code (protobuf/gRPC stubs, ORM migrations, parser output)
- test fixtures, snapshots, and table-driven cases that are deliberately explicit
- framework-mandated boilerplate (DTOs, config records)
- coincidental similarity where merging would couple two unrelated concerns
- performance-critical paths where an abstraction adds overhead
- cases where the duplication is genuinely cheaper than the abstraction
The wrong-abstraction rule: a little duplication is far cheaper than the wrong
abstraction. If unifying two clones would force one caller to carry parameters or branches
that only the other needs, leave them duplicated and say so.
What to Do After a Clone Trips the Threshold
1. Read both clone sites in full, plus enough surrounding code to judge coupling. 2. Classify the clone type and assign severity. 3. Decide whether the fix is local (private helper in the same file/module → clean-code-refactor) or structural (new shared home → software-architect then [language]-data-engineer). 4. Propose the smallest abstraction that removes the duplication without coupling unrelated callers.
Clean Code Duplication — C#
Per-language duplicate-detection tooling for C# / .NET. Read alongside the general clean-code-duplication SKILL.md and duplication-thresholds.md.
---
Recommended Detectors
| Tool | Clone types | When to use |
|---|---|---|
| PMD CPD | Type-1, Type-2, some Type-3 | Primary CLI detector for C# since dupFinder was retired |
| jscpd | Type-1, Type-2 | Default cross-language; no JVM needed |
| SonarQube / SonarCloud | Type-1, Type-2, Type-3 | Enterprise dashboards; deepest analysis |
JetBrains dupFinder is discontinued. The ReSharper Command-Line Tools dupFinderstopped shipping after the 2021.2 release. It still runs but receives no language updates,
so do not depend on it for modern C#. Rider/ReSharper keep an interactive "Find Similar
Code" inspection in-IDE, and TeamCity's Duplicates Finder build step uses the same engine.
---
PMD CPD (primary)
pmd cpd --minimum-tokens 100 --language cs --dir . --format markdown--language csis the C# id (confirm withpmd cpd --help).--minimum-tokensis mandatory; 100 balances signal and noise for C#.- Install: download the PMD 7 binary distribution, or
brew install pmd, thenpmd cpd .... - Report formats:
text,xml,csv,markdown.
---
jscpd (default)
npx --yes jscpd --format csharp --min-tokens 50 .The bundled runner wraps this:
python3 skills/clean-code-duplication/scripts/report_duplication.py . --language csharp---
SonarQube / SonarAnalyzer (deeper, optional)
SonarAnalyzer.CSharp runs as a Roslyn analyzer in-build, but copy/paste duplication is reported by the SonarQube/SonarCloud server, not by the standalone NuGet analyzer. Use this when the team already runs Sonar; it gives a duplicated-lines density metric and a clone browser across the whole solution.
---
Notes & Limitations
- Exclude generated code:
*.Designer.cs,*.g.cs,obj/,bin/, EF Core migrations,
and gRPC/protobuf output. Record any remaining generated clones as exemptions.
- Common legitimate repetition: DTO/record property blocks, mapping profiles, and
controller boilerplate — judge by behaviour, not shape.
Verification (after the fix)
dotnet build --warningsaserrors
dotnet test
python3 skills/clean-code-duplication/scripts/report_duplication.py . --language csharpClean Code Duplication — Go
Per-language duplicate-detection tooling for Go. Read alongside the general clean-code-duplication SKILL.md and duplication-thresholds.md.
---
Recommended Detectors
| Tool | Clone types | When to use |
|---|---|---|
| dupl | Type-1, Type-2, some Type-3 | Canonical Go clone detector; AST/token based |
| golangci-lint `dupl` | Type-1, Type-2 | Same engine, inside the standard Go lint flow |
| jscpd | Type-1, Type-2 | Default cross-language; no Go toolchain step |
| PMD CPD | Type-1, Type-2, some Type-3 | Alternative AST-token detector |
---
dupl (primary)
go install github.com/mibk/dupl@latest
dupl -threshold 100 .-threshold(alias-t) is the minimum token-sequence size; default100. Raise it to
cut noise, lower it to hunt a specific clone.
- A directory argument is searched recursively for
*.go; no path means the current dir. -plumbinggives script-parseable output;-htmlwrites a browsable report.
---
golangci-lint dupl linter
Same detector, wired into the aggregator most Go projects already run.
# .golangci.yml
linters:
enable:
- dupl
linters-settings:
dupl:
threshold: 150golangci-lint runThe golangci-lint default threshold is 150 (more conservative than standalone dupl).
---
jscpd (default cross-language)
npx --yes jscpd --format go --min-tokens 50 ./The bundled runner wraps this:
python3 skills/clean-code-duplication/scripts/report_duplication.py . --language go---
PMD CPD
pmd cpd --minimum-tokens 100 --language go --dir . --format markdown---
Notes & Limitations
- Exclude generated code:
*_gen.go,*.pb.go(protobuf), mocks, and_test.gotable
data when it is intentionally explicit. Record remaining generated clones as exemptions.
- Idiomatic Go favours a little duplication over premature abstraction — be conservative
about introducing generics or interfaces purely to remove a clone.
if err != nil { return ... }blocks are not clones worth removing; tools usually skip
them at sensible thresholds.
Verification (after the fix)
go vet ./...
golangci-lint run
go test ./...
gofmt -l .
python3 skills/clean-code-duplication/scripts/report_duplication.py . --language goClean Code Duplication — JavaScript / TypeScript
Per-language duplicate-detection tooling for JavaScript and TypeScript. Read alongside the general clean-code-duplication SKILL.md and duplication-thresholds.md.
---
Recommended Detectors
| Tool | Clone types | When to use |
|---|---|---|
| jscpd | Type-1, Type-2 | Default; jscpd was built for JS and supports JSX/TSX |
| eslint-plugin-sonarjs | Type-1 (function-level) | Inline in the existing ESLint flow / editor / CI |
| PMD CPD | Type-1, Type-2, some Type-3 | Deeper AST-token detection for near-miss clones |
---
jscpd (default)
npx --yes jscpd --format "javascript,jsx,typescript,tsx" --min-tokens 50 src/The bundled runner wraps this:
python3 skills/clean-code-duplication/scripts/report_duplication.py src/ --language javascriptjscpd ships extra reporters useful here: --reporters html for a browsable report, --reporters sarif for GitHub code scanning, --reporters ai for token-efficient output.
---
eslint-plugin-sonarjs
Catches duplication at the function and expression level, surfaced as ordinary lint errors.
npm install --save-dev eslint-plugin-sonarjsFlat config (eslint.config.js):
import sonarjs from "eslint-plugin-sonarjs";
export default [
sonarjs.configs.recommended,
{
rules: {
"sonarjs/no-identical-functions": ["error", 3], // min lines, default 3
"sonarjs/no-duplicate-string": ["error", { threshold: 3 }],
},
},
];no-identical-functions— flags functions with identical bodies (≥ N lines).no-duplicate-string— flags a string literal repeated ≥ threshold times (extract a
constant).
- Set
parserOptions.projectwith@typescript-eslint/parserso type-aware rules work. - Scope: function/expression level only — pair it with jscpd for block-level clones.
---
PMD CPD
# JavaScript
pmd cpd --minimum-tokens 100 --language ecmascript --dir src/ --format markdown
# TypeScript
pmd cpd --minimum-tokens 100 --language typescript --dir src/ --format markdownRun pmd cpd --help to confirm the language ids for your PMD version (ecmascript, typescript).
---
Notes & Limitations
- Exclude build output and bundles:
**/dist/**,**/build/**,**/*.min.js,
node_modules (the runner ignores these by default).
- Common false positives: generated API clients, story files, snapshot tests, and
framework component scaffolding. Record as exemptions.
Verification (after the fix)
tsc --noEmit
eslint src/
vitest run # or: jest
python3 skills/clean-code-duplication/scripts/report_duplication.py src/ --language javascriptClean Code Duplication — Python
Per-language duplicate-detection tooling for Python. Read alongside the general clean-code-duplication SKILL.md and duplication-thresholds.md.
---
Recommended Detectors
| Tool | Clone types | When to use |
|---|---|---|
| jscpd | Type-1, Type-2 | Default, fast, cross-language; the bundled runner uses it |
| pylint `duplicate-code` (R0801) | Type-1, Type-2 | Already in most Python CI; line-similarity based |
| PMD CPD | Type-1, Type-2, some Type-3 | Deeper, AST-token based; best for near-miss clones |
---
jscpd (default)
npx --yes jscpd --format python --min-tokens 50 --reporters console src/The bundled runner wraps this:
python3 skills/clean-code-duplication/scripts/report_duplication.py src/ --language python---
pylint duplicate-code (R0801)
Built into pylint via the similarities checker. Run it in isolation:
pylint --disable=all --enable=duplicate-code --min-similarity-lines=6 src/Configure in pyproject.toml:
[tool.pylint.similarities]
min-similarity-lines = 6
ignore-comments = true
ignore-docstrings = true
ignore-imports = true
ignore-signatures = truemin-similarity-linesis the key threshold. The default of4is noisy;6–8is a
better triage gate. Do not set it to 0 — that flags every line, it does not disable the check. Disable with disable = ["R0801"] instead.
ignore-importsandignore-signaturesremove the most common false positives.- R0801 reports duplicated blocks across files but does not tell you how to refactor —
that is this skill's job.
---
PMD CPD
pmd cpd --minimum-tokens 100 --language python --dir src/ --format markdown--minimum-tokensis mandatory; 100 is a balanced starting point.- Run
pmd cpd --helpto confirm the language id for your PMD version (it ispython). - Report formats:
text(default),xml,csv,markdown.
---
Notes & Limitations
- All three tools are syntactic — they will not find Type-4 (same behaviour, different
code). Find those by reading the flagged modules.
- Common Python false positives: dataclass/
__init__boilerplate, test fixtures,
Alembic/Django migrations, argparse setup blocks. Record these as exemptions.
Verification (after the fix)
ruff check src/
mypy src/
pytest
python3 skills/clean-code-duplication/scripts/report_duplication.py src/ --language pythonClean Code Duplication — Rust
Per-language duplicate-detection tooling for Rust. Read alongside the general clean-code-duplication SKILL.md and duplication-thresholds.md.
---
Recommended Detectors
| Tool | Clone types | When to use |
|---|---|---|
| jscpd | Type-1, Type-2 | Default cross-language; the bundled runner uses it |
| PMD CPD | Type-1, Type-2, some Type-3 | AST-token based; better near-miss detection |
| cargo-dupes | Type-1, Type-2, Type-3 | Rust-native; normalizes the AST via syn |
Clippy does not detect duplication. cargo clippy catches idiom and correctnesslints, not clones. Do not rely on it for this skill.
---
jscpd (default)
npx --yes jscpd --format rust --min-tokens 50 src/The bundled runner wraps this:
python3 skills/clean-code-duplication/scripts/report_duplication.py src/ --language rust---
PMD CPD
PMD 7 added Rust support to CPD.
pmd cpd --minimum-tokens 100 --language rust --dir src/ --format markdownConfirm the language id with pmd cpd --help (it is rust).
---
cargo-dupes (Rust-native)
Parses each function, method, and closure with syn, normalizes identifiers to positional placeholders, then groups by fingerprint — so it catches renamed (Type-2) and near-miss (Type-3) clones that a pure token scan misses.
cargo install cargo-dupes
cargo dupesIt reports which functions are duplicates, where they are, and the fingerprint group. Newer and less battle-tested than jscpd/PMD; cross-check its findings before acting.
---
Notes & Limitations
- Exclude
target/and generated code (build.rsoutput,prost/tonicprotobuf, macro
expansions). Record remaining generated clones as exemptions.
matcharms and trait-impl blocks often look duplicated but are structurally required —
consider a macro or a generic only when the duplication is genuinely harmful.
Verification (after the fix)
cargo clippy -- -D warnings
cargo test
cargo fmt --check
python3 skills/clean-code-duplication/scripts/report_duplication.py src/ --language rust#!/usr/bin/env python3
"""Report duplicated code blocks using jscpd as the cross-language detector.
jscpd is the universal default for this skill: a single npm package that detects
copy/paste clones across all five supported languages (Python, JavaScript/TypeScript,
C#, Rust, Go) using the Rabin-Karp algorithm. This wrapper runs jscpd with the JSON
reporter, normalizes the pairwise clone output, and prints a ranked report.
This script does not install anything. If jscpd is unavailable it prints install
instructions and exits non-zero so the caller can fall back to a native per-language
tool documented in references/languages/<language>.md.
"""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
import sys
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path
# Our language names mapped to the jscpd format names they should scan.
LANGUAGE_FORMATS = {
"python": ["python"],
"javascript": ["javascript", "jsx", "typescript", "tsx"],
"csharp": ["csharp"],
"rust": ["rust"],
"go": ["go"],
}
DEFAULT_IGNORE_GLOBS = [
"**/node_modules/**",
"**/.git/**",
"**/.venv/**",
"**/venv/**",
"**/dist/**",
"**/build/**",
"**/target/**",
"**/__pycache__/**",
"**/*.min.js",
]
# jscpd defaults — a clone must be at least this large to be reported.
DEFAULT_MIN_TOKENS = 50
DEFAULT_MIN_LINES = 5
@dataclass(frozen=True)
class CloneFile:
path: str
start_line: int
end_line: int
@dataclass(frozen=True)
class CloneReport:
format: str
lines: int
tokens: int
first: CloneFile
second: CloneFile
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Report duplicated code blocks via jscpd."
)
parser.add_argument("target_path", help="File or directory to scan.")
parser.add_argument(
"--language",
choices=("auto", "python", "javascript", "csharp", "rust", "go"),
default="auto",
help="Restrict scanning to one language. Defaults to auto (all five supported).",
)
parser.add_argument(
"--min-tokens",
type=int,
default=DEFAULT_MIN_TOKENS,
help=f"Minimum duplicated tokens to report. Defaults to {DEFAULT_MIN_TOKENS}.",
)
parser.add_argument(
"--min-lines",
type=int,
default=DEFAULT_MIN_LINES,
help=f"Minimum duplicated lines to report. Defaults to {DEFAULT_MIN_LINES}.",
)
parser.add_argument(
"--top",
type=int,
default=15,
help="Maximum number of clone pairs to print.",
)
parser.add_argument(
"--format",
choices=("markdown", "json"),
default="markdown",
help="Output format for this wrapper.",
)
return parser.parse_args()
def resolve_jscpd_command() -> list[str] | None:
"""Prefer a globally installed jscpd; fall back to npx; otherwise None."""
if shutil.which("jscpd"):
return ["jscpd"]
if shutil.which("npx"):
return ["npx", "--yes", "jscpd"]
return None
def selected_formats(language: str) -> list[str]:
if language == "auto":
formats: list[str] = []
for language_formats in LANGUAGE_FORMATS.values():
formats.extend(language_formats)
return formats
return LANGUAGE_FORMATS[language]
def run_jscpd(
command: list[str],
target_path: Path,
min_tokens: int,
min_lines: int,
formats: list[str],
output_directory: Path,
) -> None:
arguments = [
*command,
str(target_path),
"--min-tokens",
str(min_tokens),
"--min-lines",
str(min_lines),
"--reporters",
"json",
"--output",
str(output_directory),
"--format",
",".join(formats),
"--mode",
"mild",
"--silent",
]
for ignore_glob in DEFAULT_IGNORE_GLOBS:
arguments.extend(["--ignore", ignore_glob])
completed = subprocess.run(
arguments,
capture_output=True,
text=True,
check=False,
)
# jscpd exits non-zero only when the duplication threshold is breached, which is
# still a successful scan for our purposes. A missing report file is the real
# failure signal, so we do not raise on a non-zero return code here.
report_path = output_directory / "jscpd-report.json"
if not report_path.exists():
raise SystemExit(
"jscpd did not produce a report.\n"
f"stdout:\n{completed.stdout}\n"
f"stderr:\n{completed.stderr}"
)
def load_clone_reports(output_directory: Path) -> tuple[list[CloneReport], dict]:
report_path = output_directory / "jscpd-report.json"
raw = json.loads(report_path.read_text(encoding="utf-8"))
statistics = raw.get("statistics", {})
duplicates = raw.get("duplicates", [])
reports: list[CloneReport] = []
for duplicate in duplicates:
first = duplicate.get("firstFile", {})
second = duplicate.get("secondFile", {})
reports.append(
CloneReport(
format=duplicate.get("format", "unknown"),
lines=int(duplicate.get("lines", 0)),
tokens=int(duplicate.get("tokens", 0)),
first=CloneFile(
path=first.get("name", "?"),
start_line=int(first.get("start", 0)),
end_line=int(first.get("end", 0)),
),
second=CloneFile(
path=second.get("name", "?"),
start_line=int(second.get("start", 0)),
end_line=int(second.get("end", 0)),
),
)
)
reports.sort(key=lambda report: (report.lines, report.tokens), reverse=True)
return reports, statistics
def duplication_percentage(statistics: dict) -> float:
totals = statistics.get("total", {})
percentage = totals.get("percentage")
if percentage is not None:
return float(percentage)
return 0.0
def render_markdown(
reports: list[CloneReport],
statistics: dict,
top_n: int,
) -> str:
percentage = duplication_percentage(statistics)
header_lines = [
"## Duplication Report",
"",
f"Clone pairs: {len(reports)}",
f"Duplicated lines: {percentage:.2f}%",
"",
]
if not reports:
return "\n".join(
header_lines + ["No duplicated blocks exceeded the configured threshold."]
)
table_lines = [
"| Rank | Lines | Tokens | Format | Location A | Location B |",
"|------|-------|--------|--------|------------|------------|",
]
for index, report in enumerate(reports[:top_n], start=1):
location_a = f"`{report.first.path}`:{report.first.start_line}-{report.first.end_line}"
location_b = f"`{report.second.path}`:{report.second.start_line}-{report.second.end_line}"
table_lines.append(
f"| {index} | {report.lines} | {report.tokens} | {report.format} | "
f"{location_a} | {location_b} |"
)
if len(reports) > top_n:
table_lines.extend(
["", f"Showing top {top_n} clone pairs out of {len(reports)}."]
)
return "\n".join(header_lines + table_lines)
def render_json(
reports: list[CloneReport],
statistics: dict,
top_n: int,
) -> str:
return json.dumps(
{
"duplicated_percentage": duplication_percentage(statistics),
"clone_pairs": len(reports),
"items": [asdict(report) for report in reports[:top_n]],
},
indent=2,
)
def main() -> int:
args = parse_args()
target_path = Path(args.target_path).resolve()
if not target_path.exists():
raise SystemExit(f"Target path does not exist: {target_path}")
command = resolve_jscpd_command()
if command is None:
raise SystemExit(
"jscpd is not available and npx was not found.\n"
"Install it with one of:\n"
" npm install -g jscpd\n"
" npx --yes jscpd <path>\n"
"Or fall back to the native per-language tool documented in\n"
"references/languages/<language>.md (PMD CPD, pylint, dupl, etc.)."
)
with tempfile.TemporaryDirectory() as temporary_directory:
output_directory = Path(temporary_directory)
run_jscpd(
command=command,
target_path=target_path,
min_tokens=args.min_tokens,
min_lines=args.min_lines,
formats=selected_formats(args.language),
output_directory=output_directory,
)
reports, statistics = load_clone_reports(output_directory)
if args.format == "json":
print(render_json(reports, statistics, args.top))
return 0
print(render_markdown(reports, statistics, args.top))
return 0
if __name__ == "__main__":
sys.exit(main())