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] **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. 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 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
|
### 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):
|
Verify these DON'T need `getReceiverType` (methods nested in class body):
|
||||||
|
|
||||||
- [ ] TypeScript
|
- [ ] TypeScript
|
||||||
- [ ] C#
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const csharpExtractor: LanguageExtractor = {
|
|||||||
callTypes: ['invocation_expression'],
|
callTypes: ['invocation_expression'],
|
||||||
variableTypes: ['local_declaration_statement'],
|
variableTypes: ['local_declaration_statement'],
|
||||||
fieldTypes: ['field_declaration'],
|
fieldTypes: ['field_declaration'],
|
||||||
|
propertyTypes: ['property_declaration'],
|
||||||
nameField: 'name',
|
nameField: 'name',
|
||||||
bodyField: 'body',
|
bodyField: 'body',
|
||||||
paramsField: 'parameter_list',
|
paramsField: 'parameter_list',
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ export interface LanguageExtractor {
|
|||||||
variableTypes: string[];
|
variableTypes: string[];
|
||||||
/** Node types that represent class fields (extracted as 'field' kind inside class bodies) */
|
/** Node types that represent class fields (extracted as 'field' kind inside class bodies) */
|
||||||
fieldTypes?: string[];
|
fieldTypes?: string[];
|
||||||
|
/** Node types that represent class properties (extracted as 'property' kind inside class bodies) */
|
||||||
|
propertyTypes?: string[];
|
||||||
|
|
||||||
// --- Field name mappings ---
|
// --- Field name mappings ---
|
||||||
|
|
||||||
|
|||||||
@@ -285,6 +285,11 @@ export class TreeSitterExtractor {
|
|||||||
else if (this.extractor.typeAliasTypes.includes(nodeType)) {
|
else if (this.extractor.typeAliasTypes.includes(nodeType)) {
|
||||||
skipChildren = this.extractTypeAlias(node);
|
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)
|
// Check for class fields (e.g. Java field_declaration, C# field_declaration)
|
||||||
else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
|
else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
|
||||||
this.extractField(node);
|
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).
|
* 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.
|
* 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 visibility = this.extractor.getVisibility?.(node);
|
||||||
const isStatic = this.extractor.isStatic?.(node) ?? false;
|
const isStatic = this.extractor.isStatic?.(node) ?? false;
|
||||||
|
|
||||||
// Java field_declaration: "private final String name = value;"
|
// Java field_declaration: "private final String name = value;" → variable_declarator(s) are direct children
|
||||||
// Children include modifiers, type, variable_declarator(s)
|
// C# field_declaration: wraps in variable_declaration → variable_declarator(s)
|
||||||
const declarators = node.namedChildren.filter(
|
let declarators = node.namedChildren.filter(
|
||||||
c => c.type === 'variable_declarator'
|
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) {
|
if (declarators.length > 0) {
|
||||||
// Get field type from the type child
|
// Get field type from the type child
|
||||||
const typeNode = node.namedChildren.find(
|
// Java: type is a direct child of field_declaration
|
||||||
c => c.type !== 'modifiers' && c.type !== 'variable_declarator'
|
// C#: type is inside variable_declaration wrapper
|
||||||
&& c.type !== 'marker_annotation' && c.type !== 'annotation'
|
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;
|
const typeText = typeNode ? getNodeText(typeNode, this.source) : undefined;
|
||||||
|
|
||||||
for (const decl of declarators) {
|
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;
|
if (!nameNode) continue;
|
||||||
const name = getNodeText(nameNode, this.source);
|
const name = getNodeText(nameNode, this.source);
|
||||||
const signature = typeText ? `${typeText} ${name}` : name;
|
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
|
// Swift: inheritance_specifier > user_type > type_identifier
|
||||||
// Used for class inheritance, protocol conformance, and protocol inheritance
|
// Used for class inheritance, protocol conformance, and protocol inheritance
|
||||||
if (child.type === 'inheritance_specifier') {
|
if (child.type === 'inheritance_specifier') {
|
||||||
|
|||||||
Reference in New Issue
Block a user