mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-13 08:51:28 +00:00
feat(php): track Eloquent model properties and relationships with structured metadata
- Add PHP property_declaration query to capture class properties ($fillable, $casts, etc.) - Extract array values from Eloquent model properties as description field: - $fillable → comma-separated field names (e.g. "name, email, password") - $casts → key:type pairs (e.g. "email_verified_at:datetime, active:boolean") - $hidden, $guarded, $with, $appends similarly - Detect Eloquent relationship methods (hasMany, hasOne, belongsTo, etc.) and annotate them with relation type and target model (e.g. "hasMany(Post)") Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
556ba91290
commit
bb8aa0c5c5
3 changed files with 125 additions and 0 deletions
|
|
@ -347,6 +347,12 @@ export const PHP_QUERIES = `
|
|||
(method_declaration
|
||||
name: (name) @name) @definition.method
|
||||
|
||||
; ── Class properties (including Eloquent $fillable, $casts, etc.) ────────────
|
||||
(property_declaration
|
||||
(property_element
|
||||
(variable_name
|
||||
(name) @name))) @definition.property
|
||||
|
||||
; ── Imports: use statements ──────────────────────────────────────────────────
|
||||
; Simple: use App\\Models\\User;
|
||||
(namespace_use_declaration
|
||||
|
|
|
|||
|
|
@ -347,6 +347,12 @@ export const PHP_QUERIES = `
|
|||
(method_declaration
|
||||
name: (name) @name) @definition.method
|
||||
|
||||
; ── Class properties (including Eloquent $fillable, $casts, etc.) ────────────
|
||||
(property_declaration
|
||||
(property_element
|
||||
(variable_name
|
||||
(name) @name))) @definition.property
|
||||
|
||||
; ── Imports: use statements ──────────────────────────────────────────────────
|
||||
; Simple: use App\\Models\\User;
|
||||
(namespace_use_declaration
|
||||
|
|
|
|||
|
|
@ -424,6 +424,109 @@ const processBatch = (files: ParseWorkerInput[], onProgress?: (filesProcessed: n
|
|||
return result;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PHP Eloquent metadata extraction
|
||||
// ============================================================================
|
||||
|
||||
/** Eloquent model properties whose array values are worth indexing */
|
||||
const ELOQUENT_ARRAY_PROPS = new Set(['fillable', 'casts', 'hidden', 'guarded', 'with', 'appends']);
|
||||
|
||||
/** Eloquent relationship method names */
|
||||
const ELOQUENT_RELATIONS = new Set([
|
||||
'hasMany', 'hasOne', 'belongsTo', 'belongsToMany',
|
||||
'morphTo', 'morphMany', 'morphOne', 'morphToMany', 'morphedByMany',
|
||||
'hasManyThrough', 'hasOneThrough',
|
||||
]);
|
||||
|
||||
function findDescendant(node: any, type: string): any {
|
||||
if (node.type === type) return node;
|
||||
for (const child of (node.children ?? [])) {
|
||||
const found = findDescendant(child, type);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractStringContent(node: any): string | null {
|
||||
if (!node) return null;
|
||||
const content = node.children?.find((c: any) => c.type === 'string_content');
|
||||
if (content) return content.text;
|
||||
if (node.type === 'string_content') return node.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a PHP property_declaration node, extract array values as a description string.
|
||||
* Returns null if not an Eloquent model property or no array values found.
|
||||
*/
|
||||
function extractPhpPropertyDescription(propName: string, propDeclNode: any): string | null {
|
||||
if (!ELOQUENT_ARRAY_PROPS.has(propName)) return null;
|
||||
|
||||
const arrayNode = findDescendant(propDeclNode, 'array_creation_expression');
|
||||
if (!arrayNode) return null;
|
||||
|
||||
const items: string[] = [];
|
||||
for (const child of (arrayNode.children ?? [])) {
|
||||
if (child.type !== 'array_element_initializer') continue;
|
||||
const children = child.children ?? [];
|
||||
const arrowIdx = children.findIndex((c: any) => c.type === '=>');
|
||||
if (arrowIdx !== -1) {
|
||||
// key => value pair (used in $casts)
|
||||
const key = extractStringContent(children[arrowIdx - 1]);
|
||||
const val = extractStringContent(children[arrowIdx + 1]);
|
||||
if (key && val) items.push(`${key}:${val}`);
|
||||
} else {
|
||||
// Simple value (used in $fillable, $hidden, etc.)
|
||||
const val = extractStringContent(children[0]);
|
||||
if (val) items.push(val);
|
||||
}
|
||||
}
|
||||
|
||||
return items.length > 0 ? items.join(', ') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a PHP method_declaration node, detect if it defines an Eloquent relationship.
|
||||
* Returns description like "hasMany(Post)" or null.
|
||||
*/
|
||||
function extractEloquentRelationDescription(methodNode: any): string | null {
|
||||
function findRelationCall(node: any): any {
|
||||
if (node.type === 'member_call_expression') {
|
||||
const children = node.children ?? [];
|
||||
const objectNode = children.find((c: any) => c.type === 'variable_name' && c.text === '$this');
|
||||
const nameNode = children.find((c: any) => c.type === 'name');
|
||||
if (objectNode && nameNode && ELOQUENT_RELATIONS.has(nameNode.text)) return node;
|
||||
}
|
||||
for (const child of (node.children ?? [])) {
|
||||
const found = findRelationCall(child);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const callNode = findRelationCall(methodNode);
|
||||
if (!callNode) return null;
|
||||
|
||||
const relType = callNode.children?.find((c: any) => c.type === 'name')?.text;
|
||||
const argsNode = callNode.children?.find((c: any) => c.type === 'arguments');
|
||||
let targetModel: string | null = null;
|
||||
if (argsNode) {
|
||||
const firstArg = argsNode.children?.find((c: any) => c.type === 'argument');
|
||||
if (firstArg) {
|
||||
const classConstant = firstArg.children?.find((c: any) =>
|
||||
c.type === 'class_constant_access_expression'
|
||||
);
|
||||
if (classConstant) {
|
||||
targetModel = classConstant.children?.find((c: any) => c.type === 'name')?.text ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (relType && targetModel) return `${relType}(${targetModel})`;
|
||||
if (relType) return relType;
|
||||
return null;
|
||||
}
|
||||
|
||||
const processFileGroup = (
|
||||
files: ParseWorkerInput[],
|
||||
language: SupportedLanguages,
|
||||
|
|
@ -530,6 +633,15 @@ const processFileGroup = (
|
|||
const nodeName = nameNode.text;
|
||||
const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
|
||||
|
||||
let description: string | undefined;
|
||||
if (language === SupportedLanguages.PHP) {
|
||||
if (nodeLabel === 'Property' && captureMap['definition.property']) {
|
||||
description = extractPhpPropertyDescription(nodeName, captureMap['definition.property']) ?? undefined;
|
||||
} else if (nodeLabel === 'Method' && captureMap['definition.method']) {
|
||||
description = extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
result.nodes.push({
|
||||
id: nodeId,
|
||||
label: nodeLabel,
|
||||
|
|
@ -540,6 +652,7 @@ const processFileGroup = (
|
|||
endLine: nameNode.endPosition.row,
|
||||
language: language,
|
||||
isExported: isNodeExported(nameNode, nodeName, language),
|
||||
...(description !== undefined ? { description } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue