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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | // src/providers/validation/RequestValidationProvider.ts import * as vscode from 'vscode'; import type { CacheManager } from '../../cache/CacheManager'; import type { FormRequestDefinition, RequestFieldDefinition } from '../../types'; // All built-in Laravel validation rules const VALIDATION_RULES: Array<{ rule: string; doc: string; hasParam?: boolean }> = [ { rule: 'required', doc: 'The field must be present and not empty.' }, { rule: 'required_if', doc: 'Required if another field equals value.', hasParam: true }, { rule: 'required_unless', doc: 'Required unless another field equals value.', hasParam: true }, { rule: 'required_with', doc: 'Required if any listed fields are present.', hasParam: true }, { rule: 'required_with_all', doc: 'Required if all listed fields are present.', hasParam: true }, { rule: 'required_without', doc: 'Required if any listed fields are absent.', hasParam: true }, { rule: 'required_without_all', doc: 'Required if all listed fields are absent.', hasParam: true }, { rule: 'nullable', doc: 'The field may be null.' }, { rule: 'sometimes', doc: 'Validated only if present.' }, { rule: 'filled', doc: 'Must not be empty when present.' }, { rule: 'present', doc: 'Must be present in input data.' }, { rule: 'string', doc: 'Must be a string.' }, { rule: 'integer', doc: 'Must be an integer.' }, { rule: 'numeric', doc: 'Must be numeric.' }, { rule: 'boolean', doc: 'Must be boolean (true/false/1/0/"true"/"false").' }, { rule: 'array', doc: 'Must be a PHP array.' }, { rule: 'date', doc: 'Must be a valid date.' }, { rule: 'date_format', doc: 'Must match given date format.', hasParam: true }, { rule: 'date_equals', doc: 'Must be equal to given date.', hasParam: true }, { rule: 'before', doc: 'Must be a date before given date.', hasParam: true }, { rule: 'after', doc: 'Must be a date after given date.', hasParam: true }, { rule: 'before_or_equal', doc: 'Must be before or equal to given date.', hasParam: true }, { rule: 'after_or_equal', doc: 'Must be after or equal to given date.', hasParam: true }, { rule: 'email', doc: 'Must be a valid email address.' }, { rule: 'url', doc: 'Must be a valid URL.' }, { rule: 'ip', doc: 'Must be an IP address.' }, { rule: 'ipv4', doc: 'Must be an IPv4 address.' }, { rule: 'ipv6', doc: 'Must be an IPv6 address.' }, { rule: 'uuid', doc: 'Must be a valid UUID.' }, { rule: 'ulid', doc: 'Must be a valid ULID.' }, { rule: 'json', doc: 'Must be a valid JSON string.' }, { rule: 'alpha', doc: 'Must contain only alphabetic characters.' }, { rule: 'alpha_dash', doc: 'Must contain only alpha, numeric, dashes, underscores.' }, { rule: 'alpha_num', doc: 'Must contain only alphanumeric characters.' }, { rule: 'min', doc: 'Minimum value/length.', hasParam: true }, { rule: 'max', doc: 'Maximum value/length.', hasParam: true }, { rule: 'size', doc: 'Must be exact size.', hasParam: true }, { rule: 'between', doc: 'Must be between min and max.', hasParam: true }, { rule: 'digits', doc: 'Must be numeric with exact length.', hasParam: true }, { rule: 'digits_between', doc: 'Must be numeric between min/max digits.', hasParam: true }, { rule: 'in', doc: 'Must be one of given values.', hasParam: true }, { rule: 'not_in', doc: 'Must not be one of given values.', hasParam: true }, { rule: 'in_array', doc: 'Must exist in another field array.', hasParam: true }, { rule: 'exists', doc: 'Must exist in database.', hasParam: true }, { rule: 'unique', doc: 'Must be unique in database.', hasParam: true }, { rule: 'confirmed', doc: 'Must have matching *_confirmation field.' }, { rule: 'same', doc: 'Must match another field.', hasParam: true }, { rule: 'different', doc: 'Must differ from another field.', hasParam: true }, { rule: 'gt', doc: 'Must be greater than another field.', hasParam: true }, { rule: 'gte', doc: 'Must be greater than or equal to another field.', hasParam: true }, { rule: 'lt', doc: 'Must be less than another field.', hasParam: true }, { rule: 'lte', doc: 'Must be less than or equal to another field.', hasParam: true }, { rule: 'file', doc: 'Must be a successfully uploaded file.' }, { rule: 'image', doc: 'Must be an image (jpg, jpeg, png, bmp, gif, svg, webp).' }, { rule: 'mimes', doc: 'Must be of listed MIME types.', hasParam: true }, { rule: 'mimetypes', doc: 'Must match listed MIME types.', hasParam: true }, { rule: 'dimensions', doc: 'Must meet image dimension constraints.', hasParam: true }, { rule: 'max_digits', doc: 'Must not exceed given number of digits.', hasParam: true }, { rule: 'min_digits', doc: 'Must have at least given number of digits.', hasParam: true }, { rule: 'starts_with', doc: 'Must start with one of given values.', hasParam: true }, { rule: 'ends_with', doc: 'Must end with one of given values.', hasParam: true }, { rule: 'doesnt_start_with', doc: 'Must not start with given values.', hasParam: true }, { rule: 'doesnt_end_with', doc: 'Must not end with given values.', hasParam: true }, { rule: 'regex', doc: 'Must match a regular expression.', hasParam: true }, { rule: 'not_regex', doc: 'Must not match a regular expression.', hasParam: true }, { rule: 'timezone', doc: 'Must be a valid timezone identifier.' }, { rule: 'accepted', doc: 'Must be "yes", "on", 1, or true.' }, { rule: 'accepted_if', doc: 'Must be accepted if another field equals value.', hasParam: true }, { rule: 'declined', doc: 'Must be "no", "off", 0, or false.' }, { rule: 'declined_if', doc: 'Must be declined if another field equals value.', hasParam: true }, { rule: 'bail', doc: 'Stop validating after first failure.' }, { rule: 'exclude', doc: 'Exclude from validated data.' }, { rule: 'exclude_if', doc: 'Exclude if another field equals value.', hasParam: true }, { rule: 'exclude_unless', doc: 'Exclude unless another field equals value.', hasParam: true }, { rule: 'exclude_with', doc: 'Exclude if another field is present.', hasParam: true }, { rule: 'exclude_without', doc: 'Exclude if another field is absent.', hasParam: true }, { rule: 'prohibits', doc: 'If present, other listed fields must be absent.', hasParam: true }, { rule: 'prohibited', doc: 'Field must be absent or empty.' }, { rule: 'prohibited_if', doc: 'Must be absent if another field equals value.', hasParam: true }, { rule: 'prohibited_unless', doc: 'Must be absent unless another field equals value.', hasParam: true }, { rule: 'missing', doc: 'Field must be absent from input.' }, { rule: 'missing_if', doc: 'Field must be absent if another field equals value.', hasParam: true }, { rule: 'missing_unless', doc: 'Field must be absent unless condition is true.', hasParam: true }, { rule: 'missing_with', doc: 'Field must be absent if any other fields are present.', hasParam: true }, { rule: 'missing_with_all', doc: 'Field must be absent if all other fields are present.', hasParam: true }, { rule: 'current_password', doc: 'Must match the current user\'s password.' }, { rule: 'password', doc: 'Must pass password validation rules.' }, { rule: 'enum', doc: 'Must be a valid enum value.', hasParam: true }, { rule: 'list', doc: 'Must be a consecutively indexed array (list).' }, { rule: 'ascii', doc: 'Must contain only single-byte ASCII characters.' }, { rule: 'hex_color', doc: 'Must be a valid hexadecimal color.' }, { rule: 'mac_address', doc: 'Must be a valid MAC address.' }, { rule: 'lowercase', doc: 'Must be lowercase.' }, { rule: 'uppercase', doc: 'Must be uppercase.' }, ]; // ─── FormRequest Indexer ────────────────────────────────────────────────── export class FormRequestIndexer { /** * Scans PHP source for FormRequest subclasses and extracts field names * from the rules() return array. */ static extractFromSource(source: string, filePath: string): FormRequestDefinition | null { // Must extend FormRequest const classMatch = /class\s+(\w+)\s+extends\s+(?:\w+\\)*FormRequest/i.exec(source); if (!classMatch) { return null; } const className = classMatch[1]; const nsMatch = /^namespace\s+([\w\\]+)/m.exec(source); const namespace = nsMatch?.[1] ?? 'App\\Http\\Requests'; const fields: RequestFieldDefinition[] = []; // Extract rules() method body const rulesMatch = /public\s+function\s+rules\s*\(\s*\)\s*(?::\s*array\s*)?\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/s.exec(source); if (rulesMatch) { const rulesBody = rulesMatch[1]; // Match 'field_name' => [...rules...] or 'field_name' => 'rule1|rule2' const fieldPattern = /['"]([^'"]+)['"]\s*=>\s*(?:\[([^\]]*)\]|['"]([^'"]*)['"'])/g; let fm: RegExpExecArray | null; while ((fm = fieldPattern.exec(rulesBody)) !== null) { const fieldName = fm[1]; const arrayRules = fm[2] ?? ''; const stringRules = fm[3] ?? ''; const rules = arrayRules ? arrayRules.match(/['"]([^'"]+)['"]/g)?.map(r => r.replace(/['"]/g, '')) ?? [] : stringRules.split('|').filter(Boolean); const lineNum = (source.slice(0, fm.index + rulesMatch.index).match(/\n/g) ?? []).length + 1; fields.push({ name: fieldName, rules, lineNumber: lineNum }); } } return { className, namespace, fullyQualifiedName: `${namespace}\\${className}`, filePath, lineNumber: (source.slice(0, classMatch.index).match(/\n/g) ?? []).length + 1, fields, }; } } // ─── Request Field Completion Provider ──────────────────────────────────── export class RequestFieldCompletionProvider implements vscode.CompletionItemProvider { constructor(private readonly cache: CacheManager) {} provideCompletionItems( document: vscode.TextDocument, position: vscode.Position ): vscode.CompletionItem[] { const lineText = document.lineAt(position).text; const textBefore = lineText.substring(0, position.character); // $request->| or $this->| (inside controller with injected FormRequest) const isRequestChain = /\$(?:request|this)\s*->\s*\w*$/.test(textBefore); if (!isRequestChain) { return []; } // Find which FormRequest is injected in this method's signature const formRequest = this.detectFormRequest(document, position); if (!formRequest) { return []; } return formRequest.fields.map((field) => { const item = new vscode.CompletionItem(field.name, vscode.CompletionItemKind.Field); item.detail = `FormRequest field — ${field.rules.join(', ')}`; item.documentation = new vscode.MarkdownString( `**Field:** \`${field.name}\`\n\n**Rules:** ${field.rules.map(r => `\`${r}\``).join(', ')}` ); item.sortText = `0_${field.name}`; return item; }); } private detectFormRequest( document: vscode.TextDocument, position: vscode.Position ): FormRequestDefinition | undefined { const text = document.getText(); const index = this.cache.getIndex(); // Look backward for a method signature with a FormRequest type-hint const linesBefore = text.slice(0, document.offsetAt(position)).split('\n'); for (let i = linesBefore.length - 1; i >= Math.max(0, linesBefore.length - 30); i--) { const line = linesBefore[i]; // Match: SomeFormRequest $request or StoreUserRequest $request const m = /(\w+Request)\s+\$\w+/.exec(line); if (m) { const requestClass = m[1]; for (const [, fr] of index.formRequests) { if (fr.className === requestClass) { return fr; } } } } return undefined; } } // ─── Validation Rule Completion Provider ───────────────────────────────── export class ValidationRuleCompletionProvider implements vscode.CompletionItemProvider { constructor(private readonly cache: CacheManager) {} provideCompletionItems( document: vscode.TextDocument, position: vscode.Position ): vscode.CompletionItem[] { // Must be inside a validation rules array value // 'field' => ['req| or 'field' => 'req| if (!this.isInsideValidationRule(document, position)) { return []; } const items: vscode.CompletionItem[] = []; const index = this.cache.getIndex(); // Built-in rules for (const { rule, doc, hasParam } of VALIDATION_RULES) { const item = new vscode.CompletionItem(rule, vscode.CompletionItemKind.EnumMember); item.detail = 'Laravel validation rule'; item.documentation = new vscode.MarkdownString(doc); item.insertText = hasParam ? new vscode.SnippetString(`${rule}:$1`) : rule; item.sortText = `0_${rule}`; items.push(item); } // exists:table,column — complete with DB tables for (const [table, cols] of index.dbSchema) { const existsItem = new vscode.CompletionItem(`exists:${table},id`, vscode.CompletionItemKind.EnumMember); existsItem.detail = `DB table: ${table}`; existsItem.sortText = `1_exists_${table}`; items.push(existsItem); const uniqueItem = new vscode.CompletionItem(`unique:${table}`, vscode.CompletionItemKind.EnumMember); uniqueItem.detail = `DB table: ${table}`; uniqueItem.sortText = `1_unique_${table}`; items.push(uniqueItem); for (const col of cols) { const colItem = new vscode.CompletionItem(`exists:${table},${col.name}`, vscode.CompletionItemKind.EnumMember); colItem.detail = `${col.type}`; colItem.sortText = `2_exists_${table}_${col.name}`; items.push(colItem); } } // Rule:: class completions const ruleClassItems: Array<{ name: string; doc: string }> = [ { name: 'Rule::exists', doc: 'Fluent exists rule builder' }, { name: 'Rule::unique', doc: 'Fluent unique rule builder' }, { name: 'Rule::in', doc: 'Fluent in rule builder' }, { name: 'Rule::notIn', doc: 'Fluent not-in rule builder' }, { name: 'Rule::enum', doc: 'Enum validation rule' }, { name: 'Rule::password', doc: 'Password strength rule builder' }, { name: 'Rule::dimensions', doc: 'Image dimensions rule builder' }, { name: 'Rule::file', doc: 'Fluent file rule builder' }, { name: 'Rule::imageFile', doc: 'Image file rule builder' }, { name: 'Rule::when', doc: 'Conditional rule application' }, { name: 'Rule::requiredIf', doc: 'Fluent required-if rule' }, { name: 'Rule::excludeIf', doc: 'Fluent exclude-if rule' }, { name: 'Rule::prohibitedIf', doc: 'Fluent prohibited-if rule' }, { name: 'Rule::can', doc: 'Gate authorization rule' }, ]; for (const { name, doc } of ruleClassItems) { const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Class); item.detail = doc; item.sortText = `3_${name}`; items.push(item); } return items; } private isInsideValidationRule(document: vscode.TextDocument, position: vscode.Position): boolean { const text = document.getText(); const offset = document.offsetAt(position); // Simple check: are we in a rules() method? const before = text.slice(0, offset); const rulesMethodIdx = before.lastIndexOf('function rules()'); if (rulesMethodIdx === -1) { // Also check for $rules = [...] pattern const rulesArrayIdx = before.lastIndexOf('$rules = ['); if (rulesArrayIdx === -1 && !before.includes('->validate(') && !before.includes('Validator::make(')) { return false; } } // Check that we're inside an array value (after =>) const lineText = document.lineAt(position).text.substring(0, position.character); return /(?:=>\s*\[?['"][^'"]*|'[^']*$|"[^"]*$)/.test(lineText); } } // ─── "Add Eloquent Fields to rules()" Code Action ──────────────────────── export class AddFieldsToRulesAction implements vscode.CodeActionProvider { static readonly providedCodeActionKinds = [vscode.CodeActionKind.Refactor]; constructor(_cache: CacheManager) {} provideCodeActions( document: vscode.TextDocument, range: vscode.Range ): vscode.CodeAction[] { if (!document.fileName.includes('Request')) { return []; } const lineText = document.lineAt(range.start.line).text; if (!lineText.includes('rules()')) { return []; } const action = new vscode.CodeAction( '✨ Add Eloquent model fields to rules()', vscode.CodeActionKind.Refactor ); action.command = { command: 'laravelMaestro.addModelFieldsToRules', title: 'Add model fields to rules()', arguments: [document, range], }; return [action]; } } |