diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 696104b2014..6e1ef776098 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -186,6 +186,7 @@ use function is_array; use function is_int; use function is_string; +use function max; use function sprintf; use function strtolower; use function trim; @@ -3301,33 +3302,34 @@ private function doCreateCallableParameters(Scope $scope, Expr $closureExpr, ?ar $acceptors = $passedToType->getCallableParametersAcceptors($scope); foreach ($acceptors as $acceptor) { + $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ), $acceptor->getParameters()); + if ($callableParameters === null) { - $callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $callableParameter->getType(), - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - ), $acceptor->getParameters()); + $callableParameters = $acceptorParameters; continue; } $newParameters = []; - foreach ($acceptor->getParameters() as $i => $callableParameter) { + $parameterCount = max(count($callableParameters), count($acceptorParameters)); + for ($i = 0; $i < $parameterCount; $i++) { + if (!array_key_exists($i, $acceptorParameters)) { + $newParameters[] = $callableParameters[$i]->toOptional(); + continue; + } + if (!array_key_exists($i, $callableParameters)) { - $newParameters[] = $callableParameter; + $newParameters[] = $acceptorParameters[$i]->toOptional(); continue; } - $newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $callableParameter->getType(), - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - )); + $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]); } $callableParameters = $newParameters; diff --git a/src/Reflection/Native/NativeParameterReflection.php b/src/Reflection/Native/NativeParameterReflection.php index e8120868302..19da0a741a0 100644 --- a/src/Reflection/Native/NativeParameterReflection.php +++ b/src/Reflection/Native/NativeParameterReflection.php @@ -51,6 +51,23 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + /** Used when merging signatures where only some of them declare this parameter. */ + public function toOptional(): self + { + if ($this->optional) { + return $this; + } + + return new self( + $this->name, + true, + $this->type, + $this->passedByReference, + $this->variadic, + $this->defaultValue, + ); + } + public function union(self $other): self { return new self( diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 06788c89415..18218040b39 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1608,6 +1608,14 @@ public function testBug14707(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0.0')] + public function testBug15003(): void + { + // crash + $errors = $this->runAnalyse(__DIR__ . '/data/bug-15003.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return list diff --git a/tests/PHPStan/Analyser/data/bug-15003.php b/tests/PHPStan/Analyser/data/bug-15003.php new file mode 100644 index 00000000000..fcb93192b21 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-15003.php @@ -0,0 +1,82 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug15003; + +use Closure; + +/** + * @phpstan-type Foo InvokableClass|callable(string, mixed): int + */ +class TypeImportShortcut {} + +interface InvokableClass +{ + /** + * @param Closure(string, ?string=): string $fail + */ + public function __invoke(string $foo, Closure $fail): int; +} + +/** @phpstan-import-type Foo from TypeImportShortcut */ +class A +{ + + /** @param callable(string):Foo|Foo $param */ + public function foo($param): void {} + +} + +(new A)->foo(function(string $foo) { + return 5; +}); + +interface InvokableRule +{ + + /** + * @param Closure(string): string $fail + */ + public function __invoke(string $attribute, mixed $value, Closure $fail): void; + +} + +/** + * @phpstan-type FieldValidationRule InvokableRule|(callable(string, mixed, Closure): void) + * @phpstan-type ValidationRules array|FieldValidationRule + */ +final class Field +{ + + /** + * @param (callable(string): ValidationRules)|ValidationRules $rules + */ + public function rules($rules): self + { + return $this; + } + + /** + * @param (callable(string): ValidationRules)|ValidationRules ...$rules + */ + public function creationRules($rules): self + { + return $this; + } + +} + +function rules(): Field +{ + return (new Field())->rules(function ($attribute, $value, $fail) { + }); +} + +function creationRules(): Field +{ + return (new Field())->creationRules([ + function ($attribute, $value, $fail) { + }, + ]); +} diff --git a/tests/PHPStan/Analyser/nsrt/closure-passed-to-union-of-callables.php b/tests/PHPStan/Analyser/nsrt/closure-passed-to-union-of-callables.php new file mode 100644 index 00000000000..1cd77896227 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-passed-to-union-of-callables.php @@ -0,0 +1,89 @@ +|callable(bool, float): void $cb + */ + public function withInvokable($cb): void + { + } + + /** + * @param callable(int...): void|callable(string, string): void $cb + */ + public function variadicFirst($cb): void + { + } + + /** + * @param callable(string, string): void|callable(int...): void $cb + */ + public function variadicLast($cb): void + { + } + + public function run(): void + { + $this->longestFirst(function ($a, $b): void { + assertType('int', $a); + assertType('string', $b); + }); + + $this->shortestFirst(function ($a, $b): void { + assertType('int', $a); + assertType('string', $b); + }); + + $this->longestFirst(fn ($a, $b) => assertType('int', $a)); + $this->longestFirst(fn ($a, $b) => assertType('string', $b)); + $this->shortestFirst(fn ($a, $b) => assertType('int', $a)); + $this->shortestFirst(fn ($a, $b) => assertType('string', $b)); + + $this->withInvokable(function ($a, $b): void { + assertType('bool|int|string', $a); + assertType('float|string', $b); + }); + + $this->variadicFirst(function ($a, $b): void { + assertType('int|string', $a); + assertType('string', $b); + }); + + $this->variadicLast(function ($a, $b): void { + assertType('int|string', $a); + assertType('string', $b); + }); + } + +}