All files / src/indexer FormRequestIndexer.ts

0% Statements 0/48
0% Branches 0/16
0% Functions 0/7
0% Lines 0/40

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;
  }
}