Files
codegraph/__tests__/csharp-property-accessors.test.ts
Colby Mchenry 489b4749c7 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>
2026-10-05 14:41:59 +00:00

92 lines
3.6 KiB
TypeScript

/**
* 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([]);
});
});