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
@@ -200,8 +200,12 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise<v
|
||||
// tree-sitter-wasms build is too old). Lua: tree-sitter-wasms ships an
|
||||
// ABI-13 build that corrupts the shared WASM heap under web-tree-sitter
|
||||
// 0.25 (drops nested calls/imports on every file after the first); we
|
||||
// vendor the upstream ABI-15 wasm instead.
|
||||
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau')
|
||||
// vendor the upstream ABI-15 wasm instead. C#: the tree-sitter-wasms
|
||||
// build (ABI 13) has no primary-constructor support and parses
|
||||
// `class Foo(...)` as an ERROR that swallows the whole class (#237); we
|
||||
// vendor the upstream ABI-15 tree-sitter-c-sharp 0.23.5 wasm, which parses
|
||||
// primary constructors natively.
|
||||
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau' || lang === 'csharp')
|
||||
? path.join(__dirname, 'wasm', wasmFile)
|
||||
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
|
||||
const language = await WasmLanguage.load(wasmPath);
|
||||
|
||||
@@ -2,7 +2,38 @@ import type { Node as SyntaxNode } from 'web-tree-sitter';
|
||||
import { getNodeText } from '../tree-sitter-helpers';
|
||||
import type { LanguageExtractor } from '../tree-sitter-types';
|
||||
|
||||
/**
|
||||
* Blank C# conditional-compilation directive lines (`#if` / `#elif` / `#else` /
|
||||
* `#endif`) before parsing. The vendored tree-sitter-c-sharp grammar mis-parses
|
||||
* a `#if` that appears *inside an enum member list* — the canonical
|
||||
* multi-targeting shape:
|
||||
*
|
||||
* enum ReadType {
|
||||
* #if HAVE_DATE_TIME_OFFSET
|
||||
* ReadAsDateTimeOffset,
|
||||
* #endif
|
||||
* ReadAsDouble,
|
||||
* }
|
||||
*
|
||||
* It emits an ERROR that, for a nested enum, detaches the *enclosing class's*
|
||||
* member list, so most of the class's methods drop out of the index. Removing
|
||||
* the directive lines (keeping the guarded code) sidesteps it. Both branches of
|
||||
* an `#if/#else` are kept — the same behaviour the previous grammar produced,
|
||||
* and the right default for a code graph (index every symbol regardless of
|
||||
* build flags). Replacement preserves byte offsets (directive text → spaces,
|
||||
* newlines kept) so every symbol's line/column stays exact. (#237)
|
||||
*/
|
||||
export function blankCsharpPreprocessorDirectives(source: string): string {
|
||||
if (source.indexOf('#') === -1) return source;
|
||||
// Conditional-compilation directives only. `#region`/`#pragma`/`#nullable`
|
||||
// parse fine and are left alone. A directive must be the first non-space token
|
||||
// on its line (C# requirement), so anchor to line start.
|
||||
const re = /^([ \t]*)#[ \t]*(if|elif|else|endif)\b[^\n]*/gm;
|
||||
return source.replace(re, (m, indent) => indent + ' '.repeat(m.length - indent.length));
|
||||
}
|
||||
|
||||
export const csharpExtractor: LanguageExtractor = {
|
||||
preParse: blankCsharpPreprocessorDirectives,
|
||||
functionTypes: [],
|
||||
// Records are first-class type declarations in modern C# (DTOs, value objects,
|
||||
// MediatR/CQRS messages). `record` / `record class` parse as record_declaration
|
||||
|
||||
@@ -78,6 +78,16 @@ export interface ExtractorContext {
|
||||
* language-specific details like signatures, visibility, and imports.
|
||||
*/
|
||||
export interface LanguageExtractor {
|
||||
/**
|
||||
* Optional source transform applied immediately before the grammar parses the
|
||||
* file. Used to work around grammar gaps that would otherwise corrupt the
|
||||
* parse tree (e.g. C# blanks conditional-compilation directive lines the
|
||||
* grammar mis-parses inside enum bodies). MUST preserve byte offsets (replace
|
||||
* removed text with spaces, keep newlines) so node positions and getNodeText
|
||||
* stay correct; the returned string is used for both parsing and extraction.
|
||||
*/
|
||||
preParse?: (source: string) => string;
|
||||
|
||||
// --- Node type mappings ---
|
||||
|
||||
/** Node types that represent functions */
|
||||
|
||||
@@ -272,6 +272,14 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
|
||||
try {
|
||||
// Optional pre-parse source transform (offset-preserving) to work around
|
||||
// grammar gaps — e.g. C# blanks conditional-compilation directive lines
|
||||
// the grammar mis-parses inside enum bodies (#237). We reassign
|
||||
// this.source so downstream getNodeText reads the same bytes the parser
|
||||
// saw (identical outside the blanked directive lines).
|
||||
if (this.extractor?.preParse) {
|
||||
this.source = this.extractor.preParse(this.source);
|
||||
}
|
||||
this.tree = parser.parse(this.source) ?? null;
|
||||
if (!this.tree) {
|
||||
throw new Error('Parser returned null tree');
|
||||
@@ -853,6 +861,9 @@ export class TreeSitterExtractor {
|
||||
// Extract extends/implements
|
||||
this.extractInheritance(node, classNode.id);
|
||||
|
||||
// C# primary-constructor parameter dependencies (`class Svc(IRepo r, …)`).
|
||||
this.extractCsharpPrimaryCtorParamRefs(node, classNode.id);
|
||||
|
||||
// Extract decorators applied to the class (`@Foo class X {}`).
|
||||
this.extractDecoratorsFor(node, classNode.id);
|
||||
|
||||
@@ -1027,6 +1038,10 @@ export class TreeSitterExtractor {
|
||||
// Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable)
|
||||
this.extractInheritance(node, structNode.id);
|
||||
|
||||
// C# primary-constructor parameter dependencies (`struct P(int x)`, and
|
||||
// `record struct M(decimal Amount)` which the grammar nests here).
|
||||
this.extractCsharpPrimaryCtorParamRefs(node, structNode.id);
|
||||
|
||||
// Push to stack for field extraction
|
||||
this.nodeStack.push(structNode.id);
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
@@ -3486,8 +3501,11 @@ export class TreeSitterExtractor {
|
||||
* `tuple_type`, …) — none of which are `type_identifier`. Closes #381.
|
||||
*/
|
||||
private extractCsharpTypeRefs(node: SyntaxNode, nodeId: string): void {
|
||||
// Return type / property type — the field is named `type`.
|
||||
const directType = getChildByField(node, 'type');
|
||||
// A property's type is under the `type` field; a method/constructor's RETURN
|
||||
// type is under `returns` (tree-sitter-c-sharp 0.23.x — older builds used
|
||||
// `type` for both). A node carries only one of the two, so checking both
|
||||
// covers return types and property types without conflating them.
|
||||
const directType = getChildByField(node, 'type') ?? getChildByField(node, 'returns');
|
||||
if (directType) this.walkCsharpTypePosition(directType, nodeId);
|
||||
|
||||
// Field declarations wrap declarators in a `variable_declaration`
|
||||
@@ -3516,6 +3534,29 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the dependencies declared by a C# PRIMARY CONSTRUCTOR
|
||||
* (`class Svc(IRepo repo, [FromKeyedServices("k")] ICache cache) { … }`,
|
||||
* C# 12+). The parameter list hangs off the class/struct/record declaration
|
||||
* as an unnamed-field `parameter_list` child (not the `parameters` field a
|
||||
* method uses), so it's found by node type. Each parameter's declared type
|
||||
* becomes a `references` edge from the owning type — these are exactly the
|
||||
* services a DI-registered type depends on, so impact/blast-radius and
|
||||
* "who depends on this contract" now see them. No-op when there's no primary
|
||||
* constructor. (#237)
|
||||
*/
|
||||
private extractCsharpPrimaryCtorParamRefs(node: SyntaxNode, ownerId: string): void {
|
||||
if (this.language !== 'csharp') return;
|
||||
const paramList = node.namedChildren.find((c: SyntaxNode) => c.type === 'parameter_list');
|
||||
if (!paramList) return;
|
||||
for (let i = 0; i < paramList.namedChildCount; i++) {
|
||||
const param = paramList.namedChild(i);
|
||||
if (!param || param.type !== 'parameter') continue;
|
||||
const paramType = getChildByField(param, 'type');
|
||||
if (paramType) this.walkCsharpTypePosition(paramType, ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a C# subtree that is KNOWN to be in a type position
|
||||
* (return type, parameter type, property type, field type, generic
|
||||
|
||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user