Просмотр исходного кода

feat: Add PHP inheritance extraction and improve method call handling

Addresses PHP's base_clause syntax for class inheritance (extends) and implements clause for interface implementation. Adds trait_declaration support and separates property_declaration into fieldTypes. Improves PHP method call extraction by handling member_call_expression and scoped_call_expression with proper receiver name processing, including $ prefix stripping and self/this/parent/static receiver filtering.
Colby McHenry 2 месяцев назад
Родитель
Сommit
e848e6f22f
3 измененных файлов с 49 добавлено и 8 удалено
  1. 31 0
      __tests__/extraction.test.ts
  2. 3 2
      src/extraction/languages/php.ts
  3. 15 6
      src/extraction/tree-sitter.ts

+ 31 - 0
__tests__/extraction.test.ts

@@ -815,6 +815,37 @@ class UserController
     expect(classNode).toBeDefined();
     expect(classNode?.name).toBe('UserController');
   });
+
+  it('should extract class inheritance (extends) and interface implementation', () => {
+    const code = `<?php
+
+class ChildController extends BaseController implements Serializable, JsonSerializable
+{
+    public function serialize(): string
+    {
+        return json_encode($this);
+    }
+}
+`;
+    const result = extractFromSource('ChildController.php', code);
+
+    const classNode = result.nodes.find((n) => n.kind === 'class');
+    expect(classNode).toBeDefined();
+    expect(classNode?.name).toBe('ChildController');
+
+    const extendsRef = result.unresolvedReferences.find(
+      (r) => r.referenceKind === 'extends'
+    );
+    expect(extendsRef).toBeDefined();
+    expect(extendsRef?.referenceName).toBe('BaseController');
+
+    const implementsRefs = result.unresolvedReferences.filter(
+      (r) => r.referenceKind === 'implements'
+    );
+    expect(implementsRefs.length).toBe(2);
+    expect(implementsRefs.map((r) => r.referenceName)).toContain('Serializable');
+    expect(implementsRefs.map((r) => r.referenceName)).toContain('JsonSerializable');
+  });
 });
 
 describe('Swift Extraction', () => {

+ 3 - 2
src/extraction/languages/php.ts

@@ -4,7 +4,7 @@ import type { LanguageExtractor } from '../tree-sitter-types';
 
 export const phpExtractor: LanguageExtractor = {
   functionTypes: ['function_definition'],
-  classTypes: ['class_declaration'],
+  classTypes: ['class_declaration', 'trait_declaration'],
   methodTypes: ['method_declaration'],
   interfaceTypes: ['interface_declaration'],
   structTypes: [],
@@ -13,7 +13,8 @@ export const phpExtractor: LanguageExtractor = {
   typeAliasTypes: [],
   importTypes: ['namespace_use_declaration'],
   callTypes: ['function_call_expression', 'member_call_expression', 'scoped_call_expression'],
-  variableTypes: ['property_declaration', 'const_declaration'],
+  variableTypes: ['const_declaration'],
+  fieldTypes: ['property_declaration'],
   nameField: 'name',
   bodyField: 'body',
   paramsField: 'parameters',

+ 15 - 6
src/extraction/tree-sitter.ts

@@ -1151,17 +1151,25 @@ export class TreeSitterExtractor {
     let calleeName = '';
 
     // Java/Kotlin method_invocation has 'object' + 'name' fields instead of 'function'
+    // PHP member_call_expression has 'object' + 'name', scoped_call_expression has 'scope' + 'name'
     const nameField = getChildByField(node, 'name');
-    const objectField = getChildByField(node, 'object');
+    const objectField = getChildByField(node, 'object') || getChildByField(node, 'scope');
 
-    if (nameField && objectField && node.type === 'method_invocation') {
-      // Java-style method call: receiver.method()
+    if (nameField && objectField && (node.type === 'method_invocation' || node.type === 'member_call_expression' || node.type === 'scoped_call_expression')) {
+      // Method call with explicit receiver: receiver.method() / $receiver->method() / ClassName::method()
       const methodName = getNodeText(nameField, this.source);
-      const receiverName = getNodeText(objectField, this.source);
+      let receiverName = getNodeText(objectField, this.source);
+      // Strip PHP $ prefix from variable names
+      receiverName = receiverName.replace(/^\$/, '');
 
       if (methodName) {
-        // Emit receiver.method form for qualified resolution
-        calleeName = `${receiverName}.${methodName}`;
+        // Skip self/this/parent/static receivers — they don't aid resolution
+        const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super', 'parent', 'static']);
+        if (SKIP_RECEIVERS.has(receiverName)) {
+          calleeName = methodName;
+        } else {
+          calleeName = `${receiverName}.${methodName}`;
+        }
       }
     } else {
       const func = getChildByField(node, 'function') || node.namedChild(0);
@@ -1245,6 +1253,7 @@ export class TreeSitterExtractor {
       if (
         child.type === 'extends_clause' ||
         child.type === 'superclass' ||
+        child.type === 'base_clause' || // PHP class extends
         child.type === 'extends_interfaces' // Java interface extends
       ) {
         // Extract parent class/interface names