feat: Add C# property/field extraction and inheritance support

Addresses C#'s property_declaration nodes (public string Name { get; set; }) by adding propertyTypes support and extractProperty method. Improves field extraction to handle C#'s nested variable_declaration > variable_declarator structure. Adds base_list handling in extractInheritance for C#'s `: Parent, IInterface` syntax where base class and interfaces are combined in a single colon-separated list.
This commit is contained in:
Colby McHenry
2026-04-06 23:53:30 -05:00
parent 4a8d2f0396
commit b712e4de63
4 changed files with 84 additions and 8 deletions
+1 -1
View File
@@ -528,6 +528,7 @@ if (receiverType) {
- [x] **Rust**`getReceiverType` walks up to parent `impl_item` to extract type name. Also adds `contains` edges from struct to impl methods. Verified against Deno
- [x] **C** — NOT needed. No methods in C. Strong function/struct/enum extraction with excellent call edge density. Verified against Redis
- [x] **C++** — NOT needed for header-only libs. `isMisparsedFunction` hook filters macro-caused misparse artifacts (e.g. `NLOHMANN_JSON_NAMESPACE_BEGIN`). `visitFunctionBody` now extracts structural nodes (classes/structs/enums) inside macro-confused "function" bodies. Content-based `.h` detection (`looksLikeCpp` in `grammars.ts`) promotes C++ headers to `cpp` language so classes in `.h` files are extracted. Verified against nlohmann/json and gRPC. Note: out-of-class `Type::method()` definitions would need `getReceiverType` but are uncommon in header-only codebases.
- [x] **C#** — NOT needed. Methods nested in class body. Added `base_list` handling in `extractInheritance` for C#'s `: Parent, IInterface` syntax. Added `propertyTypes` support for C# `property_declaration` nodes. Fixed `extractField` to handle C#'s nested `variable_declaration > variable_declarator` structure. Verified against Jellyfin
### Needs Verification
@@ -538,4 +539,3 @@ Check these — may need `getReceiverType` if methods are top-level in the AST:
Verify these DON'T need `getReceiverType` (methods nested in class body):
- [ ] TypeScript
- [ ] C#
+1
View File
@@ -15,6 +15,7 @@ export const csharpExtractor: LanguageExtractor = {
callTypes: ['invocation_expression'],
variableTypes: ['local_declaration_statement'],
fieldTypes: ['field_declaration'],
propertyTypes: ['property_declaration'],
nameField: 'name',
bodyField: 'body',
paramsField: 'parameter_list',
+2
View File
@@ -100,6 +100,8 @@ export interface LanguageExtractor {
variableTypes: string[];
/** Node types that represent class fields (extracted as 'field' kind inside class bodies) */
fieldTypes?: string[];
/** Node types that represent class properties (extracted as 'property' kind inside class bodies) */
propertyTypes?: string[];
// --- Field name mappings ---
+80 -7
View File
@@ -285,6 +285,11 @@ export class TreeSitterExtractor {
else if (this.extractor.typeAliasTypes.includes(nodeType)) {
skipChildren = this.extractTypeAlias(node);
}
// Check for class properties (e.g. C# property_declaration)
else if (this.extractor.propertyTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
this.extractProperty(node);
skipChildren = true;
}
// Check for class fields (e.g. Java field_declaration, C# field_declaration)
else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
this.extractField(node);
@@ -743,6 +748,41 @@ export class TreeSitterExtractor {
}
}
/**
* Extract a class property declaration (e.g. C# `public string Name { get; set; }`).
* Extracts as 'property' kind node inside the owning class.
*/
private extractProperty(node: SyntaxNode): void {
if (!this.extractor) return;
const docstring = getPrecedingDocstring(node, this.source);
const visibility = this.extractor.getVisibility?.(node);
const isStatic = this.extractor.isStatic?.(node) ?? false;
// Property name is a direct identifier child
const nameNode = getChildByField(node, 'name')
|| node.namedChildren.find(c => c.type === 'identifier');
if (!nameNode) return;
const name = getNodeText(nameNode, this.source);
// Get property type from the type child (first named child that isn't modifier or identifier)
const typeNode = node.namedChildren.find(
c => c.type !== 'modifier' && c.type !== 'modifiers'
&& c.type !== 'identifier' && c.type !== 'accessor_list'
&& c.type !== 'accessors' && c.type !== 'equals_value_clause'
);
const typeText = typeNode ? getNodeText(typeNode, this.source) : undefined;
const signature = typeText ? `${typeText} ${name}` : name;
this.createNode('property', name, node, {
docstring,
signature,
visibility,
isStatic,
});
}
/**
* 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.
@@ -754,22 +794,34 @@ export class TreeSitterExtractor {
const visibility = this.extractor.getVisibility?.(node);
const isStatic = this.extractor.isStatic?.(node) ?? false;
// Java field_declaration: "private final String name = value;"
// Children include modifiers, type, variable_declarator(s)
const declarators = node.namedChildren.filter(
// Java field_declaration: "private final String name = value;" → variable_declarator(s) are direct children
// C# field_declaration: wraps in variable_declaration → variable_declarator(s)
let declarators = node.namedChildren.filter(
c => c.type === 'variable_declarator'
);
// C#: look inside variable_declaration wrapper
if (declarators.length === 0) {
const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration');
if (varDecl) {
declarators = varDecl.namedChildren.filter(c => c.type === 'variable_declarator');
}
}
if (declarators.length > 0) {
// Get field type from the type child
const typeNode = node.namedChildren.find(
c => c.type !== 'modifiers' && c.type !== 'variable_declarator'
&& c.type !== 'marker_annotation' && c.type !== 'annotation'
// Java: type is a direct child of field_declaration
// C#: type is inside variable_declaration wrapper
const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration');
const typeSearchNode = varDecl ?? node;
const typeNode = typeSearchNode.namedChildren.find(
c => c.type !== 'modifiers' && c.type !== 'modifier' && c.type !== 'variable_declarator'
&& c.type !== 'variable_declaration' && c.type !== 'marker_annotation' && c.type !== 'annotation'
);
const typeText = typeNode ? getNodeText(typeNode, this.source) : undefined;
for (const decl of declarators) {
const nameNode = getChildByField(decl, 'name');
const nameNode = getChildByField(decl, 'name')
|| decl.namedChildren.find(c => c.type === 'identifier');
if (!nameNode) continue;
const name = getNodeText(nameNode, this.source);
const signature = typeText ? `${typeText} ${name}` : name;
@@ -1489,6 +1541,27 @@ export class TreeSitterExtractor {
}
}
// C#: `class Movie : BaseItem, IPlugin` → base_list with identifier children
// base_list combines both base class and interfaces in a single colon-separated list.
// We emit all as 'extends' since the syntax doesn't distinguish them.
if (child.type === 'base_list') {
for (const baseType of child.namedChildren) {
if (baseType) {
// For generic base types like `ClientBase<T>`, extract just the type name
const name = baseType.type === 'generic_name'
? getNodeText(baseType.namedChildren.find((c: SyntaxNode) => c.type === 'identifier') ?? baseType, this.source)
: getNodeText(baseType, this.source);
this.unresolvedReferences.push({
fromNodeId: classId,
referenceName: name,
referenceKind: 'extends',
line: baseType.startPosition.row + 1,
column: baseType.startPosition.column,
});
}
}
}
// Swift: inheritance_specifier > user_type > type_identifier
// Used for class inheritance, protocol conformance, and protocol inheritance
if (child.type === 'inheritance_specifier') {