mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-19 23:20:31 +08:00
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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user