feat: Add complete PHP language support with trait handling and property extraction
Addresses PHP traits extracted as classes, missing class properties, skipped constants, and invisible trait usage. Adds classifyClassNode to distinguish traits from classes, fixes property extraction for PHP's property_element AST structure (added 4,366 field nodes), and adds visitNode hook for class constants and trait use declarations (increased trait edges from 636 to 1,514). Also improves Liquid schema name handling and file path reference resolution. Verified against Laravel codebase.
This commit is contained in:
@@ -19,6 +19,9 @@ export const phpExtractor: LanguageExtractor = {
|
||||
bodyField: 'body',
|
||||
paramsField: 'parameters',
|
||||
returnField: 'return_type',
|
||||
classifyClassNode: (node) => {
|
||||
return node.type === 'trait_declaration' ? 'trait' : 'class';
|
||||
},
|
||||
getVisibility: (node) => {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
@@ -38,6 +41,43 @@ export const phpExtractor: LanguageExtractor = {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
visitNode: (node, ctx) => {
|
||||
// Handle class constants: const_declaration inside classes
|
||||
// These are skipped by the main visitor because variableTypes check excludes class-like contexts
|
||||
if (node.type === 'const_declaration') {
|
||||
const constElements = node.namedChildren.filter((c: SyntaxNode) => c.type === 'const_element');
|
||||
for (const elem of constElements) {
|
||||
const nameNode = elem.namedChildren.find((c: SyntaxNode) => c.type === 'name');
|
||||
if (!nameNode) continue;
|
||||
const name = getNodeText(nameNode, ctx.source);
|
||||
ctx.createNode('constant', name, elem, {});
|
||||
}
|
||||
return true; // handled
|
||||
}
|
||||
|
||||
// Handle trait usage: use TraitName, OtherTrait; inside classes
|
||||
// Creates unresolved references that will be resolved to 'implements' edges
|
||||
if (node.type === 'use_declaration') {
|
||||
const names = node.namedChildren.filter((c: SyntaxNode) => c.type === 'name' || c.type === 'qualified_name');
|
||||
const parentId = ctx.nodeStack.length > 0 ? ctx.nodeStack[ctx.nodeStack.length - 1] : undefined;
|
||||
if (parentId) {
|
||||
for (const nameNode of names) {
|
||||
const traitName = getNodeText(nameNode, ctx.source);
|
||||
ctx.addUnresolvedReference({
|
||||
fromNodeId: parentId,
|
||||
referenceName: traitName,
|
||||
referenceKind: 'implements',
|
||||
filePath: ctx.filePath,
|
||||
line: node.startPosition.row + 1,
|
||||
column: node.startPosition.column,
|
||||
});
|
||||
}
|
||||
}
|
||||
return true; // handled
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
extractImport: (node, source) => {
|
||||
const importText = source.substring(node.startIndex, node.endIndex).trim();
|
||||
|
||||
|
||||
@@ -252,7 +252,10 @@ export class LiquidExtractor {
|
||||
try {
|
||||
const schemaJson = JSON.parse(schemaContent!);
|
||||
if (schemaJson.name) {
|
||||
schemaName = schemaJson.name;
|
||||
// Shopify schema names can be translation objects like {"en": "...", "fr": "..."}
|
||||
schemaName = typeof schemaJson.name === 'string'
|
||||
? schemaJson.name
|
||||
: schemaJson.name.en || Object.values(schemaJson.name)[0] as string || 'schema';
|
||||
}
|
||||
} catch {
|
||||
// Schema isn't valid JSON, use default name
|
||||
|
||||
@@ -154,7 +154,7 @@ export interface LanguageExtractor {
|
||||
* Classify a class_declaration node when the grammar reuses one node type
|
||||
* for multiple concepts (e.g. Swift uses class_declaration for classes, structs, and enums).
|
||||
*/
|
||||
classifyClassNode?: (node: SyntaxNode) => 'class' | 'struct' | 'enum' | 'interface';
|
||||
classifyClassNode?: (node: SyntaxNode) => 'class' | 'struct' | 'enum' | 'interface' | 'trait';
|
||||
|
||||
/**
|
||||
* Resolve the body node for a function/method/class when it's not a child field.
|
||||
|
||||
@@ -266,6 +266,8 @@ export class TreeSitterExtractor {
|
||||
this.extractEnum(node);
|
||||
} else if (classification === 'interface') {
|
||||
this.extractInterface(node);
|
||||
} else if (classification === 'trait') {
|
||||
this.extractClass(node, 'trait');
|
||||
} else {
|
||||
this.extractClass(node);
|
||||
}
|
||||
@@ -542,7 +544,7 @@ export class TreeSitterExtractor {
|
||||
/**
|
||||
* Extract a class
|
||||
*/
|
||||
private extractClass(node: SyntaxNode): void {
|
||||
private extractClass(node: SyntaxNode, kind: NodeKind = 'class'): void {
|
||||
if (!this.extractor) return;
|
||||
|
||||
const name = extractName(node, this.source, this.extractor);
|
||||
@@ -550,7 +552,7 @@ export class TreeSitterExtractor {
|
||||
const visibility = this.extractor.getVisibility?.(node);
|
||||
const isExported = this.extractor.isExported?.(node, this.source);
|
||||
|
||||
const classNode = this.createNode('class', name, node, {
|
||||
const classNode = this.createNode(kind, name, node, {
|
||||
docstring,
|
||||
visibility,
|
||||
isExported,
|
||||
@@ -864,6 +866,35 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
// PHP property_declaration: property_element → variable_name → name
|
||||
if (declarators.length === 0) {
|
||||
const propElements = node.namedChildren.filter(c => c.type === 'property_element');
|
||||
if (propElements.length > 0) {
|
||||
// Get type annotation if present (e.g. "string", "int", "?Foo")
|
||||
const typeNode = node.namedChildren.find(
|
||||
c => c.type !== 'visibility_modifier' && c.type !== 'static_modifier'
|
||||
&& c.type !== 'readonly_modifier' && c.type !== 'property_element'
|
||||
&& c.type !== 'var_modifier'
|
||||
);
|
||||
const typeText = typeNode ? getNodeText(typeNode, this.source) : undefined;
|
||||
|
||||
for (const elem of propElements) {
|
||||
const varName = elem.namedChildren.find(c => c.type === 'variable_name');
|
||||
const nameNode = varName?.namedChildren.find(c => c.type === 'name');
|
||||
if (!nameNode) continue;
|
||||
const name = getNodeText(nameNode, this.source);
|
||||
const signature = typeText ? `${typeText} $${name}` : `$${name}`;
|
||||
this.createNode('field', name, elem, {
|
||||
docstring,
|
||||
signature,
|
||||
visibility,
|
||||
isStatic,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (declarators.length > 0) {
|
||||
// Get field type from the type child
|
||||
// Java: type is a direct child of field_declaration
|
||||
@@ -1458,6 +1489,7 @@ export class TreeSitterExtractor {
|
||||
if (classification === 'struct') this.extractStruct(node);
|
||||
else if (classification === 'enum') this.extractEnum(node);
|
||||
else if (classification === 'interface') this.extractInterface(node);
|
||||
else if (classification === 'trait') this.extractClass(node, 'trait');
|
||||
else this.extractClass(node);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user