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>
843 lines
58 KiB
Markdown
843 lines
58 KiB
Markdown
# 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.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`):
|
||
- wasm ABI **15** == parser.c `LANGUAGE_VERSION 15`;
|
||
- wasm `stateCount` **8053** == parser.c `STATE_COUNT 8053`;
|
||
- wasm `nodeTypeCount` **533** == `SYMBOL_COUNT 530 + ALIAS_COUNT 3`;
|
||
- wasm `fieldCount` **26** 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.
|
||
- **Cargo plan:** add `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.
|
||
|
||
## Architecture decisions
|
||
|
||
1. **preParse stays TS-side — already hoisted, port NOTHING.**
|
||
`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.
|
||
2. **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 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.
|
||
3. **The framework extractor itself needs NO port** — regex over raw source
|
||
(§Frameworks below), runs in extractFromSource:6736-6758 after either arm.
|
||
4. **One walker module** (suggest `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.
|
||
5. **`.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.
|
||
6. **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_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.
|
||
|
||
## 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
|
||
**`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.
|
||
- **getVisibility (csharp.ts:102)** — scan ALL children (`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).
|
||
- **isStatic (csharp.ts:115)** — any `modifier` child with text `static`.
|
||
- **isConst (csharp.ts:127)** — `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).
|
||
- **isAsync (csharp.ts:140)** — any `modifier` child with text `async`.
|
||
**WORKS for C#** (unlike rust's dead-code isAsync) — probed: `async` is a
|
||
direct `modifier` child 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 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.
|
||
- 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:
|
||
class `Widget` in `namespace My.App` → QN **`My.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 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.
|
||
- A file-scoped `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.
|
||
|
||
## 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 = 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).
|
||
- resolveBody endLine extension (1329) is a no-op (no hook).
|
||
- contains edge from nodeStack top for every created node (1363);
|
||
`captureValueRefScope` after 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_list` namedChildren 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 `: 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 `[`).
|
||
|
||
### 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_type `string`, generic_name `List<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: 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).
|
||
|
||
### extractField (2046) — field_declaration
|
||
|
||
- kind (2058-2062): `(java|csharp) && isConst(node)` → **`constant`** (`const`
|
||
fields, `static readonly` fields), else `field`. Evaluated once per
|
||
DECLARATION — all declarators share it.
|
||
- Declarators: direct `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…]]).
|
||
- typeText (2110-2116): first namedChild of the variable_declaration that
|
||
isn't modifiers/modifier/variable_declarator/variable_declaration/
|
||
marker_annotation/annotation → the `type` node's raw text.
|
||
- Per declarator: name = `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.
|
||
- 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_declaration` is 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:
|
||
|
||
1. **`func.type === 'member_access_expression'` → the csharp branch
|
||
(4502-4517)**: recv = `expression` field, methodName = `name` field text.
|
||
- recv is `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).
|
||
- 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-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.
|
||
2. `func.type === 'scoped_identifier'` (4499) — never C# (no `::` node).
|
||
3. **else → calleeName = raw func text**: bare `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).
|
||
|
||
### 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 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).
|
||
- **QUIRK (probed): a C# 12 class primary-ctor base `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.
|
||
- **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 extends
|
||
**`BaseDto(Name)`** (full text, argument list included) + `IThing`.**
|
||
- **QUIRK (probed): an enum's underlying type `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#.
|
||
|
||
### 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):
|
||
|
||
1. directType = `type` field ?? `returns` field → walkCsharpTypePosition
|
||
(methods: `returns`; properties: `type`; constructors: neither).
|
||
2. a `variable_declaration` child's `type` field (fields) → walk.
|
||
3. `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).
|
||
|
||
### 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_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).
|
||
|
||
### 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 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.
|
||
- 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) 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).
|
||
- 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 `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.
|
||
|
||
### 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
|
||
`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#.
|
||
- rhs param-storage skip (430-444): LHS last-identifier == RHS text → skip
|
||
(`this.status = status`). `button.Click += OnClick` → LHS last-name `Click`
|
||
≠ `OnClick` → candidate.
|
||
- normalizeValue: bare `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).
|
||
- 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), 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).
|
||
- 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: 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.
|
||
|
||
### 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()` 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.
|
||
- Chained-call resolution (#645/#608/#750): csharp is in matchDottedCallChain
|
||
(name-matcher.ts:2156) — consumes the `inner().Method` encoding + 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):** 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.
|
||
|
||
## 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 —
|
||
use `Regex::is_match` on the full string, `\A…\z` semantics).
|
||
- **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_ids` vec 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) / 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`)
|
||
+ 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 (`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).
|
||
- **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 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.
|
||
- **Suite**: new `__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.
|
||
- 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)
|
||
|
||
1. `__tests__/fixtures/kernel-parity/Torture.cs` — the full pin list above
|
||
(one file is fine; keep the two-namespace case in it).
|
||
2. `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).
|
||
3. `TortureTopLevel.cs` — top-level statements program (global statements,
|
||
local function, trailing type decl).
|
||
4. CRLF variants of all three — derived in-memory in the test (the
|
||
kernel-tsjs-parity pattern), never checked in as CRLF bytes.
|
||
5. `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.
|
||
6. In-memory micro-fixtures inside the parity test for: the alias-import
|
||
moduleName quirks, the `(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.
|