Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | // src/indexer/FormRequestIndexer.ts import * as fs from 'fs'; import type { FormRequestDefinition, RequestFieldDefinition } from '../types'; // ─── FormRequest Indexer ────────────────────────────────────────────────── /** * Extracts FormRequest definitions from PHP source. * Parses the rules() method to identify field names and their validation rules. */ export class FormRequestIndexer { /** * Attempt to parse a FormRequest class from PHP source. * Returns null if the file is not a FormRequest. */ static extract(source: string, filePath: string): FormRequestDefinition | null { // Must extend FormRequest or Request if (!/class\s+\w+\s+extends\s+(?:Form)?Request/.test(source)) { return null; } const nsMatch = /^namespace\s+([\w\\]+)/m.exec(source); const classMatch = /class\s+(\w+)\s+extends/.exec(source); if (!classMatch) { return null; } const namespace = nsMatch?.[1] ?? 'App\\Http\\Requests'; const className = classMatch[1]; const fqn = `${namespace}\\${className}`; const fields = FormRequestIndexer.extractFields(source); return { className, namespace, fullyQualifiedName: fqn, filePath, lineNumber: 0, fields, }; } // ─── Extract Field Rules ─────────────────────────────────────────────── private static extractFields(source: string): RequestFieldDefinition[] { const fields: RequestFieldDefinition[] = []; // Find the rules() method body const rulesMatch = /public\s+function\s+rules\s*\(\s*\)\s*(?::\s*array\s*)?\{([\s\S]+?)\n \}/m.exec(source); if (!rulesMatch) { return fields; } const rulesBody = rulesMatch[1]; // Parse 'field' => 'rule|rule2' patterns const stringPattern = /['"]([a-zA-Z_*][a-zA-Z0-9_.*]*)['"]\s*=>\s*['"]([^'"]+)['"]/g; let m: RegExpExecArray | null; while ((m = stringPattern.exec(rulesBody)) !== null) { const fieldName = m[1]; const rules = m[2].split('|').map((r) => r.trim()); const line = (source.slice(0, source.indexOf(m[0]) + rulesMatch.index).match(/\n/g) ?? []).length + 1; fields.push({ name: fieldName, rules, lineNumber: line }); } // Parse 'field' => ['rule', 'rule2', Rule::...] array patterns const arrayPattern = /['"]([a-zA-Z_*][a-zA-Z0-9_.*]*)['"]\s*=>\s*\[([^\]]+)\]/g; while ((m = arrayPattern.exec(rulesBody)) !== null) { const fieldName = m[1]; if (fields.find((f) => f.name === fieldName)) { continue; } // already parsed const rulesRaw = m[2]; const rules: string[] = []; // Extract string rules from array const strRulePattern = /['"]([^'"]+)['"]/g; let rm: RegExpExecArray | null; while ((rm = strRulePattern.exec(rulesRaw)) !== null) { rules.push(...rm[1].split('|').map((r) => r.trim())); } if (rules.length > 0) { fields.push({ name: fieldName, rules, lineNumber: 0 }); } } return fields; } /** * Index all FormRequest files from an array of paths. */ static indexFiles(filePaths: string[]): FormRequestDefinition[] { const results: FormRequestDefinition[] = []; for (const filePath of filePaths) { try { const source = fs.readFileSync(filePath, 'utf8'); const req = FormRequestIndexer.extract(source, filePath); if (req) { results.push(req); } } catch { /* skip unreadable */ } } return results; } } |