mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +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:
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user