diff --git a/README.md b/README.md index f6e7711..a4c8740 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,3 @@ -# phpstan-laravel -Laravel plugins for [PHPStan](https://github.com/phpstan/phpstan) +# This package is no longer maintained. -[![Build Status](https://img.shields.io/travis/Weebly/phpstan-laravel/master.svg?style=flat-square)](https://travis-ci.org/Weebly/phpstan-laravel) - -## Usage - -To use this extension, require it in [Composer](https://getcomposer.org/): - -``` -composer require --dev weebly/phpstan-laravel -``` - -And include extension.neon in your project's PHPStan config: - -``` -includes: - - vendor/weebly/phpstan-laravel/extension.neon -``` +Instead, we recommend using https://github.com/nunomaduro/larastan diff --git a/bootstrap.php b/bootstrap.php index 6d17458..04d9f90 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,5 +1,7 @@ make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); diff --git a/extension.neon b/extension.neon index 0c0f500..a8cb534 100644 --- a/extension.neon +++ b/extension.neon @@ -18,5 +18,7 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: Weebly\PHPStan\Laravel\Utils\AnnotationsHelper + parameters: bootstrap: %rootDir%/../../weebly/phpstan-laravel/bootstrap.php diff --git a/src/BuilderMethodExtension.php b/src/BuilderMethodExtension.php index da6ffef..8b137fa 100644 --- a/src/BuilderMethodExtension.php +++ b/src/BuilderMethodExtension.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\Reflection\MethodReflection; +use Weebly\PHPStan\Laravel\Utils\AnnotationsHelper; final class BuilderMethodExtension implements MethodsClassReflectionExtension, BrokerAwareExtension { @@ -28,14 +29,21 @@ final class BuilderMethodExtension implements MethodsClassReflectionExtension, B */ private $methodReflectionFactory; + /** + * @var AnnotationsHelper + */ + private $annotationsHelper; + /** * BuilderMethodExtension constructor. * * @param \Weebly\PHPStan\Laravel\MethodReflectionFactory $methodReflectionFactory + * @param AnnotationsHelper $annotationsHelper */ - public function __construct(MethodReflectionFactory $methodReflectionFactory) + public function __construct(MethodReflectionFactory $methodReflectionFactory, AnnotationsHelper $annotationsHelper) { $this->methodReflectionFactory = $methodReflectionFactory; + $this->annotationsHelper = $annotationsHelper; } /** @@ -51,7 +59,10 @@ public function setBroker(Broker $broker) */ public function hasMethod(ClassReflection $classReflection, string $methodName): bool { - if ($classReflection->isSubclassOf(Model::class) && !isset($this->methods[$classReflection->getName()])) { + if (!isset($this->methods[$classReflection->getName()]) && ( + $classReflection->isSubclassOf(Model::class) + || in_array(Builder::class, $this->annotationsHelper->getMixins($classReflection)) + )) { $builder = $this->broker->getClass(Builder::class); $this->methods[$classReflection->getName()] = $this->createWrappedMethods($classReflection, $builder); diff --git a/src/FacadeMethodExtension.php b/src/FacadeMethodExtension.php index 457c343..5397a9d 100644 --- a/src/FacadeMethodExtension.php +++ b/src/FacadeMethodExtension.php @@ -10,6 +10,8 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\Reflection\MethodReflection; +use Weebly\PHPStan\Laravel\Utils\AnnotationsHelper; +use PHPStan\Broker\ClassNotFoundException; final class FacadeMethodExtension implements MethodsClassReflectionExtension, BrokerAwareExtension { @@ -36,14 +38,21 @@ final class FacadeMethodExtension implements MethodsClassReflectionExtension, Br */ private $methodReflectionFactory; + /** + * @var AnnotationsHelper + */ + private $annotationsHelper; + /** * FacadeMethodExtension constructor. * * @param \Weebly\PHPStan\Laravel\MethodReflectionFactory $methodReflectionFactory + * @param AnnotationsHelper $annotationsHelper */ - public function __construct(MethodReflectionFactory $methodReflectionFactory) + public function __construct(MethodReflectionFactory $methodReflectionFactory, AnnotationsHelper $annotationsHelper) { $this->methodReflectionFactory = $methodReflectionFactory; + $this->annotationsHelper = $annotationsHelper; } /** @@ -69,6 +78,15 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): $instanceReflection = $this->broker->getClass(get_class($instance)); $this->methods[$classReflection->getName()] = $this->createMethods($classReflection, $instanceReflection); + foreach ($this->annotationsHelper->getMixins($instanceReflection) as $mixin) { + try { + $mixinInstanceReflection = $this->broker->getClass($mixin); + } catch (ClassNotFoundException $e) { + continue; + } + $this->methods[$classReflection->getName()] += $this->createMethods($classReflection, $mixinInstanceReflection); + } + if (isset($this->extensions[$instanceReflection->getName()])) { $extensionMethod = $this->extensions[$instanceReflection->getName()]; $extensionReflection = $this->broker->getClass(get_class($instance->$extensionMethod())); diff --git a/src/HelpersReturnTypeExtension.php b/src/HelpersReturnTypeExtension.php index d4cf2bc..81819f9 100644 --- a/src/HelpersReturnTypeExtension.php +++ b/src/HelpersReturnTypeExtension.php @@ -20,6 +20,8 @@ final class HelpersReturnTypeExtension implements DynamicFunctionReturnTypeExten */ private $helpers = [ 'app', + 'redirect', + 'response', 'validator', 'view', ]; @@ -54,7 +56,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } return new MixedType(); + 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(\Illuminate\Http\Response::class); case 'validator': if (empty($functionCall->args)) { return new ObjectType(\Illuminate\Contracts\Validation\Factory::class); diff --git a/src/MacroMethodExtension.php b/src/MacroMethodExtension.php index 1c01162..4ec45a6 100644 --- a/src/MacroMethodExtension.php +++ b/src/MacroMethodExtension.php @@ -61,11 +61,11 @@ public function setBroker(Broker $broker) public function hasMethod(ClassReflection $classReflection, string $methodName): bool { if ($classReflection->hasTraitUse(Macroable::class)) { - /** @var \Illuminate\Support\Traits\Macroable $macorable */ - $macorable = $classReflection->getName(); + /** @var \Illuminate\Support\Traits\Macroable $macroable */ + $macroable = $classReflection->getName(); - if ($macorable::hasMacro($methodName) && !isset($this->methods[$classReflection->getName()])) { - $refObject = new \ReflectionClass($macorable); + if ($macroable::hasMacro($methodName) && !isset($this->methods[$classReflection->getName()])) { + $refObject = new \ReflectionClass($macroable); $refProperty = $refObject->getProperty('macros'); $refProperty->setAccessible(true); diff --git a/src/Utils/AnnotationsHelper.php b/src/Utils/AnnotationsHelper.php new file mode 100644 index 0000000..bfa7b96 --- /dev/null +++ b/src/Utils/AnnotationsHelper.php @@ -0,0 +1,27 @@ +getNativeReflection()->getDocComment(), + $mixins + ); + + return array_map(function ($mixin) { + return preg_replace('#^\\\\#', '', $mixin); + }, $mixins[1]); + } +} diff --git a/tests/BuilderMethodExtensionTest.php b/tests/BuilderMethodExtensionTest.php new file mode 100644 index 0000000..7b04617 --- /dev/null +++ b/tests/BuilderMethodExtensionTest.php @@ -0,0 +1,133 @@ +assertFalse($this->hasMethod(stdClass::class, 'find')); + $this->assertTrue($this->hasMethod($this->childOfModelClassName, 'find')); + $this->assertFalse($this->hasMethod(stdClass::class, 'select')); + $this->assertTrue($this->hasMethod($this->childOfModelClassName, 'select')); + } catch (ClassNotFoundException $e) { + $this->markTestIncomplete($e->getMessage()); + } + } + + public function testHasMethodInClassWithMixinAnnotation() + { + try { + $this->assertFalse($this->hasMethod(stdClass::class, 'find')); + $this->assertTrue($this->hasMethod(stdClass::class, 'find', true)); + $this->assertFalse($this->hasMethod(stdClass::class, 'select')); + $this->assertTrue($this->hasMethod(stdClass::class, 'select', true)); + } catch (ClassNotFoundException $e) { + $this->markTestIncomplete($e->getMessage()); + } + } + + public function testHasMethodInBuilder() + { + try { + $this->assertFalse($this->hasMethod(stdClass::class, 'find')); + $this->assertTrue($this->hasMethod(Builder::class, 'find')); + $this->assertFalse($this->hasMethod(stdClass::class, 'select')); + $this->assertTrue($this->hasMethod(Builder::class, 'select')); + } catch (ClassNotFoundException $e) { + $this->markTestIncomplete($e->getMessage()); + } + } + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->broker = $this->createBroker(); + $this->childOfModelClassName = get_class(new class() extends Model {}); + } + + /** + * @return MethodReflectionFactory + */ + private function makeMethodReflectionFactoryMock() + { + /** @var MockObject|PhpMethodReflectionFactory $phpMethodReflectionFactory */ + $phpMethodReflectionFactory = $this + ->getMockBuilder(PhpMethodReflectionFactory::class) + ->getMockForAbstractClass(); + $methodReflectionMock = $this + ->getMockBuilder(PhpMethodReflection::class) + ->disableOriginalConstructor() + ->getMock(); + $phpMethodReflectionFactory->method('create')->willReturn($methodReflectionMock); + /** @var FileTypeMapper $fileTypeMapper */ + $fileTypeMapper = $this->getContainer()->createInstance(FileTypeMapper::class); + + return new MethodReflectionFactory($phpMethodReflectionFactory, $fileTypeMapper); + } + + /** + * Check existence of the method in given class + * + * @param string $className + * @param string $methodName + * @param bool $addBuilderMixin + * @return bool + * @throws ClassNotFoundException + */ + private function hasMethod(string $className, string $methodName, bool $addBuilderMixin = false): bool + { + $extension = new BuilderMethodExtension( + $this->makeMethodReflectionFactoryMock(), + $this->makeAnnotationsHelperMock($addBuilderMixin) + ); + $extension->setBroker($this->broker); + + return $extension->hasMethod($this->broker->getClass($className), $methodName); + } + + /** + * @param bool $withBuilder + * @return AnnotationsHelper|MockObject + */ + private function makeAnnotationsHelperMock(bool $withBuilder = false) + { + $annotationsHelper = $this + ->getMockBuilder(AnnotationsHelper::class) + ->getMock(); + $annotationsHelper->method('getMixins')->willReturn($withBuilder ? [Builder::class] : []); + + return $annotationsHelper; + } +} diff --git a/tests/FacadeMethodExtensionTest.php b/tests/FacadeMethodExtensionTest.php new file mode 100644 index 0000000..68ea3d4 --- /dev/null +++ b/tests/FacadeMethodExtensionTest.php @@ -0,0 +1,117 @@ +assertTrue($this->hasMethod(get_class($testFacade), 'someMethod')); + $this->assertFalse($this->hasMethod(get_class($testFacade), 'fakeMethod')); + // Method from accessor mixin + $this->assertTrue($this->hasMethod(get_class($testFacade), 'table')); + $this->assertTrue($this->hasMethod(get_class($testFacade), 'shouldUse')); + } catch (ClassNotFoundException $e) { + $this->markTestIncomplete($e->getMessage()); + } + } + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->broker = $this->createBroker(); + } + + /** + * @return MethodReflectionFactory + */ + private function makeMethodReflectionFactoryMock() + { + /** @var MockObject|PhpMethodReflectionFactory $phpMethodReflectionFactory */ + $phpMethodReflectionFactory = $this + ->getMockBuilder(PhpMethodReflectionFactory::class) + ->getMockForAbstractClass(); + $methodReflectionMock = $this + ->getMockBuilder(PhpMethodReflection::class) + ->disableOriginalConstructor() + ->getMock(); + $phpMethodReflectionFactory->method('create')->willReturn($methodReflectionMock); + /** @var FileTypeMapper $fileTypeMapper */ + $fileTypeMapper = $this->getContainer()->createInstance(FileTypeMapper::class); + + return new MethodReflectionFactory($phpMethodReflectionFactory, $fileTypeMapper); + } + + /** + * @return AnnotationsHelper|MockObject + */ + private function makeAnnotationsHelperMock() + { + $annotationsHelper = $this + ->getMockBuilder(AnnotationsHelper::class) + ->getMock(); + $annotationsHelper->method('getMixins')->willReturn([Connection::class, AuthManager::class, 'Fake']); + + return $annotationsHelper; + } + + /** + * Check existence of the method in given class + * + * @param string $className + * @param string $methodName + * @return bool + * @throws ClassNotFoundException + */ + private function hasMethod(string $className, string $methodName): bool + { + $extension = new FacadeMethodExtension( + $this->makeMethodReflectionFactoryMock(), + $this->makeAnnotationsHelperMock() + ); + $extension->setBroker($this->broker); + + return $extension->hasMethod($this->broker->getClass($className), $methodName); + } +} diff --git a/tests/Utils/AnnotationsHelperTest.php b/tests/Utils/AnnotationsHelperTest.php new file mode 100644 index 0000000..603a6ab --- /dev/null +++ b/tests/Utils/AnnotationsHelperTest.php @@ -0,0 +1,56 @@ +makeClassReflectionMock(<<assertEquals( + [TestCase::class, ClassReflection::class], + $annotationHelper->getMixins($reflection) + ); + $this->assertEquals( + [], + $annotationHelper->getMixins($this->makeClassReflectionMock('')) + ); + } + + /** + * @param string $docBlock + * @return ClassReflection|MockObject + */ + private function makeClassReflectionMock(string $docBlock) + { + $reflectionClass = $this + ->getMockBuilder(ReflectionClass::class) + ->disableOriginalConstructor() + ->getMock(); + $reflectionClass->method('getDocComment')->willReturn($docBlock); + + $classReflection = $this + ->getMockBuilder(ClassReflection::class) + ->disableOriginalConstructor() + ->getMock(); + $classReflection->method('getNativeReflection')->willReturn($reflectionClass); + + return $classReflection; + } +}