Second R7b port, checklist-first recipe (docs/design/csharp-kernel-port-checklist.md; parity passed FIRST RUN again). No grammar bump — the #717 vendored wasm verified table-identical to crate 0.23.5 (ABI 15, STATE_COUNT 8053, node-kind + field tables); first port with no grammar-prep step. The #237 #if-blanking preParse stays TS-side via the existing route-point hoist. Walker preserves bug-for-bug: the single-namespace-node quirks (second namespace nests under the first, nested namespaces leave no trace, import refs hang off the namespace node), raw member-access callee texts (this./base./literal receivers, multi-line fluent chains) with unconditional chain re-encode, deliberate emission holes (property accessor/expression bodies, ctor initializers, delegates/events/ operators/indexers/local functions, top-level locals), garbage extends refs ((repo) primary-ctor args, BaseDto(Name) record bases, enum : byte), the alias- import moduleName quirks, nameof-as-call, CSHARP fn-ref spec (+= subscription, this.X bare-name form, argument layer, initializer lists), C# type-ref engine (nested-generic returnType failure included), and value-ref shadow pruning. Gates: sweeps 0-diff serilog 211/216 / Newtonsoft.Json 914/945 / jellyfin 2104/2105 (deferrals match the survey's per-repo predictions — both-arm #if damage; default --max-deferral 0.1 holds, no c/cpp exemption); full-init dumps byte-identical ×3 (14.0k/109.1k/210.8k lines); kernel-csharp-parity suite (torture ×3 + CRLF variants + 8 micro-pins + defer) + csharp grammar-parity row; full suite 2,608 ×2 under CODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += csharp (11 langs). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
58 KiB
C# kernel port (R7b) — the bug-for-bug checklist
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):
- Vendored wasm landed in PR #717 (commit
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.iotree-sitter-c-sharp-0.23.5tarball (src/parser.csha2560a2651e4…,src/scanner.csha25600920daa…; upstream tagv0.23.5→ commitcac6d5fb):- wasm ABI 15 == parser.c
LANGUAGE_VERSION 15; - wasm
stateCount8053 == parser.cSTATE_COUNT 8053; - wasm
nodeTypeCount533 ==SYMBOL_COUNT 530 + ALIAS_COUNT 3; - wasm
fieldCount26 with a field-name set identical to the crate's node-types.json (accessors…value); - named node-kind set: wasm ⊆ crate exactly (the 9 crate-only names are
node-types.json supertypes —
declaration,expression,pattern, … — which never instantiate; zero real divergence); - the crate ships an external scanner (
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.
- wasm ABI 15 == parser.c
- Cargo plan: add
tree-sitter-c-sharp = "=0.23.5"tocodegraph-kernel/Cargo.tomlfollowing the exact-pin comment block (the=0.24.2c/rust rows); crate symbol istree_sitter_c_sharp::LANGUAGE(aLanguageFn,.into()like the other 0.23-era crates); register"csharp" => Some(tree_sitter_c_sharp::LANGUAGE.into())incodegraph-kernel/src/langs.rs+ add"csharp"toLANGUAGES. 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.
Architecture decisions
- preParse stays TS-side — already hoisted, port NOTHING.
csharpExtractor.preParse = blankCsharpPreprocessorDirectives(languages/csharp.ts:26, #237) blanks#if/#elif/#else/#endifdirective 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 fallbacksourceIsPreParsed, 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\rof a CRLF line (probed:"…\r\n#if X\r\n…"blanks to"…\r\n \n…"— byte count preserved,\rbecomes a space). Both are JS-semantics traps (the #1329 class) that the hoist makes moot. - ASP.NET repos take the DECODED path, not raw buffers.
aspnetResolver(resolution/frameworks/csharp.ts:11,languages: ['csharp'], registered in frameworks/index.ts:62) has anextract()hook (line 133), and parse-worker.ts:93-100 forces any language with an applicable frameworkextract()onto the decodedextractFromSourcepath. detect() (csharp.ts:15) fires on.csprojAspNetCore markers,Program.cs/Startup.csshapes, 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. - The framework extractor itself needs NO port — regex over raw source (§Frameworks below), runs in extractFromSource:6736-6758 after either arm.
- One walker module (suggest
codegraph-kernel/src/csharp.rs), registered in langs.rs; per-filehas_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→csharpat detectLanguage (grammars.ts:87), wasm filenametree-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/.razorare languagerazor, a T3 standalone extractor — NOT this port). MAX_FILE_SIZE (1 MiB) and generated-file skips are orchestrator/TS-side and shared.- Deferral expectations (measured on the gate repos, post-blank = what the
kernel sees): serilog 5/216 = 2.31% (raw 8.33% — the blanking hoist
halves it), Newtonsoft.Json 31/945 = 3.28% (raw 6.88%), jellyfin
1/2105 = 0.05%. Keep the sweep default
--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_INTERFACEaround interface members in serilog's ILogger.cs; half-expression#if HAVE_DATE_TIME_OFFSETguards 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.
Extractor config (languages/csharp.ts — 163 lines, read it whole)
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] (no interfaceKind → kind
'interface'); 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):
- preParse — TS-side, hoisted (§arch-1). Not ported.
- getReturnType = extractCsharpReturnType (csharp.ts:43) — reads the
returnsfield;predefined_type(void/int/string/…) orarray_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-levelTask<Widget>→Tasksurvives);Ns.Foo→Foo;Foo?→Foo. Constructors have noreturnsfield → undefined. This feeds the #645/#608 chained-call resolution (name-matcher.ts:2156matchDottedCallChain) — emission shape must hold. - getVisibility (csharp.ts:102) — scan ALL children (
node.child(i)) for typemodifier; FIRST text match among public/private/protected/internal wins; none →'private'. Modifiers are individual namedmodifierchildren (probed — NO Java-stylemodifierswrapper). Soprotected internal→'protected',private protected→'private'. Called for classes, structs, enums, methods, properties, fields (NOT interfaces — extractInterface never asks). - isStatic (csharp.ts:115) — any
modifierchild with textstatic. - isConst (csharp.ts:127) —
constmodifier → true; elsestaticANDreadonlyboth present → true. Consumed ONLY by extractField's kind gate (tree-sitter.ts:2058-2062) and extractVariable:2546 (moot — see §local_declaration_statement). - isAsync (csharp.ts:140) — any
modifierchild with textasync. WORKS for C# (unlike rust's dead-code isAsync) — probed:asyncis a directmodifierchild of method_declaration. - extractImport (csharp.ts:149) — signature = trimmed full node text
(UTF-16 substring). moduleName = FIRST namedChild of type
qualified_name's text, else FIRST namedChild of typeidentifier'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(thestatickeyword is an anonymous token — no trace).global using GlobalNs.Thing;→ same as a plain using (theglobalkeyword is anonymous) →GlobalNs.Thing.using Alias = Some.Type<T>;→ the find(qualified_name) hits the TARGET, whose raw text INCLUDES generic args — moduleNameSystem.Collections.Generic.Dictionary<string, int>(probed verbatim).using Short = SomeType;(single-identifier target) → no qualified_name → find(identifier) returns the FIRST identifier, which is thename:field = the ALIAS → moduleNameShort, the targetSomeTypeis lost.- The hook never returns null on real shapes probed; if it did,
tree-sitter.ts:3350 (
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.
The namespace node (extractFilePackage — the biggest structural quirk)
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):
- qualifiedNames join with
::but the namespace segment keeps its dots: classWidgetinnamespace My.App→ QNMy.App::Widget, method →My.App::Widget::Build. - visitNode has NO packageTypes branch — namespace_declaration falls
through the whole ladder and its children are visited generically. So:
a NESTED namespace (
namespace Outer { namespace Inner { class Deep } }) leaves NO trace ofInner— Deep's QN isOuter::Deep; a file with TWO top-level namespaces scopes the SECOND one's types under the FIRST's node —namespace Second { class Other }afternamespace Outer {…}→ Other has QNOuter::Otherand a contains edge from theOuternamespace node. - A file-scoped
namespace My.App.Sub;behaves identically (node spans just the declaration line). usingdirectives 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 —namespaceis NOT class-like, so top-level types under a namespace extract exactly like file-scope ones.
tree-sitter.ts branches (anchors as of f1ca991)
visitNode dispatch — what each C# node hits
| 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# |
Node creation, IDs, qualified names
- createNode (1308): id =
generateNodeId(filePath, kind, name, startRow+1)=`${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}`(tree-sitter-helpers.ts:18). File node id = literalfile:${filePath}(tree-sitter.ts:509). Same-(kind,name,line) ID collisions are routine — dedupe/self-checks compare ID STRINGS (node_idsvec pattern; partial methodspartial void Hook();+partial void Hook() { }are two method nodes on different lines — fine). - resolveBody endLine extension (1329) is a no-op (no hook).
- contains edge from nodeStack top for every created node (1363);
captureValueRefScopeafter every create (1374). - qualifiedName = stack names joined
::(buildQualifiedName:1447; namespacePrefix always empty for csharp — it's cpp-only). No receiver QNs (no getReceiverType). - File node: kind
file, name basename, qualifiedName = filePath, endLine =source.split('\n').length(post-preParse source — identical bytes both arms), isExported false.
extractClass / extractStruct / extractInterface / extractEnum
- extractClass (1679): node = class/record; docstring
(getPrecedingDocstring), visibility, isExported=undefined → createNode(kind
'class') → extractInheritance (1704) → extractCsharpPrimaryCtorParamRefs
(1707) → extractDecoratorsFor (1710, NO-OP for C# — §decorators) → push →
body loop over
declaration_listnamedChildren via visitNode → pop. No synthesizeMembers. EXACT ORDER of the three pre-body calls is emission-order-sensitive. - extractStruct (1869): body gate (record exemption 1876);
createNode('struct') → extractInheritance (1891) → primaryCtor refs (1895) →
body walk only when body exists (bodiless positional
record struct M(…);mints the node, walks nothing). NOTE extractStruct does NOT call extractDecoratorsFor — struct attributes emit nothing anyway. - extractInterface (1834): createNode('interface', {docstring, isExported})
— NO visibility field (never asked); extractInheritance; body loop with
interface pushed. Interface members:
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. - extractEnum (1914): body required; createNode('enum', {docstring,
visibility, isExported}); extractInheritance (→ the
: byteextends quirk); body loop: enumMemberTypes children → extractEnumMembers (name-field path: 1960-1963 — oneenum_membernode, NO docstring, NO value), everything else → visitNode. Enum members with attributes: the member NODE's position starts at the attribute_list (probed —[Obsolete] ReadAsInt = 1spans from[).
extractProperty (1986) — property_declaration
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):
- A property whose declared type is a BARE identifier (
public Widget Parent { get; init; }) finds NO typeNode (identifier is excluded) → signature ="Parent"— identifier-typed properties lose their type in the signature (predefined_typestring, generic_nameList<Foo>, qualified_name, nullable_type etc. all keep theirs). - An expression-bodied property (
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: thevalue: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).
extractField (2046) — field_declaration
- kind (2058-2062):
(java|csharp) && isConst(node)→constant(constfields,static readonlyfields), elsefield. Evaluated once per DECLARATION — all declarators share it. - Declarators: direct
variable_declaratorfilter finds none → the C# wrapper path (2070-2075): thevariable_declarationchild's variable_declarator children (probed: field_declaration → [modifier…, variable_declaration[type field, declarator, declarator…]]). - typeText (2110-2116): first namedChild of the variable_declaration that
isn't modifiers/modifier/variable_declarator/variable_declaration/
marker_annotation/annotation → the
typenode's raw text. - Per declarator: name =
namefield (?? 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'stypefield (5905-5909) → multi-declarator fields (Foo A, B;) emit the type refs ONCE PER DECLARATOR, each from its own field node. - docstring/visibility/isStatic computed once from the outer declaration, shared by all declarators. The PHP property_element and bare-fallback branches (2078-2154) are unreachable for C#.
event_field_declarationis NOT a field_declaration — no nodes (§dispatch).
extractMethod (1737) — method_declaration + constructor_declaration
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).
extractCall (3684) — the C# paths
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 =expressionfield, methodName =namefield text.- recv is
invocation_expression(chained callFoo.Create(1).Bar()): innerFunc = recv'sfunctionfield; innerCallee = innerFunc raw text with/\s+/gstripped; 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-lineFoo.Create(1)\n .Bar()→Foo.Create().Bar(whitespace stripped on the INNER only). - recv anything else → calleeName = RAW full text of the
member_access_expression (getNodeText — UTF-16 substring, whitespace
and newlines INCLUDED). Probed consequences, all PRESERVE:
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-tokenbasevia the field — childForFieldName returns it);_repo.Save(x)→_repo.Save;builder\n .Services\n .AddSingleton<IRepo, Repo>()→ the full multi-line textbuilder\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.
- recv is
func.type === 'scoped_identifier'(4499) — never C# (no::node).- else → calleeName = raw func text: bare
Helper()→Helper; generic invocationGeneric<int>(local)→ func = generic_name →Generic<int>kept verbatim (the template strip at 4542 is c/cpp-gated);nameof(Widget)→ func = identifiernameof→ acallsref namednameof(probed — nameof is a plain invocation in this grammar); conditional accessrequest?.Method()→ func = conditional_access_expression → raw textrequest?.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).
extractInstantiation (4610) — object_creation_expression
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).
extractStaticMemberRef (4750) — csharp ∈ STATIC_MEMBER_LANGS (345)
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.
Decorators/attributes — NO-OP for C# (probed, PRESERVE)
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).
Inheritance — extractInheritance (5291), base_list branch (5577-5593)
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 FIRSTidentifierchild's text (ClientBase<Widget>→ClientBase) — but position = the generic_name node.qualified_name→ FULL dotted text (Sys.ICloneable— no last-segment reduction).- QUIRK (probed): a C# 12 class primary-ctor base
class Svc(…) : Base(repo), IThingparses base_list children as [identifier(Base), argument_list((repo)), identifier(IThing)] → emits extendsBase, extends(repo)(!), extendsIThing. The(repo)ref is garbage that never resolves — PRESERVE it. - QUIRK (probed): a RECORD's base-with-args wraps differently —
record UserDto(…) : BaseDto(Name), IThing→ base_list children are [primary_constructor_base_type, identifier] → emits extendsBaseDto(Name)(full text, argument list included) +IThing. - QUIRK (probed): an enum's underlying type
enum ReadType : byteputs predefined_type(byte) in base_list → extends refbyte(likewise: int, etc.).
extractInterface/extractStruct/extractEnum all route through the same branch. The 5652 recursion (field_declaration_list/class_heritage) never matches C#.
Type-annotation references — csharp ∈ TYPE_ANNOTATION_LANGUAGES (5753)
extractTypeAnnotations (5788) short-circuits into extractCsharpTypeRefs (5798-5801) — the generic params/return/type_annotation path below it never runs for C#. extractCsharpTypeRefs (5893):
- directType =
typefield ??returnsfield → walkCsharpTypePosition (methods:returns; properties:type; constructors: neither). - a
variable_declarationchild'stypefield (fields) → walk. parametersfield → perparameterchild (type filter — probed:bracketed_parameter_listof indexers never reaches here since indexers aren't dispatched) → itstypefield → 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).
Docstrings (tree-sitter-helpers.ts:95)
/// 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_stripas-is** — the CRLF^-after-\rsemantics (#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).
Value-reference edges — csharp ∈ VALUE_REF_LANGS (401)
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.
- Targets (captureValueRefScope:735): created nodes of kind
constant/variable, name length ≥3 AND
/[A-Z_]/, parent scope id startingfile:/class:/module:/struct:/enum:. For C# that meansconst/static readonlyfields (kind constant) inside class:/struct: parents — namespace: parents do NOT qualify (deliberate: a namespace-scoped… doesn't exist in C# anyway), and C# mints novariablenodes at all. Last same-named target wins the map; counts accumulate per name. - Reader scopes: every function/method/constant/variable node — for C#: methods (ctors included) + constant fields.
- Shadow prune (803-878): DFS the whole tree; C#-live cases of the declarator
switch:
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) huntssimple_identifierwhich 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-localvar MaxItems = …shadows the class const). - Emission (880-930): per reader scope, stack-DFS its node subtree
(push-children-in-order / pop-last → REVERSE-index traversal order — mirror
exactly, dedupe is first-wins per (scope,target)); each
identifierwhose text maps to a target, target ≠ self, name ≠ scope's own name → EDGE {source: scopeId, target: targetId, kind:'references', metadata:{valueRef:true}}. Theconstant/name/simple_identifierreader 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.
Function-as-value capture (#756) — CSHARP_SPEC (function-ref.ts:250)
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.
- varinit mechanics (function-ref.ts:471-486): C# variable_declarator has no
valuefield → 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#. - rhs param-storage skip (430-444): LHS last-identifier == RHS text → skip
(
this.status = status).button.Click += OnClick→ LHS last-nameClick≠OnClick→ candidate. - normalizeValue: bare
identifier→ candidate (NAME_STOPLIST = this/self/super/null/nil/true/false/undefined/new/NULL/nullptr/None — notevalue,args, andbaseare NOT stoplisted;base/thiscan't become candidates anyway, they parse as non-identifier tokens);member_access_expression→ normalizeSpecial (738-746): name field required; receiver must bethis— expr field typethis_expressionORthis(the vendored grammar yields the anonymous-tokenthisvia the field — probed; the text-prefix fallback exists for field-less shapes) → candidate = the BARE member name (this.HandleThing→HandleThing— NOTthis.-prefixed, unlike TS/Java; it then gates against definedHere).C.StaticHandler(type receiver) → NOTHING. Lambdas/anonymous methods → nothing (not idTypes/special). - Capture fires from visitNode:990, visitFunctionBody:5137, and
scanFnRefSubtree (property/field/variable declarations + top-level
statements, depth ≤12, capture-only). scanFnRefSubtree's halt list
(tree-sitter.ts:606-612) includes the literal type
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), whileanonymous_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). - Flush gate (flushFnRefCandidates:639): generated-file skip;
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: fromimports-kind refs — C# using-refs are dotted → QUALIFIED_IMPORT (./\separators) admits the LAST segment (using MyApp.Helpers;→Helpers), single-segmentusing Xunit;→Xunit; the alias-quirk moduleNames ride the same rules. Survivors dedupe on${fromNodeId}|${name}→function_refrefs.
Closure-collection / other passes
- csharp ∉ CC_LANGUAGES (callback-synthesizer.ts:77 — swift/kotlin only): the closure-collection synthesis pass ignores C#. Nothing to port; nothing to regress.
- Local-variable receiver inference (#1108) and typed-param receivers
(#1125/#1129/#1130) are RESOLUTION-side: name-matcher.ts:1164 has the csharp
regex rows (
= new Logger()andLogger lg;-shaped declared types, covering params via the[=;,)]tail). They consume therecv.Methodmember-access text extraction emits — the walker just has to reproduce the emission shapes above. - Chained-call resolution (#645/#608/#750): csharp is in matchDottedCallChain
(name-matcher.ts:2156) — consumes the
inner().Methodencoding + declared return types (getReturnType). Resolution-side; extraction contract pinned above. - LANGUAGE_FAMILY: csharp+razor = 'dotnet' (name-matcher.ts:149) — the razor extractor (T3, TS-side) resolves against C# nodes; unaffected by the port.
java.rs skeleton mapping (what to crib, what diverges)
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_declaration_statement zero-emission path, preproc_* passthrough.
Frameworks that consume C# extraction artifacts (stay TS-side)
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): emitsroutenodes with id`route:${filePath}:${line}:${METHOD}:${path}`(NOT hashed), kindroute, name`${METHOD} ${path}`, qualifiedName`${filePath}::route:${path}`, languagecsharp; plus onereferencesref 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.
Parity mechanics (all have bitten before — be explicit)
- Emission ORDER per file (rowid-order-sensitive downstream): file node → namespace node (when present) → walk order exactly as the dispatch table above (per declaration: inheritance refs → primary-ctor refs → [decorators: nothing] → body/member walk; methods: type-annotation refs (returns, then params) before the body's calls; fields: per-declarator node → its type refs → next declarator) → flushFnRefCandidates (refs) → flushValueRefs (edges, reverse-index DFS order). Refs carry NO filePath/language (the store denormalizes) — the wire contract is exactly extractFromSource's return.
- generateNodeId inputs: (filePath, kind, name, startRow+1) — name is the EXACT string pinned per branch above (e.g. constructors use the class name; enum members the name-field text; imports the moduleName with its generic-args quirk; the namespace node the dotted package text).
- UTF-16 everywhere: positions/columns (textutil::col16 — verified against
the wasm: emoji-preceded call at column 37 in UTF-16 units), every
getNodeText slice (raw substring — multi-line callee texts keep newlines),
the import signature trim. No
.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. - CRLF hazards: the ONLY multiline regexes on the C# path are (a) the
preParse blanking — TS-side, hoisted, never ported (§arch-1) — and (b)
docstring cleaning — already CRLF-correct via
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 — useRegex::is_matchon the full string,\A…\zsemantics). - Defer policy: per-file
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). - MAX_FILE_SIZE + generated-file skips: shared/orchestrator-side; nothing csharp-specific (isGeneratedFile gates fn-ref flush + value refs — mirror the calls, the patterns live in textutil.rs).
- Node-ID collisions: compare ID strings in fn-ref/value-ref self-checks
(
node_idsvec pattern — java.rs already does this).
Gates (per plan §5, no exceptions)
- Torture fixture
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) / bodilessrecord Empty;(node) /record UserDto(…) : BaseDto(Name), IThing(extendsBaseDto(Name)full-text quirk); C#12 class primary ctor with base args (extendsBase+(repo)+IThingquirk); bodilessstruct Fwd;(NO node); interface with bodyless method + property + default-impl arrow method; enum with: byte(extendsbytequirk), attributed member, valued members; const + static-readonly (→constant)- multi-declarator + instance fields (signatures
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 (Disposebare 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".ToUpperemitted),?.(rawrequest?.Method),p!.Force, chainedFoo.Create(1).Bar()(re-encode + inner both) +GetThing().Bar(),(myDel)(x)(conv regex →myDel),nameof(Widget)(calls refnameof);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 + trailingpartial 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 = xparam-storage skip; value-refs: const target + reader methods + avar MaxItems = …local shadow (prune) +static readonlymulti-target; preprocessor:#region/#endregion/#pragma/#nullable/#defineinterleaved 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 thedefer:signal; LINQ query + interpolated/raw/verbatim strings + switch expression + collection expressions (calls inside all reach the graph).
- multi-declarator + instance fields (signatures
- Parity sweeps (
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 vsCODEGRAPH_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. - Suite: new
__tests__/kernel-csharp-parity.test.ts(torture + CRLF + defer fixtures, strict full-object compare,CODEGRAPH_KERNEL_EXPECT=1aware); add the csharp row to__tests__/kernel-grammar-parity.test.ts(cratetree_sitter_c_sharp::LANGUAGEvs vendored wasm, id-by-id); existing pinned behavior lives in__tests__/extraction.test.ts(describe blocks at 1342C# Extraction, 2548C# imports, 8924C# records) and__tests__/resolution.test.ts— full suite green ×2 withCODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += csharp(src/extraction/kernel/index.ts:37) only after ALL of the above; changelog rides the existing kernel entry.- Post-route perf sanity: §arch-2 — measure library repos (serilog/ Newtonsoft.Json) for the raw-buffer win; ASP.NET repos take the decoded path by design.
Fixtures to build (inventory)
__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-levelusingalias 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).- CRLF variants of all three — derived in-memory in the test (the kernel-tsjs-parity pattern), never checked in as CRLF bytes.
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 kerneldefer:+ wasm-fallback byte-parity through extractFromSource.- In-memory micro-fixtures inside the parity test for: the alias-import
moduleName quirks, the
(repo)extends quirk, the enum: bytequirk,nameofcalls ref,this./base.callee prefixes, and the property-signature bare-identifier-type loss — cheap unit pins that make a future grammar bump scream early.