Vendor tree-sitter-c-sharp 0.23.5 (ABI 15) for C#, replacing the bundled ABI-13 build that dropped primary-constructor classes. Adds native primary-ctor parsing, primary-ctor parameter dependency edges, return-type extraction via the renamed `returns` field, and a preParse that blanks `#if` directive lines the new grammar mis-parses inside enum bodies. Validated on MediatR / eShopOnWeb / Newtonsoft.Json + full suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2f50473aaa
commit
80db274e5f
@@ -1013,6 +1013,86 @@ public class OrderService
|
||||
expect(classNode?.name).toBe('OrderService');
|
||||
expect(classNode?.visibility).toBe('public');
|
||||
});
|
||||
|
||||
it('indexes primary-constructor classes, including keyed-DI attribute params (#237)', () => {
|
||||
// C# 12 primary constructors (`class Foo(IDep dep) { … }`) are parsed
|
||||
// natively by the vendored tree-sitter-c-sharp 0.23.x grammar. The worst
|
||||
// shape under the previous (older) grammar — an attribute-with-args on a
|
||||
// ctor param (`[FromKeyedServices("primary")] …`, the ASP.NET keyed-DI
|
||||
// pattern) — used to parse as an ERROR that swallowed the whole class, so
|
||||
// the class and all its methods vanished. They now index in every case.
|
||||
const code = `
|
||||
public class DataService(IMemoryCache cache)
|
||||
{
|
||||
public void Warm() { }
|
||||
}
|
||||
|
||||
public class InstanceService(InstanceManager m, ProfileManager p)
|
||||
{
|
||||
public void DeployAndLaunchAsync() { }
|
||||
public void Deploy() { }
|
||||
}
|
||||
|
||||
public partial class UpdateService(int x) : ILifetimeService
|
||||
{
|
||||
public void Run() { }
|
||||
}
|
||||
|
||||
public class K1KeyedDi([FromKeyedServices("primary")] IMemoryCache cache)
|
||||
{
|
||||
public void Warm() { }
|
||||
}
|
||||
|
||||
public record CatalogBrand(int Id, string Name);
|
||||
`;
|
||||
const result = extractFromSource('Services.cs', code);
|
||||
const classNames = result.nodes.filter((n) => n.kind === 'class').map((n) => n.name);
|
||||
expect(classNames).toContain('DataService');
|
||||
expect(classNames).toContain('InstanceService');
|
||||
expect(classNames).toContain('UpdateService'); // partial + base list
|
||||
expect(classNames).toContain('K1KeyedDi'); // attribute-arg ctor param — used to vanish entirely
|
||||
expect(classNames).toContain('CatalogBrand'); // record
|
||||
|
||||
const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
|
||||
expect(methods).toContain('DeployAndLaunchAsync');
|
||||
expect(methods).toContain('Deploy');
|
||||
expect(methods).toContain('Run');
|
||||
});
|
||||
|
||||
it('keeps a class indexable when a nested enum has #if-guarded members (#237)', () => {
|
||||
// A `#if` directive inside an enum member list (the multi-targeting pattern
|
||||
// in libraries like Newtonsoft.Json) makes the grammar emit an ERROR that,
|
||||
// for a nested enum, detaches the enclosing class's member list — dropping
|
||||
// most of the class's methods. A pre-parse pass blanks the directive lines
|
||||
// (keeping both branches), so the class and all its methods still index.
|
||||
const code = `
|
||||
public class Reader
|
||||
{
|
||||
private enum ReadType
|
||||
{
|
||||
#if HAVE_DATE_TIME_OFFSET
|
||||
ReadAsDateTimeOffset,
|
||||
#endif
|
||||
ReadAsDouble,
|
||||
ReadAsString,
|
||||
}
|
||||
|
||||
public void Open() { }
|
||||
public void Close() { }
|
||||
public int ReadInt() { return 0; }
|
||||
}
|
||||
`;
|
||||
const result = extractFromSource('Reader.cs', code);
|
||||
const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
|
||||
// All three methods after the #if-bearing enum must survive.
|
||||
expect(methods).toContain('Open');
|
||||
expect(methods).toContain('Close');
|
||||
expect(methods).toContain('ReadInt');
|
||||
// Both enum branches are kept.
|
||||
const enumMembers = result.nodes.filter((n) => n.kind === 'enum_member').map((n) => n.name);
|
||||
expect(enumMembers).toContain('ReadAsDateTimeOffset');
|
||||
expect(enumMembers).toContain('ReadAsDouble');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PHP Extraction', () => {
|
||||
|
||||
@@ -1132,6 +1132,41 @@ public class DataExporter
|
||||
expect(userIncoming.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('C# primary-constructor parameters record their type dependencies (#237)', async () => {
|
||||
// C# 12 primary constructors declare a type's injected dependencies inline
|
||||
// (`class Svc(IRepo repo, [FromKeyedServices("k")] ICache cache)`). Each
|
||||
// ctor parameter's type is recorded as a `references` edge from the class,
|
||||
// so a DI-registered contract reached only through a primary ctor is no
|
||||
// longer reported as having no dependents.
|
||||
fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'src', 'Contracts.cs'),
|
||||
`namespace App;
|
||||
public interface IRepo { }
|
||||
public class ICache { }
|
||||
`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'src', 'OrderService.cs'),
|
||||
`namespace App;
|
||||
public sealed class OrderService(IRepo repo, [FromKeyedServices("primary")] ICache cache)
|
||||
{
|
||||
public void Run() { }
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
|
||||
const svc = cg.getNodesByKind('class').find((n) => n.name === 'OrderService');
|
||||
expect(svc).toBeDefined();
|
||||
// The class itself must index (it used to vanish under the old grammar).
|
||||
const out = cg.getOutgoingEdges(svc!.id).filter((e) => e.kind === 'references');
|
||||
const depNames = out.map((e) => cg.getNode(e.target)?.name);
|
||||
expect(depNames).toContain('IRepo');
|
||||
expect(depNames).toContain('ICache'); // the keyed-DI ([FromKeyedServices]) dependency
|
||||
});
|
||||
|
||||
it('Go: leaves stdlib calls (fmt.Println, etc.) external', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'go.mod'),
|
||||
|
||||
Reference in New Issue
Block a user