From 30f60c18f5efa9e7d5a5495c90f901178cf17a57 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 May 2023 15:39:43 +0200 Subject: [PATCH] CallableType and ClosureType::describe() - use phpdoc-parser Printer for better precision --- src/Type/CallableType.php | 35 ++++++++++-------- src/Type/ClosureType.php | 37 +++++++++++-------- .../Analyser/LegacyNodeScopeResolverTest.php | 4 +- tests/PHPStan/Analyser/data/generics.php | 2 +- tests/PHPStan/Analyser/data/type-aliases.php | 6 +-- .../CallToFunctionParametersRuleTest.php | 10 ++--- .../Rules/Methods/CallMethodsRuleTest.php | 12 +++--- 7 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index cbc56a796f..b968f6e546 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -8,10 +8,13 @@ use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Printer\Printer; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateType; @@ -29,8 +32,6 @@ use function array_map; use function array_merge; use function count; -use function implode; -use function sprintf; /** @api */ class CallableType implements CompoundType, ParametersAcceptor @@ -176,19 +177,23 @@ public function describe(VerbosityLevel $level): string { return $level->handle( static fn (): string => 'callable', - fn (): string => sprintf( - 'callable(%s): %s', - implode(', ', array_map( - static fn (ParameterReflection $param): string => sprintf( - '%s%s%s', - $param->isVariadic() ? '...' : '', - $param->getType()->describe($level), - $param->isOptional() && !$param->isVariadic() ? '=' : '', - ), - $this->getParameters(), - )), - $this->returnType->describe($level), - ), + function (): string { + $printer = new Printer(); + $selfWithoutParameterNames = new self( + array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter( + '', + $p->getType(), + $p->isOptional() && !$p->isVariadic(), + PassedByReference::createNo(), + $p->isVariadic(), + $p->getDefaultValue(), + ), $this->parameters), + $this->returnType, + $this->variadic, + ); + + return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + }, ); } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index f5945dd705..5bde66191c 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -9,6 +9,7 @@ use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Printer\Printer; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; @@ -16,7 +17,9 @@ use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\ClosureCallUnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; @@ -37,8 +40,6 @@ use function array_map; use function array_merge; use function count; -use function implode; -use function sprintf; /** @api */ class ClosureType implements TypeWithClassName, ParametersAcceptor @@ -170,19 +171,25 @@ public function describe(VerbosityLevel $level): string { return $level->handle( static fn (): string => 'Closure', - fn (): string => sprintf( - 'Closure(%s): %s', - implode(', ', array_map( - static fn (ParameterReflection $param): string => sprintf( - '%s%s%s', - $param->isVariadic() ? '...' : '', - $param->getType()->describe($level), - $param->isOptional() && !$param->isVariadic() ? '=' : '', - ), - $this->parameters, - )), - $this->returnType->describe($level), - ), + function (): string { + $printer = new Printer(); + $selfWithoutParameterNames = new self( + array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter( + '', + $p->getType(), + $p->isOptional() && !$p->isVariadic(), + PassedByReference::createNo(), + $p->isVariadic(), + $p->getDefaultValue(), + ), $this->parameters), + $this->returnType, + $this->variadic, + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + ); + + return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + }, ); } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index fb0291156a..e3d135dc0b 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -1412,11 +1412,11 @@ public function dataVarAnnotations(): array '$callable', ], [ - 'callable(int, ...string): void', + 'callable(int, string ...): void', '$callableWithTypes', ], [ - 'Closure(int, ...string): void', + 'Closure(int, string ...): void', '$closureWithTypes', ], [ diff --git a/tests/PHPStan/Analyser/data/generics.php b/tests/PHPStan/Analyser/data/generics.php index 3dd5e75807..9bf1ddbad6 100644 --- a/tests/PHPStan/Analyser/data/generics.php +++ b/tests/PHPStan/Analyser/data/generics.php @@ -131,7 +131,7 @@ function f($a, $b) { $result = []; assertType('array', $a); - assertType('callable(A (function PHPStan\Generics\FunctionsAssertType\f(), argument)): B (function PHPStan\Generics\FunctionsAssertType\f(), argument)', $b); + assertType('callable(A): B', $b); foreach ($a as $k => $v) { assertType('A (function PHPStan\Generics\FunctionsAssertType\f(), argument)', $v); $newV = $b($v); diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php index a6342387ab..c7c8e883fa 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -90,7 +90,7 @@ public function globalAlias($parameter) */ public function localAlias($parameter) { - assertType('callable(string): string|false', $parameter); + assertType('callable(string): (string|false)', $parameter); } /** @@ -98,7 +98,7 @@ public function localAlias($parameter) */ public function nestedLocalAlias($parameter) { - assertType('array', $parameter); + assertType('array', $parameter); } /** @@ -151,7 +151,7 @@ public function testIntAlias($int) } assertType('int|string', (new Foo)->globalAliasProperty); - assertType('callable(string): string|false', (new Foo)->localAliasProperty); + assertType('callable(string): (string|false)', (new Foo)->localAliasProperty); assertType('Countable&Traversable', (new Foo)->importedAliasProperty); assertType('Countable&Traversable', (new Foo)->reexportedAliasProperty); assertType('TypeAliasesDataset\SubScope\Foo', (new Foo)->scopedAliasProperty); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a11dcefb44..de1e2c6ba8 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -554,12 +554,12 @@ public function testArrayReduceCallback(): void 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): (string|null), Closure(string, int): non-empty-string given.', 13, 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): (string|null), Closure(string, int): non-empty-string given.', 22, 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], @@ -574,12 +574,12 @@ public function testArrayReduceArrowFunctionCallback(): void 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): (string|null), Closure(string, int): non-empty-string given.', 11, 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): (string|null), Closure(string, int): non-empty-string given.', 18, 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], @@ -910,7 +910,7 @@ public function testBug2782(): void { $this->analyse([__DIR__ . '/data/bug-2782.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): -1|1 given.', + 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): (-1|1) given.', 13, ], ]); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index d90457661f..614ff09111 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -471,7 +471,7 @@ public function testCallMethods(): void 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array{\'foo\'}|null given.', + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ @@ -791,7 +791,7 @@ public function testCallMethodsOnThisOnly(): void 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array{\'foo\'}|null given.', + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ @@ -2781,21 +2781,21 @@ public function dataCallablesWithoutCheckNullables(): iterable $errors = [ [ - 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar() expects callable(float|null): float|null, Closure(float): float given.', + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar() expects callable(float|null): (float|null), Closure(float): float given.', 25, 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', ], [ - 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz() expects Closure(float|null): float|null, Closure(float): float given.', + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz() expects Closure(float|null): (float|null), Closure(float): float given.', 28, 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', ], [ - 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar2() expects callable(float|null): float, Closure(float|null): float|null given.', + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar2() expects callable(float|null): float, Closure(float|null): (float|null) given.', 32, ], [ - 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz2() expects Closure(float|null): float, Closure(float|null): float|null given.', + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz2() expects Closure(float|null): float, Closure(float|null): (float|null) given.', 35, ], [