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:
@@ -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#
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user