From da1efdb8441be6d490ebd89c24cbd798a41cfb41 Mon Sep 17 00:00:00 2001 From: Nathanael Esayeas Date: Thu, 21 Mar 2024 12:44:12 -0500 Subject: [PATCH] Fix reserved words used to name a class, interface or trait (#1406) Signed-off-by: Nathanael Esayeas --- library/Mockery.php | 28 ++++++--- library/Mockery/Reflector.php | 15 +++++ psalm-baseline.xml | 3 + tests/Mockery/ReflectorTest.php | 27 +++++++++ tests/Unit/Regression/Issue1404Test.php | 81 +++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 tests/Unit/Regression/Issue1404Test.php diff --git a/library/Mockery.php b/library/Mockery.php index 080d4d959..9d3fac668 100644 --- a/library/Mockery.php +++ b/library/Mockery.php @@ -759,7 +759,7 @@ protected static function buildDemeterChain(LegacyMockInterface $mock, $arg, $ad $mock = self::getNewDemeterMock($container, $parent, $method, $expectations); } else { $demeterMockKey = $container->getKeyOfDemeterMockFor($method, $parent); - if ($demeterMockKey) { + if ($demeterMockKey !== null) { $mock = self::getExistingDemeterMock($container, $demeterMockKey); } } @@ -987,13 +987,27 @@ private static function getNewDemeterMock(Container $container, $parent, $method $parRefMethod = $parRef->getMethod($method); $parRefMethodRetType = Reflector::getReturnType($parRefMethod, true); - if ($parRefMethodRetType !== null && $parRefMethodRetType !== 'mixed') { - $nameBuilder = new MockNameBuilder(); - $nameBuilder->addPart('\\' . $newMockName); - $mock = self::namedMock($nameBuilder->build(), $parRefMethodRetType); - $exp->andReturn($mock); + if ($parRefMethodRetType !== null) { + $returnTypes = \explode('|', $parRefMethodRetType); - return $mock; + $filteredReturnTypes = array_filter($returnTypes, static function (string $type): bool { + return ! Reflector::isReservedWord($type); + }); + + if ($filteredReturnTypes !== []) { + $nameBuilder = new MockNameBuilder(); + + $nameBuilder->addPart('\\' . $newMockName); + + $mock = self::namedMock( + $nameBuilder->build(), + ...$filteredReturnTypes + ); + + $exp->andReturn($mock); + + return $mock; + } } } diff --git a/library/Mockery/Reflector.php b/library/Mockery/Reflector.php index 537127b03..8e4fc1582 100644 --- a/library/Mockery/Reflector.php +++ b/library/Mockery/Reflector.php @@ -44,6 +44,13 @@ class Reflector */ public const BUILTIN_TYPES = ['array', 'bool', 'int', 'float', 'null', 'object', 'string']; + /** + * List of reserved words. + * + * @var list + */ + public const RESERVED_WORDS = ['bool', 'true', 'false', 'float', 'int', 'iterable', 'mixed', 'never', 'null', 'object', 'string', 'void']; + /** * Iterable. * @@ -148,6 +155,14 @@ public static function isArray(ReflectionParameter $param) return $type instanceof ReflectionNamedType && $type->getName(); } + /** + * Determine if the given type is a reserved word. + */ + public static function isReservedWord(string $type): bool + { + return in_array(strtolower($type), self::RESERVED_WORDS, true); + } + /** * Format the given type as a nullable type. */ diff --git a/psalm-baseline.xml b/psalm-baseline.xml index f2feecb15..11fe6102d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -16,6 +16,9 @@ self::$_generator === null self::$_loader === null + + Reflector::isReservedWord($type) + $argument getMocks()[$demeterMockKey] ?? null]]> diff --git a/tests/Mockery/ReflectorTest.php b/tests/Mockery/ReflectorTest.php index 74a2606f4..0e4dca4a8 100644 --- a/tests/Mockery/ReflectorTest.php +++ b/tests/Mockery/ReflectorTest.php @@ -40,6 +40,33 @@ public static function typeHintDataProvider(): Generator ]; } + /** + * @dataProvider provideReservedWords + */ + public function testIsReservedWord(string $type): void + { + self::assertTrue(Reflector::isReservedWord($type)); + } + + public static function provideReservedWords(): Generator + { + foreach ([ + 'bool', + 'false', + 'float', + 'int', + 'iterable', + 'mixed', + 'never', + 'null', + 'object', + 'string', + 'true', + 'void' + ] as $type) { + yield $type => [$type]; + } + } } class ParentClass diff --git a/tests/Unit/Regression/Issue1404Test.php b/tests/Unit/Regression/Issue1404Test.php new file mode 100644 index 000000000..81267a309 --- /dev/null +++ b/tests/Unit/Regression/Issue1404Test.php @@ -0,0 +1,81 @@ +> + */ + public static function provideResult(): Generator + { + yield from [ + 'empty' => [[]], + 'non-empty' => [['Black', 'Lives', 'Matter']], + ]; + } + + /** + * @dataProvider provideResult + */ + public function testDemeterChainsAllows(array $result): void + { + $dbConnection = Mockery::mock(PDO::class); + + $dbConnection->allows('query->fetchAll')->andReturn($result); + + self::assertSame($result, $dbConnection->query('sql')->fetchAll()); + } + /** + * @dataProvider provideResult + */ + public function testDemeterChainsExpects(array $result): void + { + $dbConnection = Mockery::mock(PDO::class); + + $dbConnection->expects('query->fetchAll')->andReturn($result); + + self::assertSame($result, $dbConnection->query('sql')->fetchAll()); + } + + /** + * @dataProvider provideResult + */ + public function testDemeterChainsAlternativeSyntax(array $result): void + { + $dbConnection = Mockery::mock(PDO::class); + + $dbConnection->shouldReceive('query->fetchAll')->andReturn($result); + + self::assertSame($result, $dbConnection->query('sql')->fetchAll()); + } + + /** + * @dataProvider provideResult + */ + public function testNonDemeterChainsSyntax(array $result): void + { + $dbStatement = Mockery::mock(PDOStatement::class); + $dbStatement->expects('fetchAll') + ->andReturn($result); + + $dbConnection = Mockery::mock(PDO::class); + $dbConnection->expects('query') + ->with('sql') + ->andReturn($dbStatement); + + self::assertSame($result, $dbConnection->query('sql')->fetchAll()); + } +}