Spec
docs/design/specs/codeanalyzer-dotnet.md (this repo)
Summary
Adds C# to CLDK as a new language pack: codeanalyzer-dotnet, a Roslyn-based analyzer
shipped as a self-contained binary on PyPI under the package name cansharp. It emits
canonical schema v2 (analysis.json plus a Neo4j projection) and is consumed by a new
csharp model package and facade in python-sdk.
Schema impact: additive only, per the parity clause. New kind values (record,
record_struct, delegate, property, event) and new typed fields (parts,
declared_in, ref_kind, positional_parameters, invoke_signature, is_auto,
project). No shared field renamed, no shared kind repurposed, no new edge family.
The analyzer is greenfield — there is no v1 output to migrate from, so it targets v2
directly.
Affected repos
codeanalyzer-dotnet — the analyzer itself; new repo, currently empty
python-sdk — new cldk/models/csharp/ and cldk/analysis/csharp/; cansharp pinned in
dependencies and [tool.backend-versions]
docs — language-support matrix and C# quickstart
Design decisions
Sixteen decisions are recorded in the spec. The load-bearing ones:
- Scope is C# only; the language token is
csharp. VB.NET and F# can be added later as
additive tokens in the same repo. A dotnet token would lie about scope and force an id
migration when VB arrives.
- No MSBuild. The analyzer reads
obj/project.assets.json and the .csproj directly and
calls CSharpCompilation.Create. This keeps the shipped binary genuinely self-contained and
avoids MSBuildLocator version coupling, which is the most common failure mode for
Roslyn-based tools. Cost: source generators do not run, and only the first target framework
of a multi-targeted project is analyzed.
- Roslyn's own
ControlFlowGraph, flattened to statement nodes, rather than a
hand-built CFG — async state machines, iterator lowering, using, foreach, and pattern
switches arrive already lowered.
- Partial types merge into one
type node homed at the primary declaring file, with
parts[] and declared_in on out-of-file members so span.bytes slices the right
module.source. Roslyn already merges them semantically, so the merged view is the natural
output.
- Properties and events get both views — a
field node and, for non-auto accessors,
#get/#set callables — because idiomatic C# exposes state as properties but accessors
hold real logic that L3 must see.
- By-ref parameters carry
ref_kind at L1 and get their own formal_out at L4, so
TryParse-style output is not a dataflow dead end. C# is the first language to exercise the
canonical schema's by-ref formal_out slot.
- Distribution: self-contained single-file ReadyToRun binaries, one per-platform wheel on
PyPI. NativeAOT was rejected — Roslyn's reflection paths break under trimming in ways that
surface at runtime on user code rather than in our tests.
- Sibling divergences followed to
codeanalyzer-python: span offsets are UTF-8 bytes
(D15) and Neo4j labels are per-language prefixed, :CsNode / CS_* (D16). In both cases
codeanalyzer-typescript currently does the opposite; reconciling it is out of scope here
and recorded in the spec's § 8.
The initial instruction was to emit schema v1 for SDK compatibility. On inspection
python-sdk 1.5.0 already consumes v2 from both its pinned analyzers
(codeanalyzer-python==0.3.1, codeanalyzer-typescript==0.4.3), and no dotnet consumer
exists yet, so v2 was confirmed instead.
Scope guard — explicitly out:
- VB.NET and F#
- Source-generator execution
- A .NET points-to oracle. L4 ships with a flow-insensitive alias stub; no DDG edge may claim
prov:["points-to"] until a real oracle exists, and choosing one is its own design session.
- Reconciling
codeanalyzer-typescript to D15 or D16
- Entrypoint and CRUD detection (the framework/domain axis, not the structural schema)
Release plan
| Release |
Contents |
Gated on |
cansharp 0.1.0 |
L1 + L2, JSON + Neo4j, five platform wheels |
L1/L2 schema gates, Neo4j conformance |
python-sdk 1.6.0 |
cldk/models/csharp/ + facade, pins cansharp==0.1.0 |
0.1.0 on PyPI |
cansharp 0.2.0 |
L3 (CFG / CDG / DDG) |
L3 gates, monotonicity |
python-sdk 1.7.0 |
L3 surface; prov literal accepts reaching-defs |
0.2.0 on PyPI |
cansharp 0.3.0 |
L4 |
its own points-to design session first |
docs |
language matrix, C# quickstart |
tracks 0.1.0 |
Version lockstep: each python-sdk release pins an exact cansharp version in both
dependencies and [tool.backend-versions], as it already does for the Python, Java, and
TypeScript analyzers. The analyzer always ships first.
Release-plan risk: five wheels at roughly 40–60MB each. PyPI's default 100MB per-file limit
accommodates this, but if compressed size creeps past it a limit increase must be requested
before the first release, not discovered on release day.
Definition of done (epic-level)
- Every sub-issue closed with its gate green.
cansharp on PyPI with working wheels for osx-arm64, osx-x64, linux-x64, linux-arm64, and
win-x64; pip install cansharp && cansharp -i <repo> produces valid analysis.json on each.
- All six fixtures analyze clean:
Console.Basic, Api.Di, Forms.Partial,
Modern.Records, Async.Iterators, Solution.MultiProject.
L1 ⊆ L2 ⊆ L3 ⊆ L4 monotonicity gate green at every shipped level, modulo the sanctioned
callee: null → id refinement.
- No dangling endpoints in any edge list at any level.
- Byte-slice gate green including the non-ASCII fixture —
source[span.bytes] equals the
declaration text under UTF-8 offsets (D15).
- Neo4j conformance green against generated
schema.neo4j.json, with the _k discriminant
on CS_DDG and CS_CFG_NEXT and keyed MERGE materializing exactly row-count
relationships.
python-sdk round-trips every fixture through the csharp facade; prov literal accepts
reaching-defs; the body accessor honours declared_in for partial-type members.
.claude/SCHEMA_DECISIONS.md in codeanalyzer-dotnet carries all sixteen decisions.
- Docs language matrix lists C#; CHANGELOGs updated; versions pinned in lockstep.
Spec
docs/design/specs/codeanalyzer-dotnet.md(this repo)Summary
Adds C# to CLDK as a new language pack:
codeanalyzer-dotnet, a Roslyn-based analyzershipped as a self-contained binary on PyPI under the package name
cansharp. It emitscanonical schema v2 (
analysis.jsonplus a Neo4j projection) and is consumed by a newcsharpmodel package and facade inpython-sdk.Schema impact: additive only, per the parity clause. New
kindvalues (record,record_struct,delegate,property,event) and new typed fields (parts,declared_in,ref_kind,positional_parameters,invoke_signature,is_auto,project). No shared field renamed, no sharedkindrepurposed, no new edge family.The analyzer is greenfield — there is no v1 output to migrate from, so it targets v2
directly.
Affected repos
codeanalyzer-dotnet— the analyzer itself; new repo, currently emptypython-sdk— newcldk/models/csharp/andcldk/analysis/csharp/;cansharppinned independenciesand[tool.backend-versions]docs— language-support matrix and C# quickstartDesign decisions
Sixteen decisions are recorded in the spec. The load-bearing ones:
csharp. VB.NET and F# can be added later asadditive tokens in the same repo. A
dotnettoken would lie about scope and force an idmigration when VB arrives.
obj/project.assets.jsonand the.csprojdirectly andcalls
CSharpCompilation.Create. This keeps the shipped binary genuinely self-contained andavoids MSBuildLocator version coupling, which is the most common failure mode for
Roslyn-based tools. Cost: source generators do not run, and only the first target framework
of a multi-targeted project is analyzed.
ControlFlowGraph, flattened to statement nodes, rather than ahand-built CFG — async state machines, iterator lowering,
using,foreach, and patternswitches arrive already lowered.
typenode homed at the primary declaring file, withparts[]anddeclared_inon out-of-file members sospan.bytesslices the rightmodule.source. Roslyn already merges them semantically, so the merged view is the naturaloutput.
fieldnode and, for non-auto accessors,#get/#setcallables — because idiomatic C# exposes state as properties but accessorshold real logic that L3 must see.
ref_kindat L1 and get their ownformal_outat L4, soTryParse-style output is not a dataflow dead end. C# is the first language to exercise thecanonical schema's by-ref
formal_outslot.PyPI. NativeAOT was rejected — Roslyn's reflection paths break under trimming in ways that
surface at runtime on user code rather than in our tests.
codeanalyzer-python: span offsets are UTF-8 bytes(D15) and Neo4j labels are per-language prefixed,
:CsNode/CS_*(D16). In both casescodeanalyzer-typescriptcurrently does the opposite; reconciling it is out of scope hereand recorded in the spec's § 8.
The initial instruction was to emit schema v1 for SDK compatibility. On inspection
python-sdk1.5.0 already consumes v2 from both its pinned analyzers(
codeanalyzer-python==0.3.1,codeanalyzer-typescript==0.4.3), and no dotnet consumerexists yet, so v2 was confirmed instead.
Scope guard — explicitly out:
prov:["points-to"]until a real oracle exists, and choosing one is its own design session.codeanalyzer-typescriptto D15 or D16Release plan
cansharp0.1.0python-sdk1.6.0cldk/models/csharp/+ facade, pinscansharp==0.1.0cansharp0.2.0python-sdk1.7.0provliteral acceptsreaching-defscansharp0.3.0docsVersion lockstep: each
python-sdkrelease pins an exactcansharpversion in bothdependenciesand[tool.backend-versions], as it already does for the Python, Java, andTypeScript analyzers. The analyzer always ships first.
Release-plan risk: five wheels at roughly 40–60MB each. PyPI's default 100MB per-file limit
accommodates this, but if compressed size creeps past it a limit increase must be requested
before the first release, not discovered on release day.
Definition of done (epic-level)
cansharpon PyPI with working wheels for osx-arm64, osx-x64, linux-x64, linux-arm64, andwin-x64;
pip install cansharp && cansharp -i <repo>produces validanalysis.jsonon each.Console.Basic,Api.Di,Forms.Partial,Modern.Records,Async.Iterators,Solution.MultiProject.L1 ⊆ L2 ⊆ L3 ⊆ L4monotonicity gate green at every shipped level, modulo the sanctionedcallee: null → idrefinement.source[span.bytes]equals thedeclaration text under UTF-8 offsets (D15).
schema.neo4j.json, with the_kdiscriminanton
CS_DDGandCS_CFG_NEXTand keyedMERGEmaterializing exactly row-countrelationships.
python-sdkround-trips every fixture through thecsharpfacade;provliteral acceptsreaching-defs; the body accessor honoursdeclared_infor partial-type members..claude/SCHEMA_DECISIONS.mdincodeanalyzer-dotnetcarries all sixteen decisions.