From 64a4f535c0b17c8c68287c88c9f7d0d2e17ac91a Mon Sep 17 00:00:00 2001 From: Andrey Kotelnik Date: Thu, 15 Feb 2018 10:45:57 +0200 Subject: [PATCH 1/5] Added support of @mixin annotation to the BuilderMethodExtension --- src/BuilderMethodExtension.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/BuilderMethodExtension.php b/src/BuilderMethodExtension.php index da6ffef..eade1ec 100644 --- a/src/BuilderMethodExtension.php +++ b/src/BuilderMethodExtension.php @@ -51,7 +51,13 @@ 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) + || preg_match( + '/@mixin\s+' . preg_quote('\\' . Builder::class) . '/', + (string) $classReflection->getNativeReflection()->getDocComment() + ) + )) { $builder = $this->broker->getClass(Builder::class); $this->methods[$classReflection->getName()] = $this->createWrappedMethods($classReflection, $builder); From 6d0e01d7b1f81765a9a36dccabef2cfce45a3fd2 Mon Sep 17 00:00:00 2001 From: Andrey Kotelnik Date: Thu, 15 Feb 2018 10:48:33 +0200 Subject: [PATCH 2/5] Added tests for BuilderMethodExtension --- tests/BuilderMethodExtensionTest.php | 104 +++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/BuilderMethodExtensionTest.php diff --git a/tests/BuilderMethodExtensionTest.php b/tests/BuilderMethodExtensionTest.php new file mode 100644 index 0000000..09e8d9b --- /dev/null +++ b/tests/BuilderMethodExtensionTest.php @@ -0,0 +1,104 @@ +assertFalse($this->hasMethod(stdClass::class, 'find')); + $this->assertTrue($this->hasMethod(ChildOfModel::class, 'find')); + $this->assertFalse($this->hasMethod(stdClass::class, 'select')); + $this->assertTrue($this->hasMethod(ChildOfModel::class, 'select')); + } + + public function testHasMethodInClassWithMixinAnnotation() + { + $this->assertFalse($this->hasMethod(stdClass::class, 'find')); + $this->assertTrue($this->hasMethod(HasAMixinAnnotation::class, 'find')); + $this->assertFalse($this->hasMethod(stdClass::class, 'select')); + $this->assertTrue($this->hasMethod(HasAMixinAnnotation::class, 'select')); + } + + public function testHasMethodInBuilder() + { + $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')); + } + + /** + * @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); + } + + /** + * Check existence of the method in given class + * + * @param string $className + * @param string $methodName + * @return bool + */ + private function hasMethod(string $className, string $methodName): bool + { + $extension = new BuilderMethodExtension($this->makeMethodReflectionFactoryMock()); + $extension->setBroker($this->broker); + + return $extension->hasMethod($this->broker->getClass($className), $methodName); + } +} + +class ChildOfModel extends Model {} + +/** + * Some description + * + * @mixin \Illuminate\Database\Eloquent\Builder + * @method string getString() + */ +class HasAMixinAnnotation {} From 52b0925f34c6df89ab4e6a5227e9b469c608b9e7 Mon Sep 17 00:00:00 2001 From: Andrey Kotelnik Date: Tue, 20 Feb 2018 16:27:33 +0200 Subject: [PATCH 3/5] Added support of @mixin annotation to the FacadeMethodExtension --- src/FacadeMethodExtension.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/FacadeMethodExtension.php b/src/FacadeMethodExtension.php index 457c343..252823f 100644 --- a/src/FacadeMethodExtension.php +++ b/src/FacadeMethodExtension.php @@ -69,6 +69,17 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): $instanceReflection = $this->broker->getClass(get_class($instance)); $this->methods[$classReflection->getName()] = $this->createMethods($classReflection, $instanceReflection); + if (preg_match_all( + '/@mixin\s+([\w\\\\]+)/', + (string) $instanceReflection->getNativeReflection()->getDocComment(), + $mixins + )) { + foreach ($mixins[1] as $mixin) { + $mixinInstanceReflection = $this->broker->getClass($mixin); + $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())); From 8a051fbcf6ee7bf76a0ffc0142bc597ae3afbea6 Mon Sep 17 00:00:00 2001 From: Andrey Kotelnik Date: Tue, 20 Feb 2018 16:28:33 +0200 Subject: [PATCH 4/5] Added tests for FacadeMethodExtension --- tests/FacadeMethodExtensionTest.php | 98 +++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/FacadeMethodExtensionTest.php diff --git a/tests/FacadeMethodExtensionTest.php b/tests/FacadeMethodExtensionTest.php new file mode 100644 index 0000000..26200b0 --- /dev/null +++ b/tests/FacadeMethodExtensionTest.php @@ -0,0 +1,98 @@ +assertTrue($this->hasMethod(TestFacade::class, 'someMethod')); + $this->assertFalse($this->hasMethod(TestFacade::class, 'fakeMethod')); + // Method from accessor mixin + $this->assertTrue($this->hasMethod(TestFacade::class, 'table')); + $this->assertTrue($this->hasMethod(TestFacade::class, 'shouldUse')); + } + + /** + * @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); + } + + /** + * Check existence of the method in given class + * + * @param string $className + * @param string $methodName + * @return bool + */ + private function hasMethod(string $className, string $methodName): bool + { + $extension = new FacadeMethodExtension($this->makeMethodReflectionFactoryMock()); + $extension->setBroker($this->broker); + + return $extension->hasMethod($this->broker->getClass($className), $methodName); + } +} + +/** + * @mixin \Illuminate\Database\Connection + * @mixin \Illuminate\Auth\AuthManager + */ +class TestFacadeAccessor { + function someMethod() { + return true; + } +} + +class TestFacade extends Facade { + /** + * @inheritdoc + */ + protected static function getFacadeAccessor() + { + return new TestFacadeAccessor(); + } +} From 5f518798bc4a70c722a5d2c9327c4e84c6ed5911 Mon Sep 17 00:00:00 2001 From: Andrey Kotelnik Date: Wed, 11 Apr 2018 23:09:51 +0300 Subject: [PATCH 5/5] Added annotations helper --- src/BuilderMethodExtension.php | 15 ++++-- src/FacadeMethodExtension.php | 23 +++++--- src/Utils/AnnotationsHelper.php | 27 ++++++++++ tests/BuilderMethodExtensionTest.php | 75 +++++++++++++++++++-------- tests/FacadeMethodExtensionTest.php | 73 ++++++++++++++++---------- tests/Utils/AnnotationsHelperTest.php | 56 ++++++++++++++++++++ 6 files changed, 206 insertions(+), 63 deletions(-) create mode 100644 src/Utils/AnnotationsHelper.php create mode 100644 tests/Utils/AnnotationsHelperTest.php diff --git a/src/BuilderMethodExtension.php b/src/BuilderMethodExtension.php index eade1ec..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; } /** @@ -53,10 +61,7 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): { if (!isset($this->methods[$classReflection->getName()]) && ( $classReflection->isSubclassOf(Model::class) - || preg_match( - '/@mixin\s+' . preg_quote('\\' . Builder::class) . '/', - (string) $classReflection->getNativeReflection()->getDocComment() - ) + || 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 252823f..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,15 +78,13 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): $instanceReflection = $this->broker->getClass(get_class($instance)); $this->methods[$classReflection->getName()] = $this->createMethods($classReflection, $instanceReflection); - if (preg_match_all( - '/@mixin\s+([\w\\\\]+)/', - (string) $instanceReflection->getNativeReflection()->getDocComment(), - $mixins - )) { - foreach ($mixins[1] as $mixin) { + foreach ($this->annotationsHelper->getMixins($instanceReflection) as $mixin) { + try { $mixinInstanceReflection = $this->broker->getClass($mixin); - $this->methods[$classReflection->getName()] += $this->createMethods($classReflection, $mixinInstanceReflection); + } catch (ClassNotFoundException $e) { + continue; } + $this->methods[$classReflection->getName()] += $this->createMethods($classReflection, $mixinInstanceReflection); } if (isset($this->extensions[$instanceReflection->getName()])) { 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 index 09e8d9b..7b04617 100644 --- a/tests/BuilderMethodExtensionTest.php +++ b/tests/BuilderMethodExtensionTest.php @@ -13,6 +13,8 @@ use Weebly\PHPStan\Laravel\BuilderMethodExtension; use stdClass; use Illuminate\Database\Eloquent\Builder; +use Weebly\PHPStan\Laravel\Utils\AnnotationsHelper; +use PHPStan\Broker\ClassNotFoundException; /** * @package Tests\Weebly\PHPStan\Laravel @@ -24,28 +26,45 @@ class BuilderMethodExtensionTest extends TestCase */ private $broker; + /** + * @var string + */ + private $childOfModelClassName; + public function testHasMethodInSubclassOfModel() { - $this->assertFalse($this->hasMethod(stdClass::class, 'find')); - $this->assertTrue($this->hasMethod(ChildOfModel::class, 'find')); - $this->assertFalse($this->hasMethod(stdClass::class, 'select')); - $this->assertTrue($this->hasMethod(ChildOfModel::class, 'select')); + try { + $this->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() { - $this->assertFalse($this->hasMethod(stdClass::class, 'find')); - $this->assertTrue($this->hasMethod(HasAMixinAnnotation::class, 'find')); - $this->assertFalse($this->hasMethod(stdClass::class, 'select')); - $this->assertTrue($this->hasMethod(HasAMixinAnnotation::class, 'select')); + 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() { - $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')); + 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()); + } } /** @@ -55,6 +74,7 @@ protected function setUp() { parent::setUp(); $this->broker = $this->createBroker(); + $this->childOfModelClassName = get_class(new class() extends Model {}); } /** @@ -82,23 +102,32 @@ private function makeMethodReflectionFactoryMock() * * @param string $className * @param string $methodName + * @param bool $addBuilderMixin * @return bool + * @throws ClassNotFoundException */ - private function hasMethod(string $className, string $methodName): bool + private function hasMethod(string $className, string $methodName, bool $addBuilderMixin = false): bool { - $extension = new BuilderMethodExtension($this->makeMethodReflectionFactoryMock()); + $extension = new BuilderMethodExtension( + $this->makeMethodReflectionFactoryMock(), + $this->makeAnnotationsHelperMock($addBuilderMixin) + ); $extension->setBroker($this->broker); return $extension->hasMethod($this->broker->getClass($className), $methodName); } -} -class ChildOfModel extends Model {} + /** + * @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] : []); -/** - * Some description - * - * @mixin \Illuminate\Database\Eloquent\Builder - * @method string getString() - */ -class HasAMixinAnnotation {} + return $annotationsHelper; + } +} diff --git a/tests/FacadeMethodExtensionTest.php b/tests/FacadeMethodExtensionTest.php index 26200b0..68ea3d4 100644 --- a/tests/FacadeMethodExtensionTest.php +++ b/tests/FacadeMethodExtensionTest.php @@ -10,6 +10,10 @@ use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Type\FileTypeMapper; use Weebly\PHPStan\Laravel\FacadeMethodExtension; +use Weebly\PHPStan\Laravel\Utils\AnnotationsHelper; +use PHPStan\Broker\ClassNotFoundException; +use Illuminate\Database\Connection; +use Illuminate\Auth\AuthManager; use Illuminate\Support\Facades\Facade; /** @@ -24,12 +28,30 @@ class FacadeMethodExtensionTest extends TestCase public function testHasMethod() { - // Native accessor method - $this->assertTrue($this->hasMethod(TestFacade::class, 'someMethod')); - $this->assertFalse($this->hasMethod(TestFacade::class, 'fakeMethod')); - // Method from accessor mixin - $this->assertTrue($this->hasMethod(TestFacade::class, 'table')); - $this->assertTrue($this->hasMethod(TestFacade::class, 'shouldUse')); + $testFacade = new class() extends Facade{ + /** + * @inheritdoc + */ + protected static function getFacadeAccessor() + { + return new class() { + public function someMethod() { + return true; + } + }; + } + }; + + try { + // Native accessor method + $this->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()); + } } /** @@ -61,38 +83,35 @@ private function makeMethodReflectionFactoryMock() 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()); + $extension = new FacadeMethodExtension( + $this->makeMethodReflectionFactoryMock(), + $this->makeAnnotationsHelperMock() + ); $extension->setBroker($this->broker); return $extension->hasMethod($this->broker->getClass($className), $methodName); } } - -/** - * @mixin \Illuminate\Database\Connection - * @mixin \Illuminate\Auth\AuthManager - */ -class TestFacadeAccessor { - function someMethod() { - return true; - } -} - -class TestFacade extends Facade { - /** - * @inheritdoc - */ - protected static function getFacadeAccessor() - { - return new TestFacadeAccessor(); - } -} 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; + } +}