Skip to content

Commit

Permalink
Fix reserved words used to name a class, interface or trait (#1406)
Browse files Browse the repository at this point in the history
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
  • Loading branch information
ghostwriter committed Mar 21, 2024
1 parent abe61d7 commit da1efdb
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 7 deletions.
28 changes: 21 additions & 7 deletions library/Mockery.php
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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;
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions library/Mockery/Reflector.php
Expand Up @@ -44,6 +44,13 @@ class Reflector
*/
public const BUILTIN_TYPES = ['array', 'bool', 'int', 'float', 'null', 'object', 'string'];

/**
* List of reserved words.
*
* @var list<string>
*/
public const RESERVED_WORDS = ['bool', 'true', 'false', 'float', 'int', 'iterable', 'mixed', 'never', 'null', 'object', 'string', 'void'];

/**
* Iterable.
*
Expand Down Expand Up @@ -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.
*/
Expand Down
3 changes: 3 additions & 0 deletions psalm-baseline.xml
Expand Up @@ -16,6 +16,9 @@
<code>self::$_generator === null</code>
<code>self::$_loader === null</code>
</DocblockTypeContradiction>
<InternalMethod>
<code>Reflector::isReservedWord($type)</code>
</InternalMethod>
<InvalidReturnStatement>
<code>$argument</code>
<code><![CDATA[$container->getMocks()[$demeterMockKey] ?? null]]></code>
Expand Down
27 changes: 27 additions & 0 deletions tests/Mockery/ReflectorTest.php
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions tests/Unit/Regression/Issue1404Test.php
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Mockery\Tests\Unit\Regression;

use Generator;
use Mockery;
use PDO;
use PDOStatement;
use PHPUnit\Framework\TestCase;

/**
* @coversDefaultClass \Mockery
* @uses \Mockery\Reflector
*/
final class Issue1404Test extends TestCase
{
/**
* @return Generator<string,list<string>>
*/
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());
}
}

0 comments on commit da1efdb

Please sign in to comment.