Status: PORT COMPLETE (2026-07-20) — walker codegraph-kernel/src/csharp.rs,
all gates passed (parity sweeps 0-diff on serilog 211/216 / Newtonsoft.Json
914/945 / jellyfin 2104/2105 — every deferral matching §arch-6's predictions;
full-init dump gates byte-identical ×3; kernel-csharp-parity suite + grammar
row; suite ×2 under CODEGRAPH_KERNEL_EXPECT=1; DEFAULT_ROUTED += csharp). This
doc remains the quirk reference for the walker. Survey basis: every
TS-side branch a .cs file exercises, with file:line anchors as of f1ca991
(HEAD at survey time, clean main). Every grammar-shape claim below was probed
against the vendored tree-sitter-c-sharp 0.23.5 wasm
(src/extraction/wasm/tree-sitter-c_sharp.wasm — note the underscore filename);
probe scripts + raw outputs live in the survey session scratchpad
(/private/tmp/claude-501/-Users-colby-Development-CodeGraph-codegraph/0c11bda1-0b19-4fec-bcd9-d0cb4b2d6e8a/scratchpad/svy-csharp/:
probe1-core.mjs, probe2-shapes.mjs, probe3-edge.mjs, census-wasm.mjs,
error-incidence.mjs, blank-impl.mjs + probe*-out.txt). Read WITH
docs/design/rust-kernel-migration-plan.md (§0a recipe, §5 gates) and the
rust/ccpp checklists (format precedents). Blocking findings: none — the
vendored wasm is table-identical to the crates.io 0.23.5 crate (below), so no
grammar bump is needed; grammar prep is a Cargo pin + parity-test entry only.
Grammar provenance (verified, not assumed):
80db274e, 2026-06-07 — the #237
primary-constructor fix), replacing tree-sitter-wasms' ABI-13 build. The
commit message says "tree-sitter-c-sharp 0.23.5 (ABI 15)" but does NOT claim a
parser.c sha-match, so this survey verified compatibility directly against the
crates.io tree-sitter-c-sharp-0.23.5 tarball
(src/parser.c sha256 0a2651e4…, src/scanner.c sha256 00920daa…;
upstream tag v0.23.5 → commit cac6d5fb):
LANGUAGE_VERSION 15;stateCount 8053 == parser.c STATE_COUNT 8053;nodeTypeCount 533 == SYMBOL_COUNT 530 + ALIAS_COUNT 3;fieldCount 26 with a field-name set identical to the crate's
node-types.json (accessors…value);declaration, expression, pattern, … —
which never instantiate; zero real divergence);scanner.c, 12 external tokens)
and the wasm embeds it — probed: interpolated raw strings
($"""… {Interp(u)} …"""), raw string literals, and verbatim strings all
parse clean.tree-sitter-c-sharp = "=0.23.5" to
codegraph-kernel/Cargo.toml following the exact-pin comment block (the
=0.24.2 c/rust rows); crate symbol is tree_sitter_c_sharp::LANGUAGE
(a LanguageFn, .into() like the other 0.23-era crates); register
"csharp" => Some(tree_sitter_c_sharp::LANGUAGE.into()) in
codegraph-kernel/src/langs.rs + add "csharp" to LANGUAGES. The crate's
build.rs compiles parser.c and scanner.c (both in the tarball) — no extra
work. __tests__/kernel-grammar-parity.test.ts (id-by-id ABI + node-kind +
field-table compare) is the real gate and needs a csharp row; no wasm
change and no TS-side behavior change — the production wasm stays exactly
as shipped since #717, so this port has NO grammar-bump-first step.csharpExtractor.preParse = blankCsharpPreprocessorDirectives
(languages/csharp.ts:26, #237) blanks #if/#elif/#else/#endif directive
LINES (both branches of the guarded code are KEPT). The route point already
applies it for kernel calls — preParsedSource (kernel/index.ts:82) names
csharp explicitly — so both arms parse identical blanked bytes and the
deferred-file memo reuses them (takeDeferredPreParse, kernel/index.ts:120;
wasm fallback sourceIsPreParsed, tree-sitter.ts:499). Never port the
regex to Rust: it is a JS (?m)^-anchored replace
(/^([ \t]*)#[ \t]*(if|elif|else|endif)\b[^\n]*/gm) whose ^ matches after
\r (JS multiline semantics) and whose [^\n]* EATS the \r of a CRLF
line (probed: "…\r\n#if X\r\n…" blanks to "…\r\n \n…" — byte count
preserved, \r becomes a space). Both are JS-semantics traps (the #1329
class) that the hoist makes moot.aspnetResolver
(resolution/frameworks/csharp.ts:11, languages: ['csharp'], registered in
frameworks/index.ts:62) has an extract() hook (line 133), and
parse-worker.ts:93-100 forces any language with an applicable framework
extract() onto the decoded extractFromSource path. detect()
(csharp.ts:15) fires on .csproj AspNetCore markers, Program.cs/
Startup.cs shapes, or controller-source signatures — so web-app repos
(jellyfin-class) decode per file while pure libraries (serilog,
Newtonsoft.Json) ride the raw buffers-to-store transport. Don't chase a
raw-path perf number on an ASP.NET repo and conclude the port is broken.codegraph-kernel/src/csharp.rs), registered
in langs.rs; per-file has_error() → defer: like every walker. java.rs
is the skeleton (§java.rs mapping below) — same namespace-node concept,
same field/constant split, same static-member/value-ref/fn-ref chassis..cs → csharp at detectLanguage (grammars.ts:87), wasm filename
tree-sitter-c_sharp.wasm (grammars.ts:31), vendored-set membership at
grammars.ts:292, provenance comment at grammars.ts:252-261. No content
sniffing, no dialects (.cshtml/.razor are language razor, a T3
standalone extractor — NOT this port). MAX_FILE_SIZE (1 MiB) and
generated-file skips are orchestrator/TS-side and shared.--max-deferral 0.1 — C# needs
NO c/cpp-style 0.5 exemption. The residual error class is grammar-inherent
both-branches-kept damage (#if FEATURE_DEFAULT_INTERFACE around interface
members in serilog's ILogger.cs; half-expression #if HAVE_DATE_TIME_OFFSET
guards in Newtonsoft's JsonReader.cs) — those files error on BOTH arms and
defer by policy. C# 12 collection expressions ([a, b]) parse natively
(probed) — the lone jellyfin error is unrelated.Types: functionTypes=[] — C# has NO function branch anywhere (this single
fact kills several generic paths below: no nested-fn extraction, no
arrow-name recovery, extractMethod's fallback-to-extractFunction is
unreachable on non-erroring files). classTypes=[class_declaration,
record_declaration] with classifyClassNode (csharp.ts:69): a
record_declaration with an anonymous struct keyword child → 'struct', else
'class' (probed: EVERY record form is record_declaration — record struct
/ readonly record struct included; record_struct_declaration in structTypes
is forward-compat only, the grammar has no such node).
methodTypes=[method_declaration, constructor_declaration];
interfaceTypes=interface_declaration; structTypes=[struct_declaration, record_struct_declaration];
enumTypes=[enum_declaration]; enumMemberTypes=[enum_member_declaration];
typeAliasTypes=[] (a using Alias = … NEVER makes a type_alias — it goes
through extractImport, quirk below);
packageTypes=[namespace_declaration, file_scoped_namespace_declaration];
importTypes=[using_directive]; callTypes=[invocation_expression];
variableTypes=[local_declaration_statement]; fieldTypes=[field_declaration];
propertyTypes=[property_declaration]. nameField=name, bodyField=body,
paramsField=parameters, returnField=type (note: type is a dead value
for methods — the 0.23.x grammar renamed the method return field to
returns, and both consumers, getReturnType and extractCsharpTypeRefs, read
returns themselves; returnField is only reachable via the generic
extractTypeAnnotations path which csharp short-circuits out of).
Hooks PRESENT (port each exactly):
returns field; predefined_type (void/int/string/…) or array_type →
undefined; else raw text trim → strip trailing \?+ (nullable) → strip
/<[^>]*>/g (generics) → last .-segment → must match /^[A-Za-z_]\w*$/
else undefined. QUIRKS (same class as rust): the non-nested <[^>]*> strip
breaks on nested generics — Task<List<Foo>> → "Task>" → regex fails →
undefined (single-level Task<Widget> → Task survives);
Ns.Foo → Foo; Foo? → Foo. Constructors have no returns field →
undefined. This feeds the #645/#608 chained-call resolution
(name-matcher.ts:2156 matchDottedCallChain) — emission shape must hold.node.child(i)) for
type modifier; FIRST text match among public/private/protected/internal
wins; none → 'private'. Modifiers are individual named modifier children
(probed — NO Java-style modifiers wrapper). So protected internal →
'protected', private protected → 'private'. Called for classes,
structs, enums, methods, properties, fields (NOT interfaces — extractInterface
never asks).modifier child with text static.const modifier → true; else static AND
readonly both present → true. Consumed ONLY by extractField's kind gate
(tree-sitter.ts:2058-2062) and extractVariable:2546 (moot — see
§local_declaration_statement).modifier child with text async.
WORKS for C# (unlike rust's dead-code isAsync) — probed: async is a
direct modifier child of method_declaration.qualified_name's
text, else FIRST namedChild of type identifier's text, else null. Probed
shapes and the resulting QUIRKS (all PRESERVE):
using System; → identifier → System.using System.Collections.Generic; → qualified_name → full dotted text.using static System.Math; → qualified_name → System.Math (the static
keyword is an anonymous token — no trace).global using GlobalNs.Thing; → same as a plain using (the global
keyword is anonymous) → GlobalNs.Thing.using Alias = Some.Type<T>; → the find(qualified_name) hits the
TARGET, whose raw text INCLUDES generic args — moduleName
System.Collections.Generic.Dictionary<string, int> (probed verbatim).using Short = SomeType; (single-identifier target) → no qualified_name
→ find(identifier) returns the FIRST identifier, which is the name:
field = the ALIAS → moduleName Short, the target SomeType is lost.if (this.extractor.extractImport) return;) means NO
import node at all — mirror that gate.Hooks ABSENT (the walker must NOT do these): getSignature — C# methods,
constructors and classes carry signature: undefined (only properties/fields
get signatures, built inline by their extractors); getReceiverType (receiver
machinery inert — extractMethod:1799 owner-lookup never runs); resolveName,
recoverMangledName, isMisparsedFunction, isExported (every node's
isExported is undefined; the file node's is false), resolveBody,
classifyMethodNode, extractPropertyName, extraClassNodeTypes,
extractModifiers, synthesizeMembers (no Lombok analogue — confirmed; the
java.rs Lombok section has no C# counterpart), extractBareCall, visitNode
hook, skipBodilessClass (bodiless CLASSES still mint nodes — but see
extractStruct/extractEnum's own body gates), methodsAreTopLevel,
interfaceKind.
extract() (tree-sitter.ts:531-532) calls extractFilePackage(root) ONCE,
before the walk: it scans root's DIRECT namedChildren for the FIRST
packageTypes node, calls extractPackage (csharp.ts:86 — name field ??
first qualified_name/identifier child; probed: both namespace forms carry a
name: field, identifier or qualified_name), and mints ONE
kind:'namespace' node (name = dotted text, e.g. My.App; id =
generateNodeId(file,'namespace','My.App',line); contains edge from the file
node) which stays PUSHED for the ENTIRE file walk. Consequences (all probed,
all PRESERVE):
:: but the namespace segment keeps its dots:
class Widget in namespace My.App → QN My.App::Widget, method →
My.App::Widget::Build.namespace Outer { namespace Inner { class Deep } })
leaves NO trace of Inner — Deep's QN is Outer::Deep; a file with TWO
top-level namespaces scopes the SECOND one's types under the FIRST's node —
namespace Second { class Other } after namespace Outer {…} → Other has
QN Outer::Other and a contains edge from the Outer namespace node.namespace My.App.Sub; behaves identically (node spans just
the declaration line).using directives INSIDE a namespace block are reached by the generic walk
and behave exactly like top-of-file ones; ALL import refs and import nodes
in a namespaced file hang off the namespace node (nodeStack top), not the
file node.isInsideClassLikeNode() (1486) checks the stack TOP against
class/struct/interface/trait/enum/module — namespace is NOT class-like,
so top-level types under a namespace extract exactly like file-scope ones.f1ca991)| Node | Branch | Behavior |
|---|---|---|
class_declaration |
classTypes:1005 → classify:1007 → extractClass:1679 | kind class; body walk via declaration_list; skipChildren |
record_declaration |
classTypes → classify | struct keyword child → extractStruct:1869 (record struct); else extractClass. Bodiless record Empty; still mints a class node (no skipBodilessClass; body-walk loop then iterates the record node's own children harmlessly) |
struct_declaration |
structTypes:1059 → extractStruct:1869 | body (declaration_list) required: bodiless struct Fwd; mints NO node (1876 exempts only record_declaration) |
interface_declaration |
interfaceTypes:1054 → extractInterface:1834 | kind interface; NO visibility read; members visited with interface pushed (class-like) — so interface methods/properties become method/property nodes |
enum_declaration |
enumTypes:1064 → extractEnum:1914 | body enum_member_declaration_list required (bodiless → no node); extractInheritance sees base_list → the underlying type : byte emits an extends ref named byte (quirk, §inheritance); enum_member_declaration children → extractEnumMembers:1958 — name field path: ONE enum_member node per member, positioned at the member node (attributes included in its span), values/attributes ignored; non-member children (preproc_*, comment) → visitNode (no-op) |
method_declaration |
methodTypes:1027 → extractMethod:1737 | classifyMethodNode absent → always extractMethod. Gate 1747 passes via class-like (a method_declaration outside a type does not occur in non-erroring C# — top-level void M(){} parses as local_function_statement, probed); bodyless interface/partial signatures mint nodes with no body walk; expression-bodied methods have body: arrow_expression_clause (a real body FIELD, probed) → walked |
constructor_declaration |
methodTypes → extractMethod | name field = the class-name identifier → method node named like the class; returnType undefined; constructor_initializer (: base(args) / : this(args)) is a sibling of the body field → NEVER walked → calls inside initializer args are LOST (probed); expression-bodied ctor body = arrow_expression_clause → walked |
property_declaration (inside class-like) |
propertyTypes:1075 → extractProperty:1986 | property node + scanFnRefSubtree (capture-only) + skipChildren → accessor bodies (get { … }, get => …) and the => expr value clause are NEVER walked — calls inside property getters/setters/expression bodies emit NOTHING (only fn-ref candidates). §property below |
field_declaration (inside class-like) |
fieldTypes:1084 → extractField:2046 | field/constant nodes per declarator + scanFnRefSubtree + skipChildren → field initializers emit no calls/instantiates/static-member refs (fn-ref candidates only). §field below |
local_declaration_statement |
variableTypes:1098 (only reachable at top level — global statements; body locals go through visitFunctionBody instead) | not class-like → extractVariable:2538 → generic fallback (2863-2881) finds no direct identifier/variable_declarator children (the declarator nests inside variable_declaration, probed) → ZERO nodes minted; isClassScopeConstantAssignment (1508) needs node.type assignment → never true. skipChildren=true + scanFnRefSubtree → a top-level var builder = WebApplication.CreateBuilder(args); produces NO node, NO calls ref, NO instantiates — only fn-ref candidates. PRESERVE |
using_directive |
importTypes:1209 → extractImport:3170 | hook (§config) → import node + ONE generic imports ref {fromNodeId: nodeStack top (namespace node if present, else file), referenceName: moduleName, line/col of the directive}; no per-binding emitter (the TS/py/rust/php/ruby ladder at 3197-3234 excludes csharp) |
invocation_expression (top level — global statements) |
callTypes:1248 → extractCall:3684 | fires with caller = file/namespace node; children still visited (no skipChildren) so nested invocations recurse |
object_creation_expression (top level, expression-statement position) |
INSTANTIATION_KINDS:354/1255 → extractInstantiation:4610 | + findAnonymousClassBody:4815 checks for a class_body/declaration_list child — probed: C# object_creation children are [new, type, argument_list, initializer_expression] — the anon branch NEVER fires on non-erroring C# (C# has no anonymous classes; object initializers are initializer_expression). Mirror the check anyway (java.rs:1550 already has it) |
global_statement / expression_statement / await_expression / lambdas / namespace_declaration / preproc_* / global_attribute / delegate_declaration / event_declaration / event_field_declaration / operator_declaration / conversion_operator_declaration / indexer_declaration / destructor_declaration / local_function_statement / type_parameter_constraints_clause |
no branch | fall through → children visited. Consequences (all probed, all PRESERVE): delegates mint NO node; events mint NO node; operators/conversions/indexers/destructors mint NO node but their bodies' calls attribute to the ENCLOSING CLASS (visitNode descends into accessor/body blocks with the class still on top); local functions mint NO node (functionTypes empty — visitFunctionBody:5245 gate never passes) and their bodies' calls attribute to the enclosing method (or file node at top level); where T : IEntity constraint types emit NOTHING (constraints are class-node children outside the body — never visited by extractClass's body loop, and extractCsharpTypeRefs doesn't walk them) |
property_signature / method_signature / impl_item / export/store branches |
TS/rust-only node types | never C# |
generateNodeId(filePath, kind, name, startRow+1) =
`${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}`
(tree-sitter-helpers.ts:18). File node id = literal file:${filePath}
(tree-sitter.ts:509). Same-(kind,name,line) ID collisions are routine —
dedupe/self-checks compare ID STRINGS (node_ids vec pattern; partial
methods partial void Hook(); + partial void Hook() { } are two method
nodes on different lines — fine).captureValueRefScope after every create (1374).:: (buildQualifiedName:1447;
namespacePrefix always empty for csharp — it's cpp-only). No receiver QNs
(no getReceiverType).file, name basename, qualifiedName = filePath, endLine =
source.split('\n').length (post-preParse source — identical bytes both
arms), isExported false.declaration_list namedChildren via visitNode → pop. No
synthesizeMembers. EXACT ORDER of the three pre-body calls is
emission-order-sensitive.record struct M(…);
mints the node, walks nothing). NOTE extractStruct does NOT call
extractDecoratorsFor — struct attributes emit nothing anyway.Task<T> Get(int id); → method node
(no body); string Label { get; } → property node; default-impl methods
(arrow or block body) → method node WITH body walk.: byte extends quirk);
body loop: enumMemberTypes children → extractEnumMembers (name-field path:
1960-1963 — one enum_member node, NO docstring, NO value), everything else
→ visitNode. Enum members with attributes: the member NODE's position starts
at the attribute_list (probed — [Obsolete] ReadAsInt = 1 spans from [).docstring; visibility; isStatic (?? false — always a concrete bool). Name =
name field (property_declaration always has one, probed). Type/signature:
isTsJsField false → typeNode = FIRST namedChild whose type is NOT
modifier/modifiers/identifier/accessor_list/accessors/equals_value_clause
(2013-2017); signature = typeText name (the .replace(/^:\s*/,'') is inert
for C#) or bare name. QUIRKS (probed, PRESERVE):
public Widget Parent
{ get; init; }) finds NO typeNode (identifier is excluded) → signature =
"Parent" — identifier-typed properties lose their type in the
signature (predefined_type string, generic_name List<Foo>,
qualified_name, nullable_type etc. all keep theirs).public int Computed => MaxItems + 1;) has
children [modifier, predefined_type, identifier, arrow_expression_clause
(value: field)] → typeNode = predefined_type → signature "int Computed";
the arrow clause is NEVER walked (calls inside lost).{ get; } = new(); initializers: the value: implicit_object_creation and
the accessor_list are both skipped/excluded → no refs, no instantiates.Then extractDecoratorsFor (no-op) and extractTypeAnnotations (2037) →
extractCsharpTypeRefs — the type field IS walked for refs (so public
List<Foo> Items emits references List + Foo even though the signature
kept the raw text). Return value feeds no body walk (the classifyMethodNode
initializer-walk path at 1031-1047 is TS-only).
(java|csharp) && isConst(node) → constant (const
fields, static readonly fields), else field. Evaluated once per
DECLARATION — all declarators share it.variable_declarator filter finds none → the C#
wrapper path (2070-2075): the variable_declaration child's
variable_declarator children (probed: field_declaration → [modifier…,
variable_declaration[type field, declarator, declarator…]]).type node's raw text.name field (?? first identifier); signature =
`${typeText} ${name}`; createNode(kind, name, decl — the
declarator node, so line/col/ID anchor to the declarator); then
extractDecoratorsFor(node, id) (no-op) and extractTypeAnnotations(node,
id) → extractCsharpTypeRefs on the OUTER field_declaration — which walks
the variable_declaration's type field (5905-5909) → multi-declarator
fields (Foo A, B;) emit the type refs ONCE PER DECLARATOR, each from its
own field node.event_field_declaration is NOT a field_declaration — no nodes (§dispatch).receiverType = undefined (no hook) → gate 1747 passes via class-like →
name = name field (constructor: the class name; explicit interface impl
void IDisposable.Dispose(): name = Dispose — the
explicit_interface_specifier is a separate child, probed); no
isMisparsedFunction. Node props: docstring, signature: undefined (no
getSignature hook), visibility, isAsync (real), isStatic, returnType
(getReturnType — constructors undefined). Then extractTypeAnnotations (1816 →
extractCsharpTypeRefs: returns-field refs FIRST, then per-parameter type
refs — order matters), extractDecoratorsFor (1819, no-op), push, walk the
body FIELD only (block or arrow_expression_clause), pop. The
receiver-contains lookup (1799-1813) never runs (no receiverType).
Generic else-branch (4312+): func = childForFieldName('function') ??
namedChild(0). C# invocation_expression always has a function: field
(probed). The 4364 member_expression/field_expression branch does NOT match
member_access_expression, so C# calls route as:
func.type === 'member_access_expression' → the csharp branch
(4502-4517): recv = expression field, methodName = name field text.
invocation_expression (chained call Foo.Create(1).Bar()):
innerFunc = recv's function field; innerCallee = innerFunc raw text
with /\s+/g stripped; calleeName = `${innerCallee}().${methodName}`
(empty innerCallee → bare methodName). C# re-encodes EVERY
call-receiver chain — no capitalization gate (unlike kotlin/scala):
GetThing().Bar() → GetThing().Bar; multi-line
Foo.Create(1)\n .Bar() → Foo.Create().Bar (whitespace stripped on the
INNER only).this.Run(x) → this.Run (SKIP_RECEIVERS never applies — the 4400
set is in the branch C# doesn't take); base.Method() → base.Method
(recv is the anonymous-token base via the field — childForFieldName
returns it); _repo.Save(x) → _repo.Save;
builder\n .Services\n .AddSingleton<IRepo, Repo>() → the full
multi-line text builder\n .Services\n .AddSingleton<IRepo, Repo>
(generic args included, newlines included); literal receivers are NOT
filtered — "lit".ToUpper() → "lit".ToUpper (LITERAL_RECEIVER_TYPES
is consulted only in the non-C# branch); p!.Force() → p!.Force;
2-hop reads keep everything.func.type === 'scoped_identifier' (4499) — never C# (no :: node).Helper() → Helper;
generic invocation Generic<int>(local) → func = generic_name →
Generic<int> kept verbatim (the template strip at 4542 is
c/cpp-gated); nameof(Widget) → func = identifier nameof → a calls
ref named nameof (probed — nameof is a plain invocation in this
grammar); conditional access request?.Method() → func =
conditional_access_expression → raw text request?.Method; delegate
parens (myDel)(x) → func = parenthesized_expression → text (myDel) →
the conversion regex at 4530 (/^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$/)
FIRES → myDel (the one shared normalization C# actually hits — port it).Post-processing: template strip (4542) and cpp fn-ptr fan-out (4556) are
c/cpp-gated — NOT csharp. Final push: one calls ref {fromNodeId = nodeStack
top, referenceName, line = call startRow+1, column = call startColumn
(UTF-16)}. Children of the invocation are still traversed afterward (both
walkers), so inner calls of a chain emit separately: Foo.Create(1).Bar() →
refs Foo.Create().Bar AND Foo.Create. typeof(…), default(…), casts,
is/as patterns, element access, switch expressions, ??/ternary,
interpolated-string interpolations, tuple expressions: no dedicated handling —
plain recursion reaches any invocation_expression inside them (probed shapes
in probe3-out.txt).
ctor = constructor/type(hit)/name/namedChild(0) fields → the type: field
(probed). Generic path: raw text → strip from first < (new List<Foo>() →
List) → vbnet paren strip (inert) → keep last ./:: segment
(new Ns.Foo() → Foo) → trim → instantiates ref at the
object_creation_expression's position. Fires from visitNode:1255 (expression
statements at top level) AND visitFunctionBody:5145. QUIRKS, PRESERVE:
implicit_object_creation_expression (new()) and
anonymous_object_creation_expression (new { X = 1 }) and
array_creation_expression (new Widget[10]) are NOT in INSTANTIATION_KINDS
→ no instantiates refs (target-typed new() — everywhere in modern C# — is
invisible); object/collection initializer args and new[] { Mk() } contents
still recurse to their own calls. Top-level var w = new Widget(); emits
nothing at all (§local_declaration_statement).
Called for EVERY node in visitFunctionBody (5218) — body walker only (never
visitNode, so class-level field initializers and top-level statements emit no
static refs). Node gate: MEMBER_ACCESS_TYPES (323) contains
member_access_expression. Skip when the access IS a call's callee (4772-4779:
parent ∈ callTypes && callee.startIndex === node.startIndex — so
Console.WriteLine(…)'s access is skipped but DoThing(Constants.MAX)'s
argument is not). recv = expression field (hit) → must be type
identifier/type_identifier/simple_identifier/name/scoped_type_identifier —
for C# only identifier occurs — and text must match /^[A-Z][A-Za-z0-9_]*$/
→ references ref {name = recv text, line/col of RECV}. Probed:
ReadType.ReadAsDouble → ref ReadType; Outer.Inner.DEEP → the outer
access's recv is a member_access (skip) but the INNER Outer.Inner emits ref
Outer; lowercase receivers (u.Age, builder.Services) → nothing; this.X
→ recv type this → nothing.
extractDecoratorsFor (4897) accepts node types decorator/annotation/
marker_annotation/attribute (+solidity modifier_invocation). C# attributes are
attribute_list nodes containing attribute children, and attribute_list
is (a) a DIRECT child of the declaration (class/method/enum-member — probed),
so the direct-children scan sees attribute_list (not in the set → skipped,
and its attribute grandchildren are never reached); (b) never inside a
modifiers wrapper (C# has none); (c) as a PRECEDING sibling — only for
global_attribute, which is never a decorated declaration. Net: zero
decorates refs for C#. The walker needs no decorator code — but keep the
call sites' ORDER slots (they emit nothing). Corollary (contrast rust): an
attribute between a doc comment and its declaration does NOT break the
docstring chain — the attribute_list is a child of the declaration, so the
comment remains previousNamedSibling (probed: /// doc + [Attr] + method →
docstring found).
The ONLY child type that matters for C# types is base_list (extends/
implements/superclass/base_class_clause etc. never occur). The branch iterates
ALL namedChildren of base_list with NO type filter and emits ONE
extends ref each (C# never emits implements from extraction — the
comment at 5574-5576 documents the deliberate conflation; interface-vs-class
splitting happens at resolution/synthesis):
identifier → text (BaseItem, IWidget).generic_name → its FIRST identifier child's text (ClientBase<Widget> →
ClientBase) — but position = the generic_name node.qualified_name → FULL dotted text (Sys.ICloneable — no last-segment
reduction).class Svc(…) :
Base(repo), IThing parses base_list children as [identifier(Base),
argument_list((repo)), identifier(IThing)] → emits extends Base, extends
(repo) (!), extends IThing. The (repo) ref is garbage that never
resolves — PRESERVE it.record UserDto(…) : BaseDto(Name), IThing → base_list children are
[primary_constructor_base_type, identifier] → emits extends
BaseDto(Name) (full text, argument list included) + IThing.**enum ReadType : byte puts
predefined_type(byte) in base_list → extends ref byte (likewise : int,
etc.).extractInterface/extractStruct/extractEnum all route through the same branch. The 5652 recursion (field_declaration_list/class_heritage) never matches C#.
extractTypeAnnotations (5788) short-circuits into extractCsharpTypeRefs (5798-5801) — the generic params/return/type_annotation path below it never runs for C#. extractCsharpTypeRefs (5893):
type field ?? returns field → walkCsharpTypePosition
(methods: returns; properties: type; constructors: neither).variable_declaration child's type field (fields) → walk.parameters field → per parameter child (type filter — probed:
bracketed_parameter_list of indexers never reaches here since indexers
aren't dispatched) → its type field → walk.walkCsharpTypePosition (5955): predefined_type → nothing; identifier →
references ref unless name ∈ BUILTIN_TYPES (5768 — port the WHOLE set
verbatim: the Java/C# row int/long/short/byte/float/double/char, plus
cross-language rows — a C# type named String/Boolean/error IS suppressed
(Scala/Go rows) while Task/List/IRepo/dynamic are emitted —
probed: dynamic parses as identifier → a references ref named dynamic);
qualified_name → LAST .-segment as referenceName (position = the whole
qualified_name node) unless builtin; tuple_element → its type field only
(element names gated out); everything else (generic_name, nullable_type,
array_type, pointer_type, tuple_type, ref_type, scoped_type, …) → recurse ALL
namedChildren — so List<Foo> emits List AND Foo; Task<List<Foo>> emits
Task, List, Foo; Foo?/Bar[] unwrap; generic args inside
type_argument_list all surface. Called from extractMethod (params+returns),
extractProperty (type), extractField (per declarator), and
extractCsharpPrimaryCtorParamRefs.
extractCsharpPrimaryCtorParamRefs (5938) — from extractClass:1707 and
extractStruct:1895 (NOT interface/enum): finds a parameter_list child BY
TYPE (the primary ctor hangs as an unnamed-field child — probed on class,
record, record struct), walks each parameter's type field →
references refs from the TYPE node (class Svc(IRepo repo, ICache cache) →
refs IRepo, ICache). Extension-method this params ride the normal
parameter path (the this is a modifier child; type field still walked).
Body-local type annotations: NONE. visitFunctionBody:5230's
variable_declarator branch calls extractVariableTypeAnnotation (6074), which
looks for a type_annotation child — C# has no such node (the type is a field
of variable_declaration) → structurally inert. Cast/typeof/is/as/catch types
emit nothing (§extractCall).
/// XML doc comments and // comments are comment nodes (probed);
consecutive preceding named siblings accumulate (unshift → source order);
cleanCommentMarkers strips ^\/\/[/!]?\s? per line — so the stored docstring
is the XML markup minus slashes (<summary>Doc line.</summary>).
DOCSTRING_WRAPPER_TYPES contains no C#-relevant wrappers (variable_declaration
/variable_declarator ARE in the set but no C# docstring call site passes
those nodes — extractField/extractProperty pass the outer declaration).
Block /** */ comments are also comment nodes and /*-stripped. Attributes
don't break the chain (§decorators). **Reuse docstring::preceding_docstring
js_multiline_strip as-is** — the CRLF ^-after-\r semantics (#1329) are
already handled there; C#'s XML docs on CRLF checkouts hit exactly that path.
Docstring recipients: class/record/struct/interface/enum/method/ctor/property/
field (NOT enum members, NOT the namespace node, NOT imports).Port the full machinery (crib java.rs:1246 flush_value_refs — Java's cases
are csharp's minus/plus the declarator switch): CODEGRAPH_VALUE_REFS=0 kill;
MAX_VALUE_REF_NODES=20_000 caps the prune scan and each reader scan;
isGeneratedFile skip.
/[A-Z_]/, parent scope id starting
file:/class:/module:/struct:/enum:. For C# that means const /
static readonly fields (kind constant) inside class:/struct: parents —
namespace: parents do NOT qualify (deliberate: a namespace-scoped… doesn't
exist in C# anyway), and C# mints no variable nodes at all. Last
same-named target wins the map; counts accumulate per name.variable_declarator (818) → bump(namedChild(0)) — counts field
declarators (the targets) AND method-body locals AND top-level-statement
declarators (the shadow sources); property_declaration (856) fires but
is inert — no variable_declaration child, and firstSimpleIdentifier
(tree-sitter.ts:261) hunts simple_identifier which C# lacks → bump(null).
All other cases (const_item, assignment, init_declarator, …) are other
grammars' node types — a C# tree never contains them. After the scan:
declCount > fileScopeCount → target deleted (a method-local
var MaxItems = … shadows the class const).identifier whose
text maps to a target, target ≠ self, name ≠ scope's own name → EDGE
{source: scopeId, target: targetId, kind:'references',
metadata:{valueRef:true}}. The constant/name/simple_identifier reader
types are other-language rows — inert but harmless to mirror. Dart/Pascal
sibling-body pull (891) is inert. Flush order: fn-ref candidates flush FIRST
(unresolvedReferences), then value refs (edges) — extract():538-539.idTypes={identifier}; dispatch: argument_list→args,
assignment_expression→rhs(field right) — covers +=/-= event
subscription (compound assignment is still assignment_expression, probed),
initializer_expression→list (object/collection initializers),
variable_declarator→varinit (NO field — the last-named-child path).
layers: argument→null (descend named children). special:
{member_access_expression}. No unwrap/ungatedModes/addressOfOnly.
value field → value = LAST namedChild, require ≥2 namedChildren and value
≠ name child (probed: f = x => Compute(x) → [name, lambda_expression];
initializer-less _repo → 1 child → skipped). Destructuring gate
(object/array/tuple/struct_pattern) never matches C#.this.status = status). button.Click += OnClick → LHS last-name Click
≠ OnClick → candidate.identifier → candidate (NAME_STOPLIST =
this/self/super/null/nil/true/false/undefined/new/NULL/nullptr/None —
note value, args, and base are NOT stoplisted; base/this can't
become candidates anyway, they parse as non-identifier tokens);
member_access_expression → normalizeSpecial (738-746): name field
required; receiver must be this — expr field type this_expression OR
this (the vendored grammar yields the anonymous-token this via the
field — probed; the text-prefix fallback exists for field-less shapes) →
candidate = the BARE member name (this.HandleThing → HandleThing —
NOT this.-prefixed, unlike TS/Java; it then gates against definedHere).
C.StaticHandler (type receiver) → NOTHING. Lambdas/anonymous methods →
nothing (not idTypes/special).lambda_expression —
which IS C#'s lambda node — so at depth>0 the scan STOPS at a lambda in a
field/property initializer (Action A = () => Register(H); yields no
candidates from inside the lambda), while anonymous_method_expression
(delegate() { … }) is NOT in the list and is scanned through. Method-BODY
lambdas are unaffected (visitFunctionBody recursion has no halt — capture
fires per node).this.-prefixed
names skip the gate (C# never produces them — its this-forms are bare);
otherwise name ∈ definedHere ∪ importedNames. definedHere = same-file
function/method node NAMES — for C#: methods + constructors (= class
names); local functions are NOT nodes so their names don't gate in.
importedNames: from imports-kind refs — C# using-refs are dotted →
QUALIFIED_IMPORT (./\ separators) admits the LAST segment
(using MyApp.Helpers; → Helpers), single-segment using Xunit; →
Xunit; the alias-quirk moduleNames ride the same rules. Survivors dedupe
on ${fromNodeId}|${name} → function_ref refs.= new Logger() and Logger lg;-shaped declared types,
covering params via the [=;,)] tail). They consume the recv.Method
member-access text extraction emits — the walker just has to reproduce the
emission shapes above.inner().Method encoding + declared
return types (getReturnType). Resolution-side; extraction contract pinned
above.Crib directly (same chassis, adjust node kinds): Scope/NodeMeta/Extra
structs, create_node + contains + capture_value_ref_scope, push_ref
helpers, visibility/is_static/is_const modifier scans (java.rs:405-447 — but
C# reads bare modifier children, java reads a modifiers wrapper),
extract_field's wrapper-declarator loop (java.rs:704 — C# adds the
variable_declaration indirection), extract_enum/extract_enum_members,
extract_static_member_ref (java.rs:938 — swap field_access →
member_access_expression + expression field), value-ref flush
(java.rs:1246 — swap the declarator cases for variable_declarator +
inert property_declaration), fn-ref capture/flush (java.rs:1120-1243 — swap
JAVA_SPEC for CSHARP_SPEC incl. bare identifiers as idTypes and the
this-receiver special), find_anonymous_class_body (java.rs:1550 — keep,
unreachable), the package/namespace node (java.rs package_declaration ↔
extractFilePackage semantics — but C#'s is FIRST-CHILD-ONLY with the
second-namespace and nested-namespace quirks, and the namespace node kind is
namespace in both). DIVERGES from java.rs — do NOT copy: Lombok synthesis
(java.rs:1339-1548 — no C# counterpart); method_invocation this.field
unwrap + Java's chain encoding (C#'s csharp-branch semantics at
tree-sitter.ts:4502 differ: raw full text, no SKIP_RECEIVERS, unconditional
re-encode); annotations→decorates (C# emits none); anonymous classes
(<T$anon@line> — unreachable for C#); signature_of (C# methods have NO
signature); Java's type_list inheritance (C# is the unfiltered base_list
loop + its three quirk shapes); getVisibility default (private for C#).
csharp-NEW (no java.rs precedent): extractCsharpTypeRefs/walkCsharpTypePosition
(the type-ref engine), extractCsharpPrimaryCtorParamRefs, the
extractProperty branch (java has no propertyTypes), classifyClassNode
record→struct, the extractImport alias/static/global quirks, the
local_declarationstatement zero-emission path, preproc* passthrough.
aspnetResolver (resolution/frameworks/csharp.ts) — detect: .csproj
AspNetCore refs / Program.cs / Startup.cs / controller-source scan.
extract() (csharp.ts:133 — regex over RAW source via
stripCommentsForRegex, runs in extractFromSource AFTER either arm — NO port
needed): emits route nodes with id
`route:${filePath}:${line}:${METHOD}:${path}` (NOT hashed), kind
route, name `${METHOD} ${path}`, qualifiedName
`${filePath}::route:${path}`, language csharp; plus one references
ref per handler FROM the route node (framework refs DO carry
filePath+language, unlike extraction refs). Covers [HttpGet]-family
attributes (bare or with path, class-level [Route] prefix joined) and
minimal-API .MapGet("/p", handler) chains. Because it reads RAW source
(not extraction output), the port cannot break it — but parse-worker
routing (§arch-2) means its presence forces the decoded path.resolve() consumes plain unresolved refs by suffix
(Controller/Service/Repository/ViewModel/Dto) + directory conventions —
extraction-shape-agnostic beyond the referenceName spellings pinned above..slice(0,100) truncations fire for C# (that
path is the variable-initializer signature, unreachable), but signatures
built from raw type text are UTF-16 substrings.js_multiline_strip
(docstring.rs, #1329). The walker itself must add NO (?m)/^/$ regex
without the same scrutiny. getReturnType/extractImport regexes are
single-line and anchor-free except /^[A-Za-z_]\w*$/ (whole-string match —
use Regex::is_match on the full string, \A…\z semantics).has_error() → defer: signal (kernel receives
BLANKED source; incidence per §arch-6). The one-slot defer memo already
covers csharp (preParse hoist listed it from day one).node_ids vec pattern — java.rs already does this).Torture.cs (+ CRLF variant derived in-memory,
normalization-proof) pinning at minimum: block namespace + file-scoped
namespace file + two-namespaces-in-one-file (second nests under first) +
nested namespace (inner leaves no trace); class with base_list (identifier +
generic_name + qualified_name), attributes (NO decorates), /// docstring
incl. doc-over-attribute; record class / record struct / readonly record
struct (+ positional params → primary-ctor type refs) / bodiless
record Empty; (node) / record UserDto(…) : BaseDto(Name), IThing
(extends BaseDto(Name) full-text quirk); C#12 class primary ctor with
base args (extends Base + (repo) + IThing quirk); bodiless
struct Fwd; (NO node); interface with bodyless method + property +
default-impl arrow method; enum with : byte (extends byte quirk),
attributed member, valued members; const + static-readonly (→ constant)
Type name);
protected internal (→ protected); property shapes: predefined-type,
bare-identifier type (signature loses type), generic type, expression-bodied
(=> calls LOST), { get; } = new();, accessor bodies with calls (LOST);
event_field_declaration + event_declaration with add/remove bodies (no
nodes; accessor calls → class); operator + conversion operator + indexer +
destructor (no nodes; body calls → class); constructor with
: base(Compute()) (initializer calls LOST) + expression-bodied ctor
(walked); explicit interface impl (Dispose bare name); local function
(no node, calls → enclosing method); calls: bare, generic (Generic<int>
verbatim), this.Run/base.Method (prefixes kept), _repo.Save,
multi-line fluent chain (raw newlines), literal receiver ("lit".ToUpper
emitted), ?. (raw request?.Method), p!.Force, chained
Foo.Create(1).Bar() (re-encode + inner both) + GetThing().Bar(),
(myDel)(x) (conv regex → myDel), nameof(Widget) (calls ref nameof);
new Widget(…) { … } (instantiates + initializer calls) + new Ns.Foo<T>()
(strip both) + new() / new { } / new Widget[10] (all NOTHING);
static value reads (ReadType.ReadAsDouble → ReadType; Outer.Inner.DEEP
→ Outer; skip-as-callee; lowercase skip); type refs: params
(nullable/array/tuple-element/generic/qualified/dynamic), returns
(Task<List<Foo>> → refs Task+List+Foo but returnType undefined),
suppressed builtins (String, int); where T : IEntity (NO refs);
using forms: plain/dotted/static/global/alias-to-qualified (generic-args
moduleName)/alias-to-identifier (alias-name quirk) + using inside namespace
(refs from namespace node); top-level-statements file: var x = F();
(NOTHING), expression-statement calls (caller = file), top-level local
function + trailing partial class Program; fn-refs: Register(HandleThing),
Register(this.HandleThing) (bare name), Register(C.StaticHandler)
(nothing), Click += OnClick, initializer_expression list, varinit
(Action g = () => … no candidate; Del d = Handler; candidate),
this.x = x param-storage skip; value-refs: const target + reader methods +
a var MaxItems = … local shadow (prune) + static readonly multi-target;
preprocessor: #region/#endregion/#pragma/#nullable/#define
interleaved in class + enum bodies (parse-through, no emissions), and the
#if/#else/#endif-in-enum shape as the DEFER fixture pre-blank — plus
an intentionally-erroring file (e.g. an unclosed brace or the
both-branches-kept interface shape) asserting the defer: signal; LINQ
query + interpolated/raw/verbatim strings + switch expression + collection
expressions (calls inside all reach the graph).scripts/kernel-parity.mjs, ORDER-sensitive
full-object): serilog (small), Newtonsoft.Json (medium), jellyfin
(large) — survey clones live at
/private/tmp/claude-501/-Users-colby-Development-CodeGraph-codegraph/765a9532-0a92-43de-8d50-7c8ca1cb345c/scratchpad/{serilog,Newtonsoft.Json,jellyfin}
(re-clone fresh if gone). Expected deferrals per §arch-6 (2.31% / 3.28% /
0.05%) — default --max-deferral 0.1, NO c/cpp 0.5 exemption; a
double-digit rate means a broken walker, not grammar reality. Then
full-init dump-diffs byte-identical (kernel arm vs CODEGRAPH_KERNEL=0,
scripts/dump-graph.mjs, cmp) on the same three. jellyfin is an ASP.NET
detect() hit — its per-file path is decoded either way (§arch-2); the
dump gate is arm-vs-arm and unaffected.__tests__/kernel-csharp-parity.test.ts (torture + CRLF +
defer fixtures, strict full-object compare, CODEGRAPH_KERNEL_EXPECT=1
aware); add the csharp row to __tests__/kernel-grammar-parity.test.ts
(crate tree_sitter_c_sharp::LANGUAGE vs vendored wasm, id-by-id); existing
pinned behavior lives in __tests__/extraction.test.ts (describe blocks at
1342 C# Extraction, 2548 C# imports, 8924 C# records) and
__tests__/resolution.test.ts — full suite green ×2 with
CODEGRAPH_KERNEL_EXPECT=1.DEFAULT_ROUTED += csharp (src/extraction/kernel/index.ts:37) only
after ALL of the above; changelog rides the existing kernel entry.__tests__/fixtures/kernel-parity/Torture.cs — the full pin list above
(one file is fine; keep the two-namespace case in it).TortureFileScoped.cs — file-scoped namespace + records + primary ctors +
top-level using alias quirks (a file-scoped-ns file cannot host
top-level statements — keep those separate).TortureTopLevel.cs — top-level statements program (global statements,
local function, trailing type decl).TortureDefer.cs — the #if-in-enum ERROR shape with blanking DISABLED
in the test arm… no: blanking is hoisted and always on — instead use a
both-branches-kept interface shape (the serilog ILogger class) or a truly
malformed tail that still errors POST-blank, asserting kernel defer: +
wasm-fallback byte-parity through extractFromSource.(repo) extends quirk, the enum : byte quirk,
nameof calls ref, this./base. callee prefixes, and the
property-signature bare-identifier-type loss — cheap unit pins that make a
future grammar bump scream early.