Skip to content

Commit b712e4d

Browse files
committed
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.
1 parent 4a8d2f0 commit b712e4d

4 files changed

Lines changed: 84 additions & 8 deletions

File tree

docs/SEARCH_QUALITY_LOOP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ if (receiverType) {
528528
- [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
529529
- [x] **C** — NOT needed. No methods in C. Strong function/struct/enum extraction with excellent call edge density. Verified against Redis
530530
- [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.
531+
- [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
531532

532533
### Needs Verification
533534

@@ -538,4 +539,3 @@ Check these — may need `getReceiverType` if methods are top-level in the AST:
538539
Verify these DON'T need `getReceiverType` (methods nested in class body):
539540

540541
- [ ] TypeScript
541-
- [ ] C#

src/extraction/languages/csharp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const csharpExtractor: LanguageExtractor = {
1515
callTypes: ['invocation_expression'],
1616
variableTypes: ['local_declaration_statement'],
1717
fieldTypes: ['field_declaration'],
18+
propertyTypes: ['property_declaration'],
1819
nameField: 'name',
1920
bodyField: 'body',
2021
paramsField: 'parameter_list',

src/extraction/tree-sitter-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ export interface LanguageExtractor {
100100
variableTypes: string[];
101101
/** Node types that represent class fields (extracted as 'field' kind inside class bodies) */
102102
fieldTypes?: string[];
103+
/** Node types that represent class properties (extracted as 'property' kind inside class bodies) */
104+
propertyTypes?: string[];
103105

104106
// --- Field name mappings ---
105107

src/extraction/tree-sitter.ts

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ export class TreeSitterExtractor {
285285
else if (this.extractor.typeAliasTypes.includes(nodeType)) {
286286
skipChildren = this.extractTypeAlias(node);
287287
}
288+
// Check for class properties (e.g. C# property_declaration)
289+
else if (this.extractor.propertyTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
290+
this.extractProperty(node);
291+
skipChildren = true;
292+
}
288293
// Check for class fields (e.g. Java field_declaration, C# field_declaration)
289294
else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
290295
this.extractField(node);
@@ -743,6 +748,41 @@ export class TreeSitterExtractor {
743748
}
744749
}
745750

751+
/**
752+
* Extract a class property declaration (e.g. C# `public string Name { get; set; }`).
753+
* Extracts as 'property' kind node inside the owning class.
754+
*/
755+
private extractProperty(node: SyntaxNode): void {
756+
if (!this.extractor) return;
757+
758+
const docstring = getPrecedingDocstring(node, this.source);
759+
const visibility = this.extractor.getVisibility?.(node);
760+
const isStatic = this.extractor.isStatic?.(node) ?? false;
761+
762+
// Property name is a direct identifier child
763+
const nameNode = getChildByField(node, 'name')
764+
|| node.namedChildren.find(c => c.type === 'identifier');
765+
if (!nameNode) return;
766+
767+
const name = getNodeText(nameNode, this.source);
768+
769+
// Get property type from the type child (first named child that isn't modifier or identifier)
770+
const typeNode = node.namedChildren.find(
771+
c => c.type !== 'modifier' && c.type !== 'modifiers'
772+
&& c.type !== 'identifier' && c.type !== 'accessor_list'
773+
&& c.type !== 'accessors' && c.type !== 'equals_value_clause'
774+
);
775+
const typeText = typeNode ? getNodeText(typeNode, this.source) : undefined;
776+
const signature = typeText ? `${typeText} ${name}` : name;
777+
778+
this.createNode('property', name, node, {
779+
docstring,
780+
signature,
781+
visibility,
782+
isStatic,
783+
});
784+
}
785+
746786
/**
747787
* Extract a class field declaration (e.g. Java field_declaration, C# field_declaration).
748788
* Extracts each declarator as a 'field' kind node inside the owning class.
@@ -754,22 +794,34 @@ export class TreeSitterExtractor {
754794
const visibility = this.extractor.getVisibility?.(node);
755795
const isStatic = this.extractor.isStatic?.(node) ?? false;
756796

757-
// Java field_declaration: "private final String name = value;"
758-
// Children include modifiers, type, variable_declarator(s)
759-
const declarators = node.namedChildren.filter(
797+
// Java field_declaration: "private final String name = value;" → variable_declarator(s) are direct children
798+
// C# field_declaration: wraps in variable_declaration → variable_declarator(s)
799+
let declarators = node.namedChildren.filter(
760800
c => c.type === 'variable_declarator'
761801
);
802+
// C#: look inside variable_declaration wrapper
803+
if (declarators.length === 0) {
804+
const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration');
805+
if (varDecl) {
806+
declarators = varDecl.namedChildren.filter(c => c.type === 'variable_declarator');
807+
}
808+
}
762809

763810
if (declarators.length > 0) {
764811
// Get field type from the type child
765-
const typeNode = node.namedChildren.find(
766-
c => c.type !== 'modifiers' && c.type !== 'variable_declarator'
767-
&& c.type !== 'marker_annotation' && c.type !== 'annotation'
812+
// Java: type is a direct child of field_declaration
813+
// C#: type is inside variable_declaration wrapper
814+
const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration');
815+
const typeSearchNode = varDecl ?? node;
816+
const typeNode = typeSearchNode.namedChildren.find(
817+
c => c.type !== 'modifiers' && c.type !== 'modifier' && c.type !== 'variable_declarator'
818+
&& c.type !== 'variable_declaration' && c.type !== 'marker_annotation' && c.type !== 'annotation'
768819
);
769820
const typeText = typeNode ? getNodeText(typeNode, this.source) : undefined;
770821

771822
for (const decl of declarators) {
772-
const nameNode = getChildByField(decl, 'name');
823+
const nameNode = getChildByField(decl, 'name')
824+
|| decl.namedChildren.find(c => c.type === 'identifier');
773825
if (!nameNode) continue;
774826
const name = getNodeText(nameNode, this.source);
775827
const signature = typeText ? `${typeText} ${name}` : name;
@@ -1489,6 +1541,27 @@ export class TreeSitterExtractor {
14891541
}
14901542
}
14911543

1544+
// C#: `class Movie : BaseItem, IPlugin` → base_list with identifier children
1545+
// base_list combines both base class and interfaces in a single colon-separated list.
1546+
// We emit all as 'extends' since the syntax doesn't distinguish them.
1547+
if (child.type === 'base_list') {
1548+
for (const baseType of child.namedChildren) {
1549+
if (baseType) {
1550+
// For generic base types like `ClientBase<T>`, extract just the type name
1551+
const name = baseType.type === 'generic_name'
1552+
? getNodeText(baseType.namedChildren.find((c: SyntaxNode) => c.type === 'identifier') ?? baseType, this.source)
1553+
: getNodeText(baseType, this.source);
1554+
this.unresolvedReferences.push({
1555+
fromNodeId: classId,
1556+
referenceName: name,
1557+
referenceKind: 'extends',
1558+
line: baseType.startPosition.row + 1,
1559+
column: baseType.startPosition.column,
1560+
});
1561+
}
1562+
}
1563+
}
1564+
14921565
// Swift: inheritance_specifier > user_type > type_identifier
14931566
// Used for class inheritance, protocol conformance, and protocol inheritance
14941567
if (child.type === 'inheritance_specifier') {

0 commit comments

Comments
 (0)