From 668b4cfad260007948be7188607892a087a82788 Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Thu, 25 Jan 2018 10:58:25 -0500 Subject: [PATCH 1/8] Added support & tests for the redirect helper --- .gitignore | 1 + composer.json | 1 + src/HelpersReturnTypeExtension.php | 7 ++++ tests/HelpersReturnTypeExtensionTest.php | 51 ++++++++++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 tests/HelpersReturnTypeExtensionTest.php diff --git a/.gitignore b/.gitignore index 4fbb073..3c401de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor/ /composer.lock +.idea diff --git a/composer.json b/composer.json index 64520f9..e867ebf 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ } ], "require": { + "php": "7.*", "laravel/framework": "5.5.*", "phpstan/phpstan": "^0.9" }, diff --git a/src/HelpersReturnTypeExtension.php b/src/HelpersReturnTypeExtension.php index d4cf2bc..51dd6c6 100644 --- a/src/HelpersReturnTypeExtension.php +++ b/src/HelpersReturnTypeExtension.php @@ -22,6 +22,7 @@ final class HelpersReturnTypeExtension implements DynamicFunctionReturnTypeExten 'app', 'validator', 'view', + 'redirect', ]; /** @@ -68,6 +69,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } return new ObjectType(\Illuminate\View\View::class); + + case 'redirect': + if (empty($functionCall->args)) { + return new ObjectType(\Illuminate\Routing\Redirector::class); + } + return new ObjectType(\Illuminate\Http\RedirectResponse::class); } return new MixedType(); diff --git a/tests/HelpersReturnTypeExtensionTest.php b/tests/HelpersReturnTypeExtensionTest.php new file mode 100644 index 0000000..5dc2f3e --- /dev/null +++ b/tests/HelpersReturnTypeExtensionTest.php @@ -0,0 +1,51 @@ +extension = new HelpersReturnTypeExtension(); + } + + public function redirectDataProvider() + { + return [ + 'No arguments' => [[], Redirector::class], + '1 arguments' => [['/'], RedirectResponse::class], + '2 arguments' => [['/', 301], RedirectResponse::class], + '3 arguments' => [['/', 307, ['expired' => '2018-01-25 10:43:00']], RedirectResponse::class], + '4 arguments' => [['/', 307, ['expired' => '2018-01-25 10:43:00'], true], RedirectResponse::class], + ]; + } + + /** @dataProvider redirectDataProvider */ + public function testRedirectHelper($arguments, $expectedType) + { + $functionReflection = $this->createMock(FunctionReflection::class); + $functionReflection->method('getName') + ->willReturn('redirect'); + + $this->assertTrue($this->extension->isFunctionSupported($functionReflection)); + + $functionCall = new FuncCall(new Name('redirect'), $arguments, []); + $scope = $this->createMock(Scope::class); + + $type = $this->extension->getTypeFromFunctionCall($functionReflection, $functionCall, $scope); + $this->assertInstanceOf(ObjectType::class, $type); + $this->assertEquals($expectedType, $type->getClassName()); + } +} From d26a30a3a2b198ba9ce204d4fea8a6ba350b919c Mon Sep 17 00:00:00 2001 From: medisun Date: Mon, 25 Dec 2017 17:41:05 +0200 Subject: [PATCH 2/8] rootDir is phpstan vendor folder --- extension.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension.neon b/extension.neon index 836a12f..0c0f500 100644 --- a/extension.neon +++ b/extension.neon @@ -19,4 +19,4 @@ services: - phpstan.broker.dynamicFunctionReturnTypeExtension parameters: - bootstrap: %rootDir%/vendor/weebly/phpstan-laravel/bootstrap.php + bootstrap: %rootDir%/../../weebly/phpstan-laravel/bootstrap.php From 2f8062ebef1ed3d98e5cc6b7671d6d805bc7901e Mon Sep 17 00:00:00 2001 From: medisun Date: Mon, 25 Dec 2017 17:42:26 +0200 Subject: [PATCH 3/8] need three level up: vendor/author/package --- bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap.php b/bootstrap.php index 46653c9..6d17458 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,5 +1,5 @@ make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); From 8fdaad04635840b12cf1929b7b4db062f4aaf059 Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Thu, 25 Jan 2018 14:04:25 -0500 Subject: [PATCH 4/8] Read facades annotations --- bootstrap.php | 17 ++++++- src/FacadeMethodExtension.php | 83 ++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/bootstrap.php b/bootstrap.php index 6d17458..832e43a 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,5 +1,18 @@ -make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); diff --git a/src/FacadeMethodExtension.php b/src/FacadeMethodExtension.php index 457c343..4796cb3 100644 --- a/src/FacadeMethodExtension.php +++ b/src/FacadeMethodExtension.php @@ -1,11 +1,16 @@ -methodReflectionFactory = $methodReflectionFactory; + $this->phpDocStringResolver = $phpDocStringResolver; } /** @@ -61,18 +74,19 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): { if ($classReflection->isSubclassOf(Facade::class)) { if (!isset($this->methods[$classReflection->getName()])) { - /** @var \Illuminate\Support\Facades\Facade $class */ $class = $classReflection->getName(); $instance = $class::getFacadeRoot(); $instanceReflection = $this->broker->getClass(get_class($instance)); - $this->methods[$classReflection->getName()] = $this->createMethods($classReflection, $instanceReflection); + $this->methods[$classReflection->getName()] = $this->createMethods($classReflection, + $instanceReflection); if (isset($this->extensions[$instanceReflection->getName()])) { $extensionMethod = $this->extensions[$instanceReflection->getName()]; $extensionReflection = $this->broker->getClass(get_class($instance->$extensionMethod())); - $this->methods[$classReflection->getName()] += $this->createMethods($classReflection, $extensionReflection); + $this->methods[$classReflection->getName()] += $this->createMethods($classReflection, + $extensionReflection); } } } @@ -93,13 +107,50 @@ public function getMethod(ClassReflection $classReflection, string $methodName): * @param \PHPStan\Reflection\ClassReflection $instance * * @return \PHPStan\Reflection\MethodReflection[] - * * @throws \PHPStan\ShouldNotHappenException */ private function createMethods(ClassReflection $classReflection, ClassReflection $instance): array { $methods = []; + + $docBlock = $classReflection->getNativeReflection()->getDocComment(); + + if ($docBlock !== false) { + $nameScope = new NameScope($classReflection->getNativeReflection()->getNamespaceName()); + $doc = $this->phpDocStringResolver->resolve( + $docBlock, + $nameScope + ); + // TODO: Find a way to not copy-paste this from + // \PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension + foreach ($doc->getMethodTags() as $methodName => $methodTag) { + $parameters = []; + foreach ($methodTag->getParameters() as $parameterName => $parameterTag) { + $parameters[] = new AnnotationsMethodParameterReflection( + $parameterName, + $parameterTag->getType(), + $parameterTag->isPassedByReference(), + $parameterTag->isOptional(), + $parameterTag->isVariadic() + ); + } + + $methods[$methodName] = new AnnotationMethodReflection( + $methodName, + $classReflection, + $methodTag->getReturnType(), + $parameters, + $methodTag->isStatic(), + $this->detectMethodVariadic($parameters) + ); + } + } + foreach ($instance->getNativeReflection()->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + // Trust annotated methods + if (isset($methods[$method->getName()])) { + continue; + } $methods[$method->getName()] = $this->methodReflectionFactory->create( $classReflection, $method, @@ -109,4 +160,22 @@ private function createMethods(ClassReflection $classReflection, ClassReflection return $methods; } + + /** + * Detect if last parameter is variadic + * @see \PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension + * @param AnnotationsMethodParameterReflection[] $parameters + * @return bool + */ + private function detectMethodVariadic(array $parameters): bool + { + if ($parameters === []) { + return false; + } + + $possibleVariadicParameterIndex = count($parameters) - 1; + $possibleVariadicParameter = $parameters[$possibleVariadicParameterIndex]; + + return $possibleVariadicParameter->isVariadic(); + } } From 17e0dcfedd9f31eef2b11319510e7b5caf37ffb8 Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Thu, 25 Jan 2018 16:16:57 -0500 Subject: [PATCH 5/8] response helper support --- src/FacadeMethodExtension.php | 12 ++++++--- src/HelpersReturnTypeExtension.php | 7 +++++ tests/HelpersReturnTypeExtensionTest.php | 33 ++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/FacadeMethodExtension.php b/src/FacadeMethodExtension.php index 4796cb3..46f0b6d 100644 --- a/src/FacadeMethodExtension.php +++ b/src/FacadeMethodExtension.php @@ -79,14 +79,18 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): $instance = $class::getFacadeRoot(); $instanceReflection = $this->broker->getClass(get_class($instance)); - $this->methods[$classReflection->getName()] = $this->createMethods($classReflection, - $instanceReflection); + $this->methods[$classReflection->getName()] = $this->createMethods( + $classReflection, + $instanceReflection + ); if (isset($this->extensions[$instanceReflection->getName()])) { $extensionMethod = $this->extensions[$instanceReflection->getName()]; $extensionReflection = $this->broker->getClass(get_class($instance->$extensionMethod())); - $this->methods[$classReflection->getName()] += $this->createMethods($classReflection, - $extensionReflection); + $this->methods[$classReflection->getName()] += $this->createMethods( + $classReflection, + $extensionReflection + ); } } } diff --git a/src/HelpersReturnTypeExtension.php b/src/HelpersReturnTypeExtension.php index 51dd6c6..2d5a84a 100644 --- a/src/HelpersReturnTypeExtension.php +++ b/src/HelpersReturnTypeExtension.php @@ -23,6 +23,7 @@ final class HelpersReturnTypeExtension implements DynamicFunctionReturnTypeExten 'validator', 'view', 'redirect', + 'response', ]; /** @@ -75,6 +76,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ObjectType(\Illuminate\Routing\Redirector::class); } return new ObjectType(\Illuminate\Http\RedirectResponse::class); + + case 'response': + if (empty($functionCall->args)) { + return new ObjectType(\Illuminate\Contracts\Routing\ResponseFactory::class); + } + return new ObjectType(\Symfony\Component\HttpFoundation\Response::class); } return new MixedType(); diff --git a/tests/HelpersReturnTypeExtensionTest.php b/tests/HelpersReturnTypeExtensionTest.php index 5dc2f3e..7a461ac 100644 --- a/tests/HelpersReturnTypeExtensionTest.php +++ b/tests/HelpersReturnTypeExtensionTest.php @@ -2,6 +2,7 @@ namespace Tests\Weebly\PHPStan\Laravel; +use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Http\RedirectResponse; use Illuminate\Routing\Redirector; use PhpParser\Node\Expr\FuncCall; @@ -9,6 +10,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ObjectType; +use Symfony\Component\HttpFoundation\Response; use Weebly\PHPStan\Laravel\HelpersReturnTypeExtension; class HelpersReturnTypeExtensionTest extends \PHPStan\Testing\TestCase @@ -27,8 +29,8 @@ public function redirectDataProvider() 'No arguments' => [[], Redirector::class], '1 arguments' => [['/'], RedirectResponse::class], '2 arguments' => [['/', 301], RedirectResponse::class], - '3 arguments' => [['/', 307, ['expired' => '2018-01-25 10:43:00']], RedirectResponse::class], - '4 arguments' => [['/', 307, ['expired' => '2018-01-25 10:43:00'], true], RedirectResponse::class], + '3 arguments' => [['/', 307, ['expires' => '2018-01-25 10:43:00']], RedirectResponse::class], + '4 arguments' => [['/', 307, ['expires' => '2018-01-25 10:43:00'], true], RedirectResponse::class], ]; } @@ -48,4 +50,31 @@ public function testRedirectHelper($arguments, $expectedType) $this->assertInstanceOf(ObjectType::class, $type); $this->assertEquals($expectedType, $type->getClassName()); } + + public function responseDataProvider() + { + return [ + 'No arguments' => [[], ResponseFactory::class], + '1 arguments' => [['Hello World'], Response::class], + '2 arguments' => [['Hello World', 204], Response::class], + '3 arguments' => [['Hello World', 200, ['expires' => '2018-01-25 10:43:00']], Response::class], + ]; + } + + /** @dataProvider responseDataProvider */ + public function testResponseHelper($arguments, $expectedType) + { + $functionReflection = $this->createMock(FunctionReflection::class); + $functionReflection->method('getName') + ->willReturn('response'); + + $this->assertTrue($this->extension->isFunctionSupported($functionReflection)); + + $functionCall = new FuncCall(new Name('response'), $arguments, []); + $scope = $this->createMock(Scope::class); + + $type = $this->extension->getTypeFromFunctionCall($functionReflection, $functionCall, $scope); + $this->assertInstanceOf(ObjectType::class, $type); + $this->assertEquals($expectedType, $type->getClassName()); + } } From 26268ff106b48d40904ef33f294f073869c63f84 Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Thu, 25 Jan 2018 16:17:03 -0500 Subject: [PATCH 6/8] Lumen support --- bootstrap.php | 4 +++- composer.json | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bootstrap.php b/bootstrap.php index 832e43a..7fdf814 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -15,4 +15,6 @@ throw new Exception('Could not find app boostrap, tried: ' . implode(', ', $paths)); } -$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +if ($app instanceof Illuminate\Foundation\Application) { + $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +} diff --git a/composer.json b/composer.json index e867ebf..9654343 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,6 @@ ], "require": { "php": "7.*", - "laravel/framework": "5.5.*", "phpstan/phpstan": "^0.9" }, "require-dev": { From 7b8d6c3a174898927501d3801eb6db3edb087b5b Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Fri, 5 Oct 2018 16:01:58 -0400 Subject: [PATCH 7/8] Update dependencies to 0.10 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9654343..25e9ca5 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "php": "7.*", - "phpstan/phpstan": "^0.9" + "phpstan/phpstan": "^0.10" }, "require-dev": { "phpunit/phpunit": "^6.5.2" From cf107d14683c611595d69ae77ce39f2d4672464a Mon Sep 17 00:00:00 2001 From: Hugo Vacher Date: Fri, 5 Oct 2018 16:08:29 -0400 Subject: [PATCH 8/8] Update classes to be compatible with 0.10 --- src/BuilderMethodExtension.php | 2 +- src/FacadeMethodExtension.php | 2 +- src/MacroMethodExtension.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BuilderMethodExtension.php b/src/BuilderMethodExtension.php index da6ffef..2267295 100644 --- a/src/BuilderMethodExtension.php +++ b/src/BuilderMethodExtension.php @@ -41,7 +41,7 @@ public function __construct(MethodReflectionFactory $methodReflectionFactory) /** * @inheritdoc */ - public function setBroker(Broker $broker) + public function setBroker(Broker $broker): void { $this->broker = $broker; } diff --git a/src/FacadeMethodExtension.php b/src/FacadeMethodExtension.php index 46f0b6d..6acaa58 100644 --- a/src/FacadeMethodExtension.php +++ b/src/FacadeMethodExtension.php @@ -62,7 +62,7 @@ public function __construct( /** * @inheritdoc */ - public function setBroker(Broker $broker) + public function setBroker(Broker $broker): void { $this->broker = $broker; } diff --git a/src/MacroMethodExtension.php b/src/MacroMethodExtension.php index 1c01162..f7877a9 100644 --- a/src/MacroMethodExtension.php +++ b/src/MacroMethodExtension.php @@ -50,7 +50,7 @@ public function __construct(PhpMethodReflectionFactory $methodReflectionFactory, /** * @inheritdoc */ - public function setBroker(Broker $broker) + public function setBroker(Broker $broker): void { $this->broker = $broker; }