mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-17 18:03:07 +00:00
169 lines
6.5 KiB
PHP
169 lines
6.5 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of Twig.
|
|
*
|
|
* (c) Fabien Potencier
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Twig\Tests\Util;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Twig\Util\CallableParameters;
|
|
|
|
class CallableParametersTest extends TestCase
|
|
{
|
|
/**
|
|
* @dataProvider provideTypes
|
|
*/
|
|
public function testIsStringCoercionSafe(?\ReflectionType $type, bool $expected, ?\ReflectionClass $scope = null): void
|
|
{
|
|
$this->assertSame($expected, CallableParameters::isStringCoercionSafe($type, $scope));
|
|
}
|
|
|
|
public function testIsStringCoercionSafeDoesNotAutoloadUnknownClasses(): void
|
|
{
|
|
$autoloaded = false;
|
|
$autoload = static function () use (&$autoloaded): void {
|
|
$autoloaded = true;
|
|
};
|
|
spl_autoload_register($autoload);
|
|
|
|
try {
|
|
$type = (new \ReflectionFunction(eval('return static fn (CallableParametersAutoloadProbe $x) => null;')))->getParameters()[0]->getType();
|
|
|
|
$this->assertFalse(CallableParameters::isStringCoercionSafe($type));
|
|
$this->assertFalse($autoloaded);
|
|
} finally {
|
|
spl_autoload_unregister($autoload);
|
|
}
|
|
}
|
|
|
|
public static function provideTypes(): iterable
|
|
{
|
|
$param = static fn (\Closure $c): ?\ReflectionType => (new \ReflectionFunction($c))->getParameters()[0]->getType();
|
|
$mParam = static fn (string $class, string $m): ?\ReflectionType => (new \ReflectionMethod($class, $m))->getParameters()[0]->getType();
|
|
$mReturn = static fn (string $class, string $m): ?\ReflectionType => (new \ReflectionMethod($class, $m))->getReturnType();
|
|
|
|
// No type information: must keep the check.
|
|
yield 'untyped' => [$param(static fn ($x) => null), false];
|
|
|
|
// Builtin scalars that cannot hold a string-coercible value.
|
|
yield 'int' => [$param(static fn (int $x) => null), true];
|
|
yield 'float' => [$param(static fn (float $x) => null), true];
|
|
yield 'bool' => [$param(static fn (bool $x) => null), true];
|
|
yield 'never (return)' => [(new \ReflectionFunction(static function (): never { throw new \LogicException(); }))->getReturnType(), true];
|
|
|
|
// Builtins that can carry or coerce to a string.
|
|
yield 'string' => [$param(static fn (string $x) => null), false];
|
|
yield 'array' => [$param(static fn (array $x) => null), false];
|
|
yield 'iterable' => [$param(static fn (iterable $x) => null), false];
|
|
yield 'object' => [$param(static fn (object $x) => null), false];
|
|
yield 'mixed' => [$param(static fn (mixed $x) => null), false];
|
|
yield 'callable' => [$param(static fn (callable $x) => null), false];
|
|
|
|
// Class types: safe only when final and neither Stringable nor
|
|
// Traversable. Interfaces and non-final classes are open (a subtype
|
|
// could add Stringable/Traversable), so they must stay unsafe.
|
|
yield 'final plain class' => [$param(static fn (FinalPlainObject $x) => null), true];
|
|
yield 'nullable final class' => [$param(static fn (?FinalPlainObject $x) => null), true];
|
|
yield 'non-final class' => [$param(static fn (\stdClass $x) => null), false];
|
|
yield 'final Stringable class' => [$param(static fn (FinalStringableObject $x) => null), false];
|
|
yield 'Countable interface' => [$param(static fn (\Countable $x) => null), false];
|
|
yield 'Stringable interface' => [$param(static fn (\Stringable $x) => null), false];
|
|
yield 'Traversable interface' => [$param(static fn (\Iterator $x) => null), false];
|
|
yield 'Traversable class' => [$param(static fn (\ArrayIterator $x) => null), false];
|
|
|
|
// Enums are implicitly final and cannot be Stringable (no __toString).
|
|
yield 'backed enum' => [$param(static fn (SampleBackedEnum $x) => null), true];
|
|
yield 'pure enum' => [$param(static fn (SamplePureEnum $x) => null), true];
|
|
|
|
// `self`/`parent`/`static` resolve to the declaring class (passed as
|
|
// scope) and are then judged by the class rules. PHP < 8.4 reports
|
|
// them verbatim, recent versions resolve them in getName(); both paths
|
|
// must yield the same verdict.
|
|
yield 'self -> Stringable class' => [$mParam(CoercionFixture::class, 'selfParam'), false, new \ReflectionClass(CoercionFixture::class)];
|
|
yield 'self -> final class' => [$mParam(FinalSelfFixture::class, 'selfParam'), true, new \ReflectionClass(FinalSelfFixture::class)];
|
|
yield 'parent -> non-final class' => [$mParam(CoercionFixture::class, 'parentParam'), false, new \ReflectionClass(CoercionFixture::class)];
|
|
yield 'static (return) -> self' => [$mReturn(CoercionFixture::class, 'staticReturn'), false, new \ReflectionClass(CoercionFixture::class)];
|
|
|
|
// Union: the value is one of the members, so all must be safe.
|
|
yield 'int|float union' => [$param(static fn (int|float $x) => null), true];
|
|
yield 'int|null union' => [$param(static fn (?int $x) => null), true];
|
|
yield 'int|float|null union' => [$param(static fn (int|float|null $x) => null), true];
|
|
yield 'int|string union' => [$param(static fn (int|string $x) => null), false];
|
|
|
|
// Intersection: safe as soon as one member is a final, non-coercible
|
|
// class (it pins the concrete class); otherwise open and unsafe.
|
|
yield 'final class intersection' => [$param(static fn (FinalMarkedObject&SampleMarker $x) => null), true];
|
|
yield 'interface intersection' => [$param(static fn (\Countable&\ArrayAccess $x) => null), false];
|
|
yield 'Stringable intersection' => [$param(static fn (\Stringable&\Countable $x) => null), false];
|
|
yield 'Traversable intersection' => [$param(static fn (\Traversable&\Countable $x) => null), false];
|
|
}
|
|
}
|
|
|
|
enum SampleBackedEnum: string
|
|
{
|
|
case A = 'a';
|
|
}
|
|
|
|
enum SamplePureEnum
|
|
{
|
|
case A;
|
|
}
|
|
|
|
interface SampleMarker
|
|
{
|
|
}
|
|
|
|
final class FinalMarkedObject implements SampleMarker
|
|
{
|
|
}
|
|
|
|
final class FinalPlainObject
|
|
{
|
|
}
|
|
|
|
final class FinalStringableObject implements \Stringable
|
|
{
|
|
public function __toString(): string
|
|
{
|
|
return '';
|
|
}
|
|
}
|
|
|
|
final class FinalSelfFixture
|
|
{
|
|
public function selfParam(self $x): void
|
|
{
|
|
}
|
|
}
|
|
|
|
class CoercionFixtureParent
|
|
{
|
|
}
|
|
|
|
class CoercionFixture extends CoercionFixtureParent implements \Stringable
|
|
{
|
|
public function selfParam(self $x): void
|
|
{
|
|
}
|
|
|
|
public function parentParam(parent $x): void
|
|
{
|
|
}
|
|
|
|
public function staticReturn(): static
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
public function __toString(): string
|
|
{
|
|
return '';
|
|
}
|
|
}
|