From e848e6f22f5a6d2ec29b5a4392642f7b746590de Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Mon, 6 Apr 2026 22:10:23 -0500 Subject: [PATCH] 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. --- __tests__/extraction.test.ts | 31 +++++++++++++++++++++++++++++++ src/extraction/languages/php.ts | 5 +++-- src/extraction/tree-sitter.ts | 21 +++++++++++++++------ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 59535e1..d0246f6 100644 --- a/__tests__/extraction.test.ts +++ b/__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 = ` 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', () => { diff --git a/src/extraction/languages/php.ts b/src/extraction/languages/php.ts index 5e2a9c5..be3795e 100644 --- a/src/extraction/languages/php.ts +++ b/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', diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 602371c..093ff67 100644 --- a/src/extraction/tree-sitter.ts +++ b/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