fix(vbnet,csharp): index every Structure member; walk property bodies and VB initializers (#2345)

VB.NET's grammar tags each member of a `Structure` as its own `body`
field, so extractAggregate's field lookup saw only the first member.
SCrawler's UserMedia indexed none of its members, and a structure that
opened with a nested enum indexed only the enum. Aggregates now resolve
their body the way classes and enums do (resolveBody, which VB maps to
the declaration itself). Objective-C, the only other struct language
with that hook, is unchanged.

Property bodies were never walked: the propertyTypes branch set
skipChildren after extractProperty, so calls, instantiations and reads
in a VB `Get`/`Set` block, `= initializer` or `As New`, and in a C#
accessor body or `=> expr`, were lost.

- propertyBodies() names the parts of a property that run code (VB: the
  whole declaration, as its methods are walked; C#: each accessor's body
  and an arrow value), walked with the property on the stack. The
  candidates-only fn-ref scan skips the walked subtrees by node id, so a
  function value is captured once, from the property. C# `= initializer`s
  stay unwalked, as C# field initializers are.
- The C# kernel mirrors it (property_bodies, scan skip list). The parity
  suite and a new micro pass; a serilog sweep is 209/214 byte-identical
  with 0 diffs (5 deferred for parse errors).
- VB field declarators (`= expr`, `As New T`) and Custom Event
  AddHandler/RemoveHandler/RaiseEvent blocks are walked as their field or
  event.

Before/after (nodes / edges): SCrawler 9,950 -> 10,427 / 14,336 -> 15,092;
staxrip 13,440 -> 13,644 / 24,120 -> 28,002, most of it its
`Property X As New NumParam With {...}` settings; serilog edges
6,642 -> 6,654. All 23 removed edges are the same call sites re-resolved
by VB's name-guessing resolver now that struct members are candidates:
10 wrong edges dropped, 9 correct ones now guessed wrong, 4 wrong either
way. Designer -> Add/Size edge counts are unchanged.

EXTRACTION_VERSION 27 -> 28.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-05 14:41:59 +00:00
committed by GitHub
parent 6560052a6f
commit 489b4749c7
9 changed files with 459 additions and 42 deletions
+6
View File
@@ -12,6 +12,12 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixes
- In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty.
- In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused.
- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
## [1.6.2] - 2026-10-03
@@ -0,0 +1,91 @@
/**
* C# property accessor bodies belong to the property. `get { … }`,
* `set { … }`, `get => …` and an expression-bodied `=> …` property used to
* be skipped by both extractors, so the calls, instantiations and static
* reads written there were lost and a getter's callee looked unused.
*
* Runs against the native kernel (when built) and the wasm extractor, which
* must agree.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';
const FILES: Record<string, string> = {
'Helper.cs': `public static class Helper {
public static int Compute(int n) => n;
public static void Store(int n) {}
public static int Arrow() => 1;
public static int GetA() => 1;
public static void SetA(int v) {}
}
public class Widget {}
public static class Defaults { public const string Name = "x"; }
`,
'Box.cs': `public class Box {
private int _x;
public int Value {
get { return Helper.Compute(_x); }
set { Helper.Store(value); _x = value; }
}
public int Arrow => Helper.Arrow();
public int Both { get => Helper.GetA(); set => Helper.SetA(value); }
public Widget Made { get { return new Widget(); } }
public string Label { get { return Defaults.Name; } }
private void Handle(int v) { }
public System.Action<int> Handler { get { return Pick(Handle); } }
}
`,
};
describe('C# property accessor bodies', () => {
let root = '';
let cg: CodeGraph | undefined;
let kernel: string | undefined;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cs-accessors-'));
for (const [rel, content] of Object.entries(FILES)) fs.writeFileSync(path.join(root, rel), content);
kernel = process.env.CODEGRAPH_KERNEL;
});
afterEach(() => {
cg?.close();
cg = undefined;
fs.rmSync(root, { recursive: true, force: true });
if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL;
else process.env.CODEGRAPH_KERNEL = kernel;
});
it.each(['default', 'wasm'])('are walked as the property (%s)', async (backend) => {
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
else delete process.env.CODEGRAPH_KERNEL;
cg = await CodeGraph.init(root, { index: true });
const graph = cg;
const member = (qualifiedName: string): Node => {
const node = graph.getNodesInFile('Box.cs').find((n) => n.qualifiedName === qualifiedName);
expect(node, qualifiedName).toBeDefined();
return node!;
};
const targets = (node: Node, kind: string): string[] =>
graph
.getOutgoingEdgesFrom([node.id])
.filter((e) => e.kind === kind)
.map((e) => graph.getNode(e.target)!.qualifiedName)
.sort();
expect(targets(member('Box::Value'), 'calls')).toEqual(['Helper::Compute', 'Helper::Store']);
expect(targets(member('Box::Arrow'), 'calls')).toEqual(['Helper::Arrow']);
expect(targets(member('Box::Both'), 'calls')).toEqual(['Helper::GetA', 'Helper::SetA']);
expect(targets(member('Box::Made'), 'instantiates')).toEqual(['Widget']);
expect(targets(member('Box::Label'), 'references')).toContain('Defaults');
// A method passed as a value inside an accessor is the property's
// reference, captured once — not the class's as well.
expect(targets(member('Box::Handler'), 'references')).toContain('Box::Handle');
expect(targets(member('Box'), 'references')).not.toContain('Box::Handle');
expect(targets(member('Box'), 'calls')).toEqual([]);
});
});
+19 -1
View File
@@ -8,7 +8,8 @@
*
* - Torture.cs — block namespace + nested/second-namespace quirks,
* base_list shapes, records, properties (incl. the bare-identifier
* signature loss and never-walked accessor bodies), fields/constants,
* signature loss, accessor bodies walked as the property and
* never-walked initializers), fields/constants,
* events/operators/indexer/destructor (no nodes, calls → class), ctor
* initializer hole, explicit interface impl, local functions, the call
* zoo (raw member-access texts, chained re-encode, `(myDel)(x)` conv,
@@ -167,6 +168,23 @@ describe.skipIf(!kernelBuilt)('kernel C# extraction parity', () => {
source: 'public record Empty;\n',
minNodes: 2,
},
{
// Accessor bodies and `=> expr` are walked as the property (calls,
// instantiations, static reads, fn-ref candidates); an `= initializer`
// is only scanned for candidates, attributed to the class.
name: 'property bodies are walked as the property; initializers only scanned',
source: [
'public class C {',
' void H(int v) { }',
' public Action P { get { return Make(H); } set { Register(H); } }',
' public int Q => Run(H);',
' public Action R { get; } = Wrap(H);',
' public int S { get => Calc.Max; init => Store(new Widget()); }',
'}',
'',
].join('\n'),
minNodes: 7,
},
];
for (const m of MICROS) {
+209
View File
@@ -0,0 +1,209 @@
/**
* VB.NET member bodies the extractor used to skip:
*
* - A `Structure` lists each member as its own `body` field, so reading the
* field returned only the first member: SCrawler's `UserMedia` indexed its
* nested `States` enum (declared first) and nothing else, and a structure
* that opened with a field indexed no members at all.
* - A property's `Get` / `Set` blocks, its `= initializer` and its
* `As New T` were never walked, nor a `Custom Event`'s accessors.
* - A field's `= initializer` and `As New T` were never walked.
*
* The calls, instantiations and reads in those places belong to the member
* that declares them.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-bodies-'));
const files: Record<string, string> = {
'Helper.vb': `Public Class Helper
Public Sub New(ByVal n As Integer)
End Sub
Public Shared Function Compute(ByVal n As Integer) As Integer
Return n
End Function
Public Shared Sub Store(ByVal n As Integer)
End Sub
Public Shared Function Seed() As Integer
Return 1
End Function
Public Shared Function Format(ByVal o As Object) As String
Return ""
End Function
Public Shared Sub Log(ByVal s As String)
End Sub
Public Shared Sub Attach(ByVal h As EventHandler)
End Sub
End Class
`,
'Widget.vb': `Public Class Widget
End Class
`,
'UserMedia.vb': `Public Structure UserMedia
Public State As States
Private Count As Integer
Public Enum States
Unknown
Downloaded
End Enum
Public ReadOnly Property Name As String
Get
Return Helper.Format(State)
End Get
End Property
Public Sub New(ByVal s As States)
State = s
Helper.Log("created")
End Sub
Public Function IsDone() As Boolean
Return State = States.Downloaded
End Function
End Structure
`,
'Box.vb': `Public Class Box
Private _x As Integer
Private _items As New Widget
Private _helper As Helper = New Helper(1)
Private Shared ReadOnly Def As Integer = Helper.Compute(3), Other As Integer = Helper.Seed()
Public Property Value As Integer
Get
Return Helper.Compute(_x)
End Get
Set(ByVal v As Integer)
Helper.Store(v)
_x = v
End Set
End Property
Public Property Auto As Integer = Helper.Seed()
Public ReadOnly Property Lst As New Widget
Public Custom Event Changed As EventHandler
AddHandler(ByVal value As EventHandler)
Helper.Attach(value)
End AddHandler
RemoveHandler(ByVal value As EventHandler)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As EventArgs)
End RaiseEvent
End Event
End Class
`,
// The 1.6.2 receiver gate must hold in the newly walked places too:
// `Me.Panel.Controls.Add(…)` and `New System.Drawing.Size(…)` name no
// project member.
'Collections/DataColorCollection.vb': `Friend Class DataColorCollection
Friend Sub Add(ByVal Item As Object)
End Sub
End Class
`,
'Editors/UsersInfoForm.vb': `Friend Class UsersInfoForm
Private Enum EComparers
Name
Size
End Enum
End Class
`,
'MainForm.vb': `Public Class MainForm
Private ReadOnly DefaultSize As System.Drawing.Size = New System.Drawing.Size(184, 25)
Public ReadOnly Property Ready As Boolean
Get
Me.Panel.Controls.Add(Me.Button1)
Return True
End Get
End Property
End Class
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
function member(file: string, qualifiedName: string): Node {
const node = cg.getNodesInFile(file).find((n) => n.qualifiedName === qualifiedName);
expect(node, `${qualifiedName} in ${file}`).toBeDefined();
return node!;
}
/** `kind` edges leaving `node`, as target qualified names. */
function targets(node: Node, kind: string): string[] {
return cg
.getOutgoingEdgesFrom([node.id])
.filter((e) => e.kind === kind)
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}
describe('VB.NET member bodies', () => {
it('indexes every member of a Structure, not just the first', () => {
const struct = member('UserMedia.vb', 'UserMedia');
expect(struct.kind).toBe('struct');
const contained = cg
.getOutgoingEdgesFrom([struct.id])
.filter((e) => e.kind === 'contains')
.map((e) => cg.getNode(e.target)!)
.map((n) => `${n.kind} ${n.qualifiedName}`)
.sort();
expect(contained).toEqual([
'enum UserMedia::States',
'field UserMedia::Count',
'field UserMedia::State',
'method UserMedia::IsDone',
'method UserMedia::New',
'property UserMedia::Name',
]);
expect(member('UserMedia.vb', 'UserMedia::States::Downloaded').kind).toBe('enum_member');
expect(targets(member('UserMedia.vb', 'UserMedia::New'), 'calls')).toEqual(['Helper::Log']);
});
it("attributes a property's Get and Set blocks to the property", () => {
expect(targets(member('Box.vb', 'Box::Value'), 'calls')).toEqual(['Helper::Compute', 'Helper::Store']);
expect(targets(member('UserMedia.vb', 'UserMedia::Name'), 'calls')).toEqual(['Helper::Format']);
});
it("walks a property's initializer and its As New", () => {
expect(targets(member('Box.vb', 'Box::Auto'), 'calls')).toEqual(['Helper::Seed']);
expect(targets(member('Box.vb', 'Box::Lst'), 'instantiates')).toEqual(['Widget']);
});
it("walks each field's initializer and its As New, per declarator", () => {
expect(targets(member('Box.vb', 'Box::Def'), 'calls')).toEqual(['Helper::Compute']);
expect(targets(member('Box.vb', 'Box::Other'), 'calls')).toEqual(['Helper::Seed']);
expect(targets(member('Box.vb', 'Box::_items'), 'instantiates')).toEqual(['Widget']);
expect(targets(member('Box.vb', 'Box::_helper'), 'instantiates')).toEqual(['Helper']);
});
it("attributes a Custom Event's accessors to the event", () => {
expect(targets(member('Box.vb', 'Box::Changed'), 'calls')).toEqual(['Helper::Attach']);
});
it('keeps the class out of what its members do', () => {
expect(targets(member('Box.vb', 'Box'), 'calls')).toEqual([]);
expect(targets(member('Box.vb', 'Box'), 'instantiates')).toEqual([]);
});
it('still links nothing through a receiver no name resolves', () => {
const ids = cg.getNodesInFile('MainForm.vb').map((n) => n.id);
const linked = cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind !== 'contains')
.map((e) => cg.getNode(e.target)!.qualifiedName);
expect(linked).not.toContain('DataColorCollection::Add');
expect(linked).not.toContain('UsersInfoForm::EComparers::Size');
});
});
+61 -19
View File
@@ -5,7 +5,7 @@
//! path, bug-for-bug, verified by scripts/kernel-parity.mjs and the full-index
//! dump-diff gate. The authoritative quirk list is
//! docs/design/csharp-kernel-port-checklist.md — including every deliberate
//! emission hole (property/accessor bodies, constructor initializers,
//! emission hole (field/property initializers, constructor initializers,
//! delegates/events/operators/indexers, top-level locals) and garbage ref
//! (`(repo)` primary-ctor extends, `: byte` enum extends, `nameof` calls)
//! this file preserves on purpose. Positions in UTF-16 code units. Files whose
@@ -572,21 +572,36 @@ impl<'t> Walker<'t> {
self.extract_enum(node);
skip_children = true;
} else if kind == "property_declaration" && self.inside_class_like() {
// Property accessor/expression bodies are NEVER walked (calls
// inside are lost by design) — candidates-only scan.
self.extract_property(node);
self.scan_fn_ref_subtree(node, 0);
// The code a property runs — its accessor bodies and `=> expr` —
// is walked with the property on the stack (propertyBodies). The
// candidates-only scan covers the rest (an `= initializer`),
// skipping what the body walk captured.
let walked: Vec<usize> = match self.extract_property(node) {
Some((row, name)) => {
let bodies = property_bodies(node);
if !bodies.is_empty() {
self.stack.push(Scope { row, kind: "property", name });
for body in &bodies {
self.visit_function_body(*body);
}
self.stack.pop();
}
bodies.iter().map(|b| b.id()).collect()
}
None => Vec::new(),
};
self.scan_fn_ref_subtree(node, 0, &walked);
skip_children = true;
} else if kind == "field_declaration" && self.inside_class_like() {
self.extract_field(node);
self.scan_fn_ref_subtree(node, 0);
self.scan_fn_ref_subtree(node, 0, &[]);
skip_children = true;
} else if kind == "local_declaration_statement" && !self.inside_class_like() {
// Top-level statements: extractVariable's generic fallback finds no
// direct identifier/variable_declarator children (C# nests them in
// variable_declaration) → ZERO nodes, zero refs. Candidates only.
self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0);
self.scan_fn_ref_subtree(node, 0, &[]);
skip_children = true;
} else if kind == "using_directive" {
self.extract_import(node);
@@ -789,9 +804,9 @@ impl<'t> Walker<'t> {
}
/// extractProperty (1986) — property_declaration only (dispatch-gated to
/// class-like scopes). Accessor bodies and `=>` value clauses are never
/// walked; type refs DO come from the `type` field.
fn extract_property(&mut self, node: Node<'t>) {
/// class-like scopes). Type refs come from the `type` field; the caller
/// walks the bodies (property_bodies) with the returned row on the stack.
fn extract_property(&mut self, node: Node<'t>) -> Option<(u32, String)> {
let docstring = preceding_docstring(node, self.src);
let visibility = Some(self.visibility_of(node));
let is_static = Some(self.is_static(node)); // ?? false — always concrete
@@ -804,10 +819,10 @@ impl<'t> Walker<'t> {
.filter_map(|i| node.named_child(i))
.find(|c| c.kind() == "identifier")
});
let Some(name_node) = name_node else { return };
let name_node = name_node?;
let name = self.text(name_node).to_string();
if name.is_empty() {
return;
return None;
}
// Generic scan (isTsJsField=false): FIRST namedChild that isn't a
@@ -842,11 +857,10 @@ impl<'t> Walker<'t> {
&name,
node,
Extra { docstring, signature: Some(signature), visibility, is_static, ..Extra::default() },
);
if let Some(row) = row {
// decorators: none for C#; then the csharp type-ref path.
self.extract_csharp_type_refs(node, row);
}
)?;
// decorators: none for C#; then the csharp type-ref path.
self.extract_csharp_type_refs(node, row);
Some((row, name))
}
/// extractField (2046) — field_declaration; each declarator becomes a
@@ -1468,11 +1482,15 @@ impl<'t> Walker<'t> {
});
}
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
stack_guard!();
if depth > 12 {
return;
}
// Subtrees the body walker has already been through.
if walked.contains(&node.id()) {
return;
}
// functionTypes is EMPTY for C#; the literal halt list applies —
// lambda_expression IS C#'s lambda, so initializer lambdas stop the
// scan; anonymous_method_expression is NOT listed and scans through.
@@ -1487,7 +1505,7 @@ impl<'t> Walker<'t> {
self.maybe_capture_fn_refs(node);
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.scan_fn_ref_subtree(c, depth + 1);
self.scan_fn_ref_subtree(c, depth + 1, walked);
}
}
}
@@ -1624,6 +1642,30 @@ impl<'t> Walker<'t> {
}
}
/// propertyBodies (tree-sitter.ts) — the parts of a property that run code:
/// each accessor's `body` (a block or `=> expr`) and an expression-bodied
/// property's `=> …` value. An `= initializer` value is not a body.
fn property_bodies(node: Node) -> Vec<Node> {
let mut bodies = Vec::new();
if let Some(accessors) = node.child_by_field_name("accessors") {
for i in 0..accessors.named_child_count() {
let Some(accessor) = accessors.named_child(i) else { continue };
if accessor.kind() != "accessor_declaration" {
continue;
}
if let Some(body) = accessor.child_by_field_name("body") {
bodies.push(body);
}
}
}
if let Some(value) = node.child_by_field_name("value") {
if value.kind() == "arrow_expression_clause" {
bodies.push(value);
}
}
bodies
}
fn find_anonymous_class_body(node: Node) -> Option<Node> {
for i in 0..node.named_child_count() {
if let Some(child) = node.named_child(i) {
+15 -7
View File
@@ -234,7 +234,7 @@ all PRESERVE):
| `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 |
| `property_declaration` (inside class-like) | propertyTypes:1075 → extractProperty:1986 | property node, then **propertyBodies — each accessor's `body` (`get { … }`, `set => …`) and an expression-bodied `=> expr` `value:` — walked by visitFunctionBody with the property pushed** (calls, instantiates, static reads and fn-ref candidates attribute to the property; changed 2026-10-04, previously never walked), then scanFnRefSubtree (capture-only, attributed to the class) over the rest — **the `= initializer` stays unwalked** — skipping the walked bodies, + skipChildren. §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) |
@@ -305,15 +305,22 @@ for C#) or bare `name`. QUIRKS (probed, PRESERVE):
- 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.
the arrow clause is a property body (walked as the property, below).
- `{ get; } = new();` initializers: the `value:` implicit_object_creation is
NOT a body → no refs, no instantiates (candidates-only scan, attributed to
the class); the accessor_list is excluded from the type scan.
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).
kept the raw text). The returned node is pushed while propertyBodies
(tree-sitter.ts; csharp.rs `property_bodies`) are walked: every
`accessor_declaration`'s `body` field (block or arrow_expression_clause, in
accessor order), then the property's `value:` when it is an
arrow_expression_clause. The fn-ref scan that follows skips those subtrees
by node id, so a candidate is captured once — from the property when it sits
in a body, from the class when it sits in an initializer. (The
classifyMethodNode initializer-walk path at 1031-1047 is TS-only.)
### extractField (2046) — field_declaration
@@ -758,7 +765,8 @@ AspNetCore refs / Program.cs / Startup.cs / controller-source scan.
+ 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);
(`=>` calls → the property), `{ get; } = new();` (initializer LOST),
accessor bodies with calls (→ the property);
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
+1 -1
View File
@@ -21,4 +21,4 @@
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
* in the product is load-bearing").
*/
export const EXTRACTION_VERSION = 27;
export const EXTRACTION_VERSION = 28;
+7 -2
View File
@@ -116,8 +116,13 @@ export const vbnetExtractor: LanguageExtractor = {
// findable declaration (WinForms/WPF code is built around them).
if (node.type === 'event_declaration' || node.type === 'custom_event_declaration') {
const nameNode = node.childForFieldName('name');
if (nameNode) {
ctx.createNode('field', getNodeText(nameNode, ctx.source), node);
const event = nameNode ? ctx.createNode('field', getNodeText(nameNode, ctx.source), node) : null;
// A Custom Event's AddHandler / RemoveHandler / RaiseEvent blocks run
// code, which is the event's.
if (event && node.type === 'custom_event_declaration') {
ctx.pushScope(event.id);
ctx.visitFunctionBody(node, event.id);
ctx.popScope();
}
return true;
}
+50 -12
View File
@@ -733,8 +733,10 @@ export class TreeSitterExtractor {
* nested function definitions: their bodies are walked — and their
* candidates attributed — by extractFunction's own body walk.
*/
private scanFnRefSubtree(node: SyntaxNode, depth: number): void {
private scanFnRefSubtree(node: SyntaxNode, depth: number, walked?: ReadonlySet<number>): void {
if (!this.fnRefSpec || depth > 12) return;
// Subtrees the body walker has already been through.
if (walked?.has(node.id)) return;
const nodeType = node.type;
if (depth > 0 && (
this.extractor?.functionTypes.includes(nodeType) ||
@@ -749,7 +751,7 @@ export class TreeSitterExtractor {
this.maybeCaptureFnRefs(node, nodeType);
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) this.scanFnRefSubtree(child, depth + 1);
if (child) this.scanFnRefSubtree(child, depth + 1, walked);
}
}
@@ -1279,11 +1281,19 @@ export class TreeSitterExtractor {
}
// Check for class properties (e.g. C# property_declaration)
else if (this.extractor.propertyTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
this.extractProperty(node);
// Property initializers aren't walked — scan for function-as-value
// candidates (#756): Scala `val table = Seq(targetCb)` in an object,
// Kotlin `val cb = ::handler` class properties.
this.scanFnRefSubtree(node, 0);
const propNode = this.extractProperty(node);
// The code a property runs is the property's: its calls,
// instantiations and reads attribute to it, as a method's do.
const bodies = propNode ? this.propertyBodies(node) : [];
if (propNode && bodies.length > 0) {
this.nodeStack.push(propNode.id);
for (const body of bodies) this.visitFunctionBody(body, propNode.id);
this.nodeStack.pop();
}
// Whatever the body walk didn't cover (a C# `= initializer`, any other
// language's whole declaration) is scanned for function-as-value
// candidates (#756); the bodies captured their own.
this.scanFnRefSubtree(node, 0, new Set(bodies.map((b) => b.id)));
skipChildren = true;
}
// Check for class fields (e.g. Java field_declaration, C# field_declaration)
@@ -2113,7 +2123,12 @@ export class TreeSitterExtractor {
// (#1093) because the two defaults differ — a bodiless CLASS is kept
// unless a language opts into skipping, a bodiless STRUCT is skipped
// unless a language opts into keeping.
const body = getChildByField(node, this.extractor.bodyField);
//
// resolveBody first, as for classes and enums: a VB.NET Structure tags
// every member as its own `body` field, so the field alone yields only
// the first member.
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
if (!body && node.type !== 'record_declaration' && !this.extractor.allowBodilessStruct)
return;
@@ -2304,6 +2319,28 @@ export class TreeSitterExtractor {
return propNode;
}
/**
* The parts of a property declaration that run code, for the body walker.
* VB.NET writes its `Get` / `Set` blocks, `= initializer` and `As New T`
* as children of the declaration itself, so the declaration is walked
* whole, the way its methods are (resolveBody). C# runs code in each
* accessor's body (`get { … }`, `set => …`) and in an expression-bodied
* property's `=> …`; its `= initializer`, like a field's, stays unwalked.
* Mirrored in the kernel (csharp.rs property_bodies).
*/
private propertyBodies(node: SyntaxNode): SyntaxNode[] {
if (this.language === 'vbnet') return [node];
if (this.language !== 'csharp') return [];
const bodies: SyntaxNode[] = [];
for (const accessor of getChildByField(node, 'accessors')?.namedChildren ?? []) {
const body = accessor.type === 'accessor_declaration' ? getChildByField(accessor, 'body') : null;
if (body) bodies.push(body);
}
const value = getChildByField(node, 'value');
if (value?.type === 'arrow_expression_clause') bodies.push(value);
return bodies;
}
/**
* Extract a class field declaration (e.g. Java field_declaration, C# field_declaration).
* Extracts each declarator as a 'field' kind node inside the owning class.
@@ -2408,10 +2445,11 @@ export class TreeSitterExtractor {
// candidates, so a lambda / method reference / anonymous class in
// `private final Runnable r = () -> target();` contributed NO call
// edge at all and `target` looked callerless. Keyed on the `value`
// FIELD, which only Java's `variable_declarator` carries — C#,
// VB.NET and PHP spell their initializer differently and are
// deliberately untouched here.
const valueNode = getChildByField(decl, 'value');
// FIELD, which only Java's `variable_declarator` carries. VB.NET
// writes `= expr` (the declarator's `initializer`) or `As New T(…)`
// (inside its as_clause), so its whole declarator is walked. C#
// and PHP spell their initializer differently and are untouched.
const valueNode = this.language === 'vbnet' ? decl : getChildByField(decl, 'value');
if (valueNode) {
this.nodeStack.push(fieldNode.id);
this.visitFunctionBody(valueNode, fieldNode.id);