Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 149 additions & 2 deletions src/Analyzers/Error/ValidationErrorAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ public function extract(AnalysisContext $ctx): array
}
}

// Also check for inline validation in AST
// Also check for inline validation or container-resolved FormRequest in AST
if (! $hasFormRequest && $ctx->hasAst()) {
$hasFormRequest = $this->hasInlineValidation($ctx);
$hasFormRequest = $this->hasInlineValidation($ctx)
|| $this->hasContainerResolvedFormRequest($ctx);
}

if (! $hasFormRequest) {
Expand Down Expand Up @@ -82,6 +83,152 @@ private function hasInlineValidation(AnalysisContext $ctx): bool
return false;
}

/**
* Detect resolve(FormRequest::class) or app(FormRequest::class) in the method body,
* or $this->method() calls that lead to a FormRequest (up to 3 levels deep).
*/
private function hasContainerResolvedFormRequest(AnalysisContext $ctx): bool
{
if ($ctx->controllerClass() === null) {
return $this->stmtsContainContainerResolve($ctx->astNode->stmts ?? []);
}

return $this->hasValidationInStmts(
$ctx->astNode->stmts ?? [],
$ctx->controllerClass(),
depth: 0,
);
}

private const MAX_DEPTH = 3;

/**
* Check if statements contain resolve()/app() calls or $this->method() calls
* that eventually lead to FormRequest resolution.
*
* @param \PhpParser\Node[] $stmts
*/
private function hasValidationInStmts(array $stmts, string $controllerClass, int $depth): bool
{
if ($depth >= self::MAX_DEPTH) {
return false;
}

// Direct resolve()/app() calls
if ($this->stmtsContainContainerResolve($stmts)) {
return true;
}

// $this->method() calls — check return type or trace into body
$nodeFinder = new \PhpParser\NodeFinder;
$methodCalls = $nodeFinder->findInstanceOf($stmts, \PhpParser\Node\Expr\MethodCall::class);

foreach ($methodCalls as $call) {
if (! $call->var instanceof \PhpParser\Node\Expr\Variable
|| $call->var->name !== 'this'
|| ! $call->name instanceof \PhpParser\Node\Identifier
) {
continue;
}

try {
$refClass = new \ReflectionClass($controllerClass);
$methodName = $call->name->toString();
if (! $refClass->hasMethod($methodName)) {
continue;
}

// Shortcut: return type is FormRequest
$returnType = $refClass->getMethod($methodName)->getReturnType();
if ($returnType instanceof \ReflectionNamedType
&& ! $returnType->isBuiltin()
&& is_subclass_of($returnType->getName(), FormRequest::class)
) {
return true;
}

// Trace into helper method body (recursive)
$declaringClass = $refClass->getMethod($methodName)->getDeclaringClass();
$fileName = $declaringClass->getFileName();
if (! $fileName || ! file_exists($fileName)) {
continue;
}

$helperStmts = $this->getMethodStmts($fileName, $methodName);
if ($helperStmts !== null && $this->hasValidationInStmts($helperStmts, $controllerClass, $depth + 1)) {
return true;
}
} catch (\Throwable) {
continue;
}
}

return false;
}

/**
* Check if statements contain resolve(SomeClass::class) or app(SomeClass::class) calls.
*
* @param \PhpParser\Node[] $stmts
*/
private function stmtsContainContainerResolve(array $stmts): bool
{
$nodeFinder = new \PhpParser\NodeFinder;
$funcCalls = $nodeFinder->findInstanceOf($stmts, \PhpParser\Node\Expr\FuncCall::class);

foreach ($funcCalls as $call) {
if (! $call->name instanceof \PhpParser\Node\Name) {
continue;
}
$funcName = $call->name->toString();
if (in_array($funcName, ['resolve', 'app'], true) && ! empty($call->args)) {
$firstArg = $call->args[0]->value ?? null;
if ($firstArg instanceof \PhpParser\Node\Expr\ClassConstFetch
&& $firstArg->name instanceof \PhpParser\Node\Identifier
&& $firstArg->name->toString() === 'class'
) {
return true;
}
}
}

return false;
}

/**
* Parse a file and extract a specific method's statements.
*
* @return \PhpParser\Node[]|null
*/
private function getMethodStmts(string $filePath, string $methodName): ?array
{
try {
$code = file_get_contents($filePath);
if ($code === false) {
return null;
}

$parser = (new \PhpParser\ParserFactory)->createForNewestSupportedVersion();
$stmts = $parser->parse($code);
if ($stmts === null) {
return null;
}

$nodeFinder = new \PhpParser\NodeFinder;
$classMethods = $nodeFinder->findInstanceOf($stmts, \PhpParser\Node\Stmt\ClassMethod::class);

foreach ($classMethods as $method) {
if ($method->name->toString() === $methodName) {
return $method->stmts ?? [];
}
}
} catch (\Throwable) {
// Parsing failed
}

return null;
}

private function validationErrorSchema(): SchemaObject
{
$custom = $this->handlerAnalyzer?->getErrorSchema(422, includeValidationErrors: true);
Expand Down
Loading