Ver Fonte

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.
Colby McHenry há 2 meses atrás
pai
commit
b712e4de63

+ 1 - 1
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#

+ 1 - 0
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',

+ 2 - 0
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 ---
 

+ 80 - 7
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<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') {