diff --git a/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts b/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts index a7abb0a71..5cb2d46ff 100644 --- a/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts @@ -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 diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index dc2756453..ff4f8f28f 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -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 diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 8c683c3b6..f4541d0d2 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -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 } : {}), }, });