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/bootstrap.php b/bootstrap.php index 46653c9..7fdf814 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,5 +1,20 @@ -make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +foreach ($paths as $path) { + if (file_exists($path)) { + $app = require $path; + } +} + +if (!isset($app)) { + throw new Exception('Could not find app boostrap, tried: ' . implode(', ', $paths)); +} + +if ($app instanceof Illuminate\Foundation\Application) { + $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +} diff --git a/composer.json b/composer.json index 64520f9..25e9ca5 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,8 @@ } ], "require": { - "laravel/framework": "5.5.*", - "phpstan/phpstan": "^0.9" + "php": "7.*", + "phpstan/phpstan": "^0.10" }, "require-dev": { "phpunit/phpunit": "^6.5.2" 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 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 457c343..6acaa58 100644 --- a/src/FacadeMethodExtension.php +++ b/src/FacadeMethodExtension.php @@ -1,11 +1,16 @@ -methodReflectionFactory = $methodReflectionFactory; + $this->phpDocStringResolver = $phpDocStringResolver; } /** * @inheritdoc */ - public function setBroker(Broker $broker) + public function setBroker(Broker $broker): void { $this->broker = $broker; } @@ -61,18 +74,23 @@ 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 +111,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 +164,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(); + } } diff --git a/src/HelpersReturnTypeExtension.php b/src/HelpersReturnTypeExtension.php index d4cf2bc..2d5a84a 100644 --- a/src/HelpersReturnTypeExtension.php +++ b/src/HelpersReturnTypeExtension.php @@ -22,6 +22,8 @@ final class HelpersReturnTypeExtension implements DynamicFunctionReturnTypeExten 'app', 'validator', 'view', + 'redirect', + 'response', ]; /** @@ -68,6 +70,18 @@ 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); + + 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/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; } diff --git a/tests/HelpersReturnTypeExtensionTest.php b/tests/HelpersReturnTypeExtensionTest.php new file mode 100644 index 0000000..7a461ac --- /dev/null +++ b/tests/HelpersReturnTypeExtensionTest.php @@ -0,0 +1,80 @@ +extension = new HelpersReturnTypeExtension(); + } + + public function redirectDataProvider() + { + return [ + 'No arguments' => [[], Redirector::class], + '1 arguments' => [['/'], RedirectResponse::class], + '2 arguments' => [['/', 301], 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], + ]; + } + + /** @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()); + } + + 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()); + } +}