roslyn-duplicate-detection
Skill2 starsChanged 43 days agoMITView on GitHub
Use to measure code duplication in a C#/.NET repo and to find the duplicated method/accessor/constructor bodies that should become a shared helper - locally, without a Sonar server. Write a throwaway read-only Roslyn program (a single-file `dotnet run detect-clones.cs` app) that normalizes each body to a token-kind stream and groups equal fingerprints, so renamed copies still match. Also use it to verify a de-duplication effort actually hit its target percentage.
In this repository56 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- benchmarkingSkill
- commit-messagesSkill
- csharp-15-syntaxSkill
- csharp-aot-trimmingSkill
- csharp-api-designSkill
- csharp-assembly-inspectionSkill
- csharp-asyncSkill
- csharp-collections-modernSkill
- csharp-concurrency-primitivesSkill
- csharp-data-layoutSkill
- csharp-debuggingSkill
- csharp-discardsSkill
- csharp-disposable-patternsSkill
- csharp-docsSkill
- csharp-error-handlingSkill
- csharp-extension-membersSkill
- csharp-generics-modernSkill
- csharp-guard-clausesSkill
- csharp-json-source-generationSkill
- csharp-language-versionsSkill
- csharp-logging-diagnosticsSkill
- csharp-lspSkill
- csharp-modern-typesSkill
- csharp-nullabilitySkill
- csharp-nullable-migrationSkill
- csharp-nunitSkill
- csharp-pattern-matchingSkill
- csharp-performanceSkill
- csharp-polyfillsSkill
- csharp-source-generatorsSkill
- csharp-static-lambdasSkill
- csharp-string-handlingSkill
- csharp-tunitSkill
- csharp-verificationSkill
- dispatching-subagentsSkill
- dotnet-11-librariesSkill
- dotnet-analyzersSkill
- dotnet-central-package-managementSkill
- dotnet-file-based-appsSkill
- dotnet-msbuild-usingsSkill
- dotnet-multi-targetingSkill
- dotnet-project-hygieneSkill
- dotnet-runSkill
- dotnet-runtime-asyncSkill
- dotnet-slnxSkill
- dotnet-test-platformsSkill
- executing-plansSkill
- performance-bounded-deduplicationSkill
- roslyn-analyzer-performanceSkill
- roslyn-discoverySkill
- roslyn-guard-rewritersSkill
- roslyn-rewritersSkill
- subagent-driven-developmentSkill
- tddSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 13
Use jscpd for the headline number.
- Distinctive: line 26
Run it from the repo root:
- Very specific: line 41
Keep the host repo's discipline in the helper: if it runs on a hot path (an analyzer's per-node callback), the helper stays allocation-free …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Roslyn duplicate detection - find and verify clones, structurally
The duplication percentage a Sonar server reports is computed off a token-based copy-paste detector that runs in CI, not by the analyzer package your build loads - so dotnet build never tells you the number, and you are blind between pushes. You have two good local options, and they answer different questions:
- A token-window tool (jscpd) gives you a fast percentage and finds duplicated runs of tokens anywhere, including a clone buried inside two otherwise-different methods. It is the closest local proxy for the Sonar metric.
npx --yes jscpd src --pattern "src/**/*.cs" --ignore "**/bin/**,**/obj/**,**/tests/**,**/benchmarks/**,**/tools/**,**/*.Designer.cs,**/*.g.cs" --min-tokens 100 --reporters console. - A structural Roslyn detector (this skill) works at the granularity of a whole body - a method, accessor, constructor, operator, or local function - and can canonicalize identifiers and literals so a copy that only renamed its variables still matches (a "type-2" clone). That is exactly the unit you extract into a helper, and it survives the renames a token tool splits on. It parses the real syntax tree, so it never trips on comments or strings.
Use jscpd for the headline number; use this for what to extract and to prove each clone set collapsed. They are complementary - run both.
This is a read-only sibling of roslyn-discovery and roslyn-rewriters: same single-file dotnet run harness, same parse-don't-guess principle. It reports; you (or a rewriter from roslyn-rewriters) do the edit.
How it works
./examples/detect-clones.cs is a complete, runnable detector. It:
- Parses every
.csunder a root (skippingbin/obj/tests/benchmarks/tools/generated). - For each method/accessor/constructor/operator/local-function body over
--min-tokens(default 75), walks itsDescendantTokens()and emits a normalized stream: each token becomes itsSyntaxKind, except identifiers become$idand literals become$lit(unless--exact). Trivia - whitespace and comments - is ignored for free because you are iterating tokens. - Hashes the stream and groups equal hashes. A group of 2+ is a clone set.
- Reports the clone sets (with
file:linespans), the total duplicated line count, and a duplication percentage (duplicated lines / total code lines).
Run it from the repo root:
dotnet run <path-to>/detect-clones.cs -- src --min-tokens 100 --top 30--exact switches to type-1 (identical text) matching, closer to what a token tool counts. The default type-2 mode is what surfaces "these eight analyzers are the same body with a different SyntaxKind" - the highest-value extractions.
Reading the output and acting on it
Each clone set is a candidate for one helper. For each:
- Extract the shared body into a
static internalhelper method, in a domain-named helper class (notDedupHelper). Parameterize only the parts that differ between instances - aSyntaxKind, a selector delegate, a descriptor. If the instances differ in more than a couple of small ways, they may not be a true clone; do not contort call sites to force a merge. - Give every helper you add its own unit test - a real behaviour-asserting test named for what the helper does, covering its cases. A helper with no test is half the job.
- Do not merge coincidental look-alikes. Two bodies that hash the same but mean genuinely different things, or that will evolve independently, are better left apart - say so rather than gluing them together. A false merge that hurts readability is worse than the duplication.
- Keep the host repo's discipline in the helper: if it runs on a hot path (an analyzer's per-node callback), the helper stays allocation-free on the no-diagnostic path - no LINQ, no
DescendantNodes(),netstandard2.0-safe.
Then re-run the detector to prove it: the clone set you extracted should be gone and the percentage should drop. That closing measurement is the point - "I extracted a helper" is not the same as "the duplication metric moved."
Verifying a de-duplication goal
To decide whether a target ("under 1.5%, ideally under 0.5%") is met, run both tools and report both numbers, because they measure differently:
- jscpd's percentage is the closest analogue to the Sonar gate - quote it as the headline.
- the Roslyn detector's clone-set count going to zero (at your
--min-tokens) is the stronger signal that no extractable duplication remains.
State the --min-tokens you used and whether you ran type-1 or type-2 - a lower threshold or type-2 finds more, so a bare percentage without those is not reproducible. If a residual clone set is duplication that should stay (two rules that legitimately look alike but will diverge), name it and exclude it from the verdict rather than pretending it is gone. And note that Sonar's own number will still only settle on the next CI run - the local tools tell you when you are in range, not the official figure.
Notes
- Syntax-only: no build, no
MSBuildWorkspace, no project context needed - justCSharpSyntaxTree.ParseText. Fast enough to run on every iteration. - Method granularity is a feature for the "extract a helper" job and a limitation for fine-grained clones - pair with jscpd when a clone is a fragment, not a whole body.
- It is throwaway. Keep it in this skill's
examples/, or drop a copy in a scratch dir - do not commit it into the product repo unless the team wants a standing check. - Pin
Microsoft.CodeAnalysis.CSharpto a version the box already restores (the example pins4.14.0); syntax-only parsing does not need the newest line.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

