diff --git a/docs/SEARCH_QUALITY_LOOP.md b/docs/SEARCH_QUALITY_LOOP.md index 43d9529..cb040b5 100644 --- a/docs/SEARCH_QUALITY_LOOP.md +++ b/docs/SEARCH_QUALITY_LOOP.md @@ -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# diff --git a/src/extraction/languages/csharp.ts b/src/extraction/languages/csharp.ts index e385ef3..9de5373 100644 --- a/src/extraction/languages/csharp.ts +++ b/src/extraction/languages/csharp.ts @@ -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', diff --git a/src/extraction/tree-sitter-types.ts b/src/extraction/tree-sitter-types.ts index e482da8..bfdaab3 100644 --- a/src/extraction/tree-sitter-types.ts +++ b/src/extraction/tree-sitter-types.ts @@ -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 --- diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 8f1ae58..0263316 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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`, 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') {