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
+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) {