From 2540741171edce32ace1d59c9410a3bdd1e8e041 Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Thu, 4 Aug 2022 18:03:34 +0200 Subject: [PATCH] fix: handle classes in a case-sensitive way in type parser --- src/Type/Parser/Lexer/AliasLexer.php | 10 ++++------ src/Type/Parser/Lexer/NativeLexer.php | 6 +++--- src/Type/Types/ClassStringType.php | 5 ++--- src/Utility/Reflection/Reflection.php | 16 +++++++++++++++- .../Type/Parser/Lexer/NativeLexerTest.php | 5 +++++ .../Object/ShapedArrayValuesMappingTest.php | 12 +++++++++++- 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/Type/Parser/Lexer/AliasLexer.php b/src/Type/Parser/Lexer/AliasLexer.php index 4957d16..685cf0f 100644 --- a/src/Type/Parser/Lexer/AliasLexer.php +++ b/src/Type/Parser/Lexer/AliasLexer.php @@ -6,14 +6,12 @@ namespace CuyZ\Valinor\Type\Parser\Lexer; use CuyZ\Valinor\Type\Parser\Lexer\Token\Token; use CuyZ\Valinor\Utility\Reflection\ClassAliasParser; +use CuyZ\Valinor\Utility\Reflection\Reflection; use ReflectionClass; - use ReflectionFunction; - use Reflector; -use function class_exists; -use function interface_exists; +use function strtolower; /** @internal */ final class AliasLexer implements TypeLexer @@ -43,7 +41,7 @@ final class AliasLexer implements TypeLexer { $alias = ClassAliasParser::get()->resolveAlias($symbol, $this->reflection); - if ($alias !== $symbol) { + if (strtolower($alias) !== strtolower($symbol)) { return $alias; } @@ -77,7 +75,7 @@ final class AliasLexer implements TypeLexer $full = $namespace . '\\' . $symbol; - if (class_exists($full) || interface_exists($full)) { + if (Reflection::classOrInterfaceExists($full)) { return $full; } diff --git a/src/Type/Parser/Lexer/NativeLexer.php b/src/Type/Parser/Lexer/NativeLexer.php index f7baaec..ad5d2c1 100644 --- a/src/Type/Parser/Lexer/NativeLexer.php +++ b/src/Type/Parser/Lexer/NativeLexer.php @@ -29,11 +29,10 @@ use CuyZ\Valinor\Type\Parser\Lexer\Token\Token; use CuyZ\Valinor\Type\Parser\Lexer\Token\UnionToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\UnknownSymbolToken; use CuyZ\Valinor\Utility\Polyfill; +use CuyZ\Valinor\Utility\Reflection\Reflection; use UnitEnum; -use function class_exists; use function filter_var; -use function interface_exists; use function is_numeric; use function strtolower; use function substr; @@ -109,7 +108,8 @@ final class NativeLexer implements TypeLexer return new EnumNameToken($symbol); } - if (class_exists($symbol) || interface_exists($symbol)) { + if (Reflection::classOrInterfaceExists($symbol)) { + /** @var class-string $symbol */ return new ClassNameToken($symbol); } diff --git a/src/Type/Types/ClassStringType.php b/src/Type/Types/ClassStringType.php index 9923671..9b4db37 100644 --- a/src/Type/Types/ClassStringType.php +++ b/src/Type/Types/ClassStringType.php @@ -11,9 +11,8 @@ use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\Exception\CannotCastValue; use CuyZ\Valinor\Type\Types\Exception\InvalidClassString; use CuyZ\Valinor\Type\Types\Exception\InvalidUnionOfClassString; +use CuyZ\Valinor\Utility\Reflection\Reflection; -use function class_exists; -use function interface_exists; use function is_object; use function is_string; use function method_exists; @@ -51,7 +50,7 @@ final class ClassStringType implements StringType, CompositeType return false; } - if (! class_exists($value) && ! interface_exists($value)) { + if (! Reflection::classOrInterfaceExists($value)) { return false; } diff --git a/src/Utility/Reflection/Reflection.php b/src/Utility/Reflection/Reflection.php index 0819eaf..102838c 100644 --- a/src/Utility/Reflection/Reflection.php +++ b/src/Utility/Reflection/Reflection.php @@ -18,8 +18,10 @@ use ReflectionUnionType; use Reflector; use RuntimeException; +use function class_exists; use function get_class; use function implode; +use function interface_exists; use function preg_match_all; use function preg_replace; use function spl_object_hash; @@ -39,6 +41,18 @@ final class Reflection /** @var array */ private static array $functionReflection = []; + /** + * Case-sensitive implementation of `class_exists` and `interface_exists`. + */ + public static function classOrInterfaceExists(string $name): bool + { + if (! class_exists($name) && ! interface_exists($name)) { + return false; + } + + return self::class($name)->name === ltrim($name, '\\'); + } + /** * @param class-string $className * @return ReflectionClass @@ -132,7 +146,7 @@ final class Reflection if (PHP_VERSION_ID >= 8_00_00 && $reflection->isPromoted()) { $type = self::parseDocBlock( - // @phpstan-ignore-next-line / parameter is promoted so class exists for sure + // @phpstan-ignore-next-line / parameter is promoted so class exists for sure self::sanitizeDocComment($reflection->getDeclaringClass()->getProperty($reflection->name)), sprintf('@%s?var\s+%s', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION) ); diff --git a/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php b/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php index 8262dc5..f99f5b6 100644 --- a/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php +++ b/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php @@ -563,6 +563,11 @@ final class NativeLexerTest extends TestCase 'transformed' => 'array{foo: string}', 'type' => ShapedArrayType::class, ], + 'Shaped array with key equal to class name' => [ + 'raw' => 'array{stdclass: string}', + 'transformed' => 'array{stdclass: string}', + 'type' => ShapedArrayType::class, + ], 'Iterable type' => [ 'raw' => 'iterable', 'transformed' => 'iterable', diff --git a/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php b/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php index b269902..3438f6b 100644 --- a/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php @@ -38,6 +38,9 @@ final class ShapedArrayValuesMappingTest extends IntegrationTest 1337, 42.404, ], + 'shapedArrayWithClassNameAsKey' => [ + 'stdclass' => 'foo', + ], ]; foreach ([ShapedArrayValues::class, ShapedArrayValuesWithConstructor::class] as $class) { @@ -55,6 +58,7 @@ final class ShapedArrayValuesMappingTest extends IntegrationTest self::assertSame('bar', $result->advancedShapedArray['mandatoryString']); self::assertSame(1337, $result->advancedShapedArray[0]); self::assertSame(42.404, $result->advancedShapedArray[1]); + self::assertSame('foo', $result->shapedArrayWithClassNameAsKey['stdclass']); } } @@ -100,6 +104,9 @@ class ShapedArrayValues /** @var array{0: int, float, optionalString?: string, mandatoryString: string} */ public array $advancedShapedArray; + + /** @var array{stdclass: string} */ + public array $shapedArrayWithClassNameAsKey; } class ShapedArrayValuesWithConstructor extends ShapedArrayValues @@ -114,6 +121,7 @@ class ShapedArrayValuesWithConstructor extends ShapedArrayValues * bar: int * } $shapedArrayOnSeveralLines * @param array{0: int, float, optionalString?: string, mandatoryString: string} $advancedShapedArray + * @param array{stdclass: string} $shapedArrayWithClassNameAsKey */ public function __construct( array $basicShapedArrayWithStringKeys, @@ -121,7 +129,8 @@ class ShapedArrayValuesWithConstructor extends ShapedArrayValues array $shapedArrayWithObject, array $shapedArrayWithOptionalValue, array $shapedArrayOnSeveralLines, - array $advancedShapedArray + array $advancedShapedArray, + array $shapedArrayWithClassNameAsKey ) { $this->basicShapedArrayWithStringKeys = $basicShapedArrayWithStringKeys; $this->basicShapedArrayWithIntegerKeys = $basicShapedArrayWithIntegerKeys; @@ -129,5 +138,6 @@ class ShapedArrayValuesWithConstructor extends ShapedArrayValues $this->shapedArrayWithOptionalValue = $shapedArrayWithOptionalValue; $this->shapedArrayOnSeveralLines = $shapedArrayOnSeveralLines; $this->advancedShapedArray = $advancedShapedArray; + $this->shapedArrayWithClassNameAsKey = $shapedArrayWithClassNameAsKey; } }