diff --git a/composer.json b/composer.json index 9fcad252b..58fa41d10 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "php": "~8.0.12 || ~8.1.0", "ext-json": "*", "jetbrains/phpstorm-stubs": "2022.2", - "nikic/php-parser": "^4.14.0", - "roave/signature": "^1.5" + "nikic/php-parser": "^4.15.1", + "roave/signature": "^1.6" }, "authors": [ { @@ -34,11 +34,11 @@ ], "require-dev": { "doctrine/coding-standard": "^10.0.0", - "phpstan/phpstan": "^1.8.2", + "phpstan/phpstan": "^1.8.5", "phpstan/phpstan-phpunit": "^1.1.1", - "phpunit/phpunit": "^9.5.23", - "vimeo/psalm": "^4.26", - "roave/infection-static-analysis-plugin": "^1.21.0" + "phpunit/phpunit": "^9.5.24", + "vimeo/psalm": "^4.27", + "roave/infection-static-analysis-plugin": "^1.23.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 5109d5db0..ed7f1b20b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fed91762a9cedc06cfd307840acec84d", + "content-hash": "1660e2990add72e280e1d1b3b7f645e7", "packages": [ { "name": "jetbrains/phpstorm-stubs", @@ -56,16 +56,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.14.0", + "version": "v4.15.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "shasum": "" }, "require": { @@ -106,32 +106,32 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" }, - "time": "2022-05-31T20:59:12+00:00" + "time": "2022-09-04T07:30:47+00:00" }, { "name": "roave/signature", - "version": "1.5.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/Roave/Signature.git", - "reference": "b100e2c40e51f3c56a0b29faf3e7ca75c33df60b" + "reference": "ed898589a3088217e6aa4f4a77d7b2b8f5e91a8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/Signature/zipball/b100e2c40e51f3c56a0b29faf3e7ca75c33df60b", - "reference": "b100e2c40e51f3c56a0b29faf3e7ca75c33df60b", + "url": "https://api.github.com/repos/Roave/Signature/zipball/ed898589a3088217e6aa4f4a77d7b2b8f5e91a8a", + "reference": "ed898589a3088217e6aa4f4a77d7b2b8f5e91a8a", "shasum": "" }, "require": { - "php": "7.4.*|8.0.*|8.1.*" + "php": "7.4.*|8.0.*|8.1.*|8.2.*" }, "require-dev": { - "doctrine/coding-standard": "^9.0", - "infection/infection": "^0.25.1", + "doctrine/coding-standard": "^10.0.0", + "infection/infection": "^0.26.6", "phpunit/phpunit": "^9.5.9", - "vimeo/psalm": "^4.10.1" + "vimeo/psalm": "^4.27.0" }, "type": "library", "autoload": { @@ -146,9 +146,9 @@ "description": "Sign and verify stuff", "support": { "issues": "https://github.com/Roave/Signature/issues", - "source": "https://github.com/Roave/Signature/tree/1.5.0" + "source": "https://github.com/Roave/Signature/tree/1.6.0" }, - "time": "2021-09-18T13:37:44+00:00" + "time": "2022-09-06T11:01:18+00:00" } ], "packages-dev": [ @@ -1126,16 +1126,16 @@ }, { "name": "infection/infection", - "version": "0.26.13", + "version": "0.26.14", "source": { "type": "git", "url": "https://github.com/infection/infection.git", - "reference": "c2f121b1f86af20b1373236535abfe06e5358224" + "reference": "d35e0795d64c2a1e030e63c51fbf6b37991b08d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/infection/infection/zipball/c2f121b1f86af20b1373236535abfe06e5358224", - "reference": "c2f121b1f86af20b1373236535abfe06e5358224", + "url": "https://api.github.com/repos/infection/infection/zipball/d35e0795d64c2a1e030e63c51fbf6b37991b08d2", + "reference": "d35e0795d64c2a1e030e63c51fbf6b37991b08d2", "shasum": "" }, "require": { @@ -1144,6 +1144,7 @@ "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", + "ext-mbstring": "*", "infection/abstract-testframework-adapter": "^0.5.0", "infection/extension-installer": "^0.1.0", "infection/include-interceptor": "^0.2.5", @@ -1235,7 +1236,7 @@ ], "support": { "issues": "https://github.com/infection/infection/issues", - "source": "https://github.com/infection/infection/tree/0.26.13" + "source": "https://github.com/infection/infection/tree/0.26.14" }, "funding": [ { @@ -1247,7 +1248,7 @@ "type": "open_collective" } ], - "time": "2022-06-23T12:48:45+00:00" + "time": "2022-08-31T21:53:53+00:00" }, { "name": "justinrainbow/json-schema", @@ -1878,16 +1879,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.2", + "version": "1.8.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c" + "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c53312ecc575caf07b0e90dee43883fdf90ca67c", - "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6598a5ff12ca4499a836815e08b4d77a2ddeb20", + "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20", "shasum": "" }, "require": { @@ -1911,9 +1912,13 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.2" + "source": "https://github.com/phpstan/phpstan/tree/1.8.5" }, "funding": [ { @@ -1924,16 +1929,12 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2022-07-20T09:57:31+00:00" + "time": "2022-09-07T16:05:32+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -1989,16 +1990,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.16", + "version": "9.2.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073" + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073", - "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", "shasum": "" }, "require": { @@ -2054,7 +2055,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.16" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" }, "funding": [ { @@ -2062,7 +2063,7 @@ "type": "github" } ], - "time": "2022-08-20T05:26:47+00:00" + "time": "2022-08-30T12:24:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2307,16 +2308,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.23", + "version": "9.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "888556852e7e9bbeeedb9656afe46118765ade34" + "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/888556852e7e9bbeeedb9656afe46118765ade34", - "reference": "888556852e7e9bbeeedb9656afe46118765ade34", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", + "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", "shasum": "" }, "require": { @@ -2345,7 +2346,7 @@ "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", + "sebastian/type": "^3.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -2389,7 +2390,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.24" }, "funding": [ { @@ -2401,7 +2402,7 @@ "type": "github" } ], - "time": "2022-08-22T14:01:36+00:00" + "time": "2022-08-30T07:42:16+00:00" }, { "name": "psr/container", @@ -2508,28 +2509,28 @@ }, { "name": "roave/infection-static-analysis-plugin", - "version": "1.21.0", + "version": "1.23.0", "source": { "type": "git", "url": "https://github.com/Roave/infection-static-analysis-plugin.git", - "reference": "7d7587684cc5e2425d9ae52e8ab7358936373c0e" + "reference": "7f189bfdd28e4fcbd79f219232a1f2a1d748994d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/infection-static-analysis-plugin/zipball/7d7587684cc5e2425d9ae52e8ab7358936373c0e", - "reference": "7d7587684cc5e2425d9ae52e8ab7358936373c0e", + "url": "https://api.github.com/repos/Roave/infection-static-analysis-plugin/zipball/7f189bfdd28e4fcbd79f219232a1f2a1d748994d", + "reference": "7f189bfdd28e4fcbd79f219232a1f2a1d748994d", "shasum": "" }, "require": { - "infection/infection": "0.26.13", + "infection/infection": "0.26.14", "ocramius/package-versions": "^1.9.0 || ^2.0.0", - "php": "~8.0.0|~8.1.0", + "php": "~8.0.0|~8.1.0|~8.2.0", "sanmai/later": "^0.1.2", - "vimeo/psalm": "^4.24.0" + "vimeo/psalm": "^4.27.0" }, "require-dev": { - "doctrine/coding-standard": "^9.0.0", - "phpunit/phpunit": "^9.5.21" + "doctrine/coding-standard": "^10.0.0", + "phpunit/phpunit": "^9.5.24" }, "bin": [ "bin/roave-infection-static-analysis-plugin" @@ -2553,9 +2554,9 @@ "description": "Static analysis on top of mutation testing - prevents escaped mutants from being invalid according to static analysis", "support": { "issues": "https://github.com/Roave/infection-static-analysis-plugin/issues", - "source": "https://github.com/Roave/infection-static-analysis-plugin/tree/1.21.0" + "source": "https://github.com/Roave/infection-static-analysis-plugin/tree/1.23.0" }, - "time": "2022-06-29T08:30:25+00:00" + "time": "2022-09-06T10:50:54+00:00" }, { "name": "sanmai/later", @@ -3537,16 +3538,16 @@ }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "fb44e1cc6e557418387ad815780360057e40753e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb44e1cc6e557418387ad815780360057e40753e", + "reference": "fb44e1cc6e557418387ad815780360057e40753e", "shasum": "" }, "require": { @@ -3558,7 +3559,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -3581,7 +3582,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.1.0" }, "funding": [ { @@ -3589,7 +3590,7 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2022-08-29T06:55:37+00:00" }, { "name": "sebastian/version", @@ -4687,16 +4688,16 @@ }, { "name": "thecodingmachine/safe", - "version": "v2.2.2", + "version": "v2.2.3", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "440284f9592c9df402832452a6871a8b3c48d97e" + "reference": "e454a3142d2197694d1fda291a4462ccd3f66e12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/440284f9592c9df402832452a6871a8b3c48d97e", - "reference": "440284f9592c9df402832452a6871a8b3c48d97e", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/e454a3142d2197694d1fda291a4462ccd3f66e12", + "reference": "e454a3142d2197694d1fda291a4462ccd3f66e12", "shasum": "" }, "require": { @@ -4819,9 +4820,9 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v2.2.2" + "source": "https://github.com/thecodingmachine/safe/tree/v2.2.3" }, - "time": "2022-07-20T17:46:34+00:00" + "time": "2022-08-04T14:05:49+00:00" }, { "name": "theseer/tokenizer", @@ -4875,16 +4876,16 @@ }, { "name": "vimeo/psalm", - "version": "4.26.0", + "version": "4.27.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "6998fabb2bf528b65777bf9941920888d23c03ac" + "reference": "faf106e717c37b8c81721845dba9de3d8deed8ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/6998fabb2bf528b65777bf9941920888d23c03ac", - "reference": "6998fabb2bf528b65777bf9941920888d23c03ac", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/faf106e717c37b8c81721845dba9de3d8deed8ff", + "reference": "faf106e717c37b8c81721845dba9de3d8deed8ff", "shasum": "" }, "require": { @@ -4976,9 +4977,9 @@ ], "support": { "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/4.26.0" + "source": "https://github.com/vimeo/psalm/tree/4.27.0" }, - "time": "2022-07-31T13:10:26+00:00" + "time": "2022-08-31T13:47:09+00:00" }, { "name": "webmozart/assert", diff --git a/src/NodeCompiler/CompileNodeToValue.php b/src/NodeCompiler/CompileNodeToValue.php index b5fa0070c..ed8744af7 100644 --- a/src/NodeCompiler/CompileNodeToValue.php +++ b/src/NodeCompiler/CompileNodeToValue.php @@ -8,6 +8,7 @@ use PhpParser\Node; use Roave\BetterReflection\Reflection\ReflectionClass; use Roave\BetterReflection\Reflection\ReflectionClassConstant; +use Roave\BetterReflection\Reflection\ReflectionEnum; use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; use Roave\BetterReflection\Util\FileHelper; @@ -121,6 +122,13 @@ public function __invoke(Node $node, CompilerContext $context): CompiledValue return constant($node->args[0]->value->value); } + if ( + $node instanceof Node\Expr\PropertyFetch + && $node->var instanceof Node\Expr\ClassConstFetch + ) { + return $this->getEnumPropertyValue($node, $context); + } + throw Exception\UnableToCompileNode::forUnRecognizedExpressionInContext($node, $context); }); @@ -130,6 +138,35 @@ public function __invoke(Node $node, CompilerContext $context): CompiledValue return new CompiledValue($value, $constantName); } + private function getEnumPropertyValue(Node\Expr\PropertyFetch $node, CompilerContext $context): mixed + { + assert($node->var instanceof Node\Expr\ClassConstFetch); + assert($node->var->class instanceof Node\Name); + + $className = $this->resolveClassName($node->var->class->toString(), $context); + $class = $context->getReflector()->reflectClass($className); + + if (! $class instanceof ReflectionEnum) { + throw Exception\UnableToCompileNode::becauseOfInvalidEnumCasePropertyFetch($context, $class, $node); + } + + assert($node->var->name instanceof Node\Identifier); + + $case = $class->getCase($node->var->name->name); + + if ($case === null) { + throw Exception\UnableToCompileNode::becauseOfInvalidEnumCasePropertyFetch($context, $class, $node); + } + + assert($node->name instanceof Node\Identifier); + + return match ($node->name->toString()) { + 'value' => $case->getValue(), + 'name' => $case->getName(), + default => throw Exception\UnableToCompileNode::becauseOfInvalidEnumCasePropertyFetch($context, $class, $node), + }; + } + private function resolveConstantName(Node\Expr\ConstFetch $constNode, CompilerContext $context): string { $constantName = $constNode->name->toString(); diff --git a/src/NodeCompiler/Exception/UnableToCompileNode.php b/src/NodeCompiler/Exception/UnableToCompileNode.php index 2ba4680b7..0678682b9 100644 --- a/src/NodeCompiler/Exception/UnableToCompileNode.php +++ b/src/NodeCompiler/Exception/UnableToCompileNode.php @@ -69,6 +69,26 @@ public static function becauseOfNotFoundConstantReference( return $exception; } + public static function becauseOfInvalidEnumCasePropertyFetch( + CompilerContext $fetchContext, + ReflectionClass $targetClass, + Node\Expr\PropertyFetch $propertyFetch, + ): self { + assert($propertyFetch->var instanceof Node\Expr\ClassConstFetch); + assert($propertyFetch->var->name instanceof Node\Identifier); + assert($propertyFetch->name instanceof Node\Identifier); + + return new self(sprintf( + 'Could not get %s::%s->%s while trying to evaluate constant expression in %s in file %s (line %d)', + $targetClass->getName(), + $propertyFetch->var->name->name, + $propertyFetch->name->toString(), + self::compilerContextToContextDescription($fetchContext), + self::getFileName($fetchContext), + $propertyFetch->getLine(), + )); + } + public static function becauseOfMissingFileName( CompilerContext $context, Node\Scalar\MagicConst\Dir|Node\Scalar\MagicConst\File $node, diff --git a/src/Reflection/Adapter/ReflectionClass.php b/src/Reflection/Adapter/ReflectionClass.php index 67c70025c..b3c8fef75 100644 --- a/src/Reflection/Adapter/ReflectionClass.php +++ b/src/Reflection/Adapter/ReflectionClass.php @@ -308,6 +308,11 @@ public function isFinal(): bool return $this->betterReflectionClass->isFinal(); } + public function isReadOnly(): bool + { + return $this->betterReflectionClass->isReadOnly(); + } + public function getModifiers(): int { return $this->betterReflectionClass->getModifiers(); diff --git a/src/Reflection/Adapter/ReflectionEnum.php b/src/Reflection/Adapter/ReflectionEnum.php index b345ca579..2b39ab142 100644 --- a/src/Reflection/Adapter/ReflectionEnum.php +++ b/src/Reflection/Adapter/ReflectionEnum.php @@ -295,6 +295,11 @@ public function isFinal(): bool return $this->betterReflectionEnum->isFinal(); } + public function isReadOnly(): bool + { + return $this->betterReflectionEnum->isReadOnly(); + } + public function getModifiers(): int { return $this->betterReflectionEnum->getModifiers(); diff --git a/src/Reflection/Adapter/ReflectionNamedType.php b/src/Reflection/Adapter/ReflectionNamedType.php index 06d0919be..ddd889bcc 100644 --- a/src/Reflection/Adapter/ReflectionNamedType.php +++ b/src/Reflection/Adapter/ReflectionNamedType.php @@ -23,8 +23,17 @@ public function getName(): string public function __toString(): string { - return ($this->allowsNull && strtolower($this->betterReflectionType->getName()) !== 'mixed' ? '?' : '') - . $this->betterReflectionType->__toString(); + $type = strtolower($this->betterReflectionType->getName()); + + if ( + ! $this->allowsNull + || $type === 'mixed' + || $type === 'null' + ) { + return $this->betterReflectionType->__toString(); + } + + return '?' . $this->betterReflectionType->__toString(); } public function allowsNull(): bool diff --git a/src/Reflection/Adapter/ReflectionObject.php b/src/Reflection/Adapter/ReflectionObject.php index e1480e7f1..2ac261082 100644 --- a/src/Reflection/Adapter/ReflectionObject.php +++ b/src/Reflection/Adapter/ReflectionObject.php @@ -258,6 +258,11 @@ public function isFinal(): bool return $this->betterReflectionObject->isFinal(); } + public function isReadOnly(): bool + { + return $this->betterReflectionObject->isReadOnly(); + } + public function getModifiers(): int { return $this->betterReflectionObject->getModifiers(); diff --git a/src/Reflection/ReflectionClass.php b/src/Reflection/ReflectionClass.php index 68a4e58bf..0efc6c635 100644 --- a/src/Reflection/ReflectionClass.php +++ b/src/Reflection/ReflectionClass.php @@ -62,6 +62,14 @@ class ReflectionClass implements Reflection { + /** + * We cannot use CoreReflectionClass::IS_READONLY because it does not exist in PHP < 8.2. + * Constant is public, so we can use it in tests. + * + * @internal + */ + public const IS_READONLY = 65536; + public const ANONYMOUS_CLASS_NAME_PREFIX = 'class@anonymous'; public const ANONYMOUS_CLASS_NAME_PREFIX_REGEXP = '~^(?:class|[\w\\\\]+)@anonymous~'; private const ANONYMOUS_CLASS_NAME_SUFFIX = '@anonymous'; @@ -1055,6 +1063,11 @@ public function isFinal(): bool return $this->node instanceof ClassNode && $this->node->isFinal(); } + public function isReadOnly(): bool + { + return $this->node instanceof ClassNode && $this->node->isReadonly(); + } + /** * Get the core-reflection-compatible modifier values. */ @@ -1062,6 +1075,7 @@ public function getModifiers(): int { $val = $this->isAbstract() ? CoreReflectionClass::IS_EXPLICIT_ABSTRACT : 0; $val += $this->isFinal() ? CoreReflectionClass::IS_FINAL : 0; + $val += $this->isReadOnly() ? self::IS_READONLY : 0; return $val; } diff --git a/src/Reflection/ReflectionNamedType.php b/src/Reflection/ReflectionNamedType.php index 285769fba..8d4ffd597 100644 --- a/src/Reflection/ReflectionNamedType.php +++ b/src/Reflection/ReflectionNamedType.php @@ -33,7 +33,7 @@ class ReflectionNamedType extends ReflectionType 'null' => null, 'never' => null, 'false' => null, - 'true' => null, + 'true' => null, ]; private string $name; @@ -107,7 +107,11 @@ public function getClass(): ReflectionClass public function allowsNull(): bool { - return strtolower($this->name) === 'mixed'; + return match (strtolower($this->name)) { + 'mixed' => true, + 'null' => true, + default => false, + }; } public function __toString(): string diff --git a/src/Reflection/ReflectionObject.php b/src/Reflection/ReflectionObject.php index a2e29d735..f604c726f 100644 --- a/src/Reflection/ReflectionObject.php +++ b/src/Reflection/ReflectionObject.php @@ -352,6 +352,11 @@ public function isFinal(): bool return $this->reflectionClass->isFinal(); } + public function isReadOnly(): bool + { + return $this->reflectionClass->isReadOnly(); + } + public function getModifiers(): int { return $this->reflectionClass->getModifiers(); diff --git a/src/Reflection/ReflectionType.php b/src/Reflection/ReflectionType.php index ca44a71e9..eb1d07f09 100644 --- a/src/Reflection/ReflectionType.php +++ b/src/Reflection/ReflectionType.php @@ -32,15 +32,19 @@ public static function createFromNode( } if ($type instanceof Identifier || $type instanceof Name) { - if ($allowsNull) { - return new ReflectionUnionType( - $reflector, - $owner, - new UnionType([$type, new Identifier('null')]), - ); + if ( + $type->toLowerString() === 'null' + || $type->toLowerString() === 'mixed' + || ! $allowsNull + ) { + return new ReflectionNamedType($reflector, $owner, $type); } - return new ReflectionNamedType($reflector, $owner, $type); + return new ReflectionUnionType( + $reflector, + $owner, + new UnionType([$type, new Identifier('null')]), + ); } if ($type instanceof IntersectionType) { diff --git a/src/SourceLocator/SourceStubber/ReflectionSourceStubber.php b/src/SourceLocator/SourceStubber/ReflectionSourceStubber.php index 10e3f209a..49954c2bd 100644 --- a/src/SourceLocator/SourceStubber/ReflectionSourceStubber.php +++ b/src/SourceLocator/SourceStubber/ReflectionSourceStubber.php @@ -599,7 +599,7 @@ private function formatType(CoreReflectionNamedType|CoreReflectionUnionType|Core $name = $type->getName(); $nameNode = $this->formatNamedType($type); - if (! $type->allowsNull() || $name === 'mixed') { + if (! $type->allowsNull() || $name === 'mixed' || $name === 'null') { return $nameNode; } diff --git a/test/unit/Fixture/ExampleClass.php b/test/unit/Fixture/ExampleClass.php index f2f831f59..fce5d620d 100644 --- a/test/unit/Fixture/ExampleClass.php +++ b/test/unit/Fixture/ExampleClass.php @@ -68,6 +68,10 @@ final class FinalClass { } + readonly class ReadOnlyClass + { + } + trait ExampleTrait { } diff --git a/test/unit/NodeCompiler/CompileNodeToValueTest.php b/test/unit/NodeCompiler/CompileNodeToValueTest.php index 3443ba276..a7d347483 100644 --- a/test/unit/NodeCompiler/CompileNodeToValueTest.php +++ b/test/unit/NodeCompiler/CompileNodeToValueTest.php @@ -638,6 +638,106 @@ class Foo extends Baz { self::assertSame('parentConstant', $classInfo->getProperty('parentConstant')->getDefaultValue()); } + /** @return list */ + public function enumCasePropertyProvider(): array + { + return [ + ['name', 'ONE'], + ['value', 1], + ]; + } + + /** @dataProvider enumCasePropertyProvider */ + public function testEnumPropertyValue(string $propertyName, string|int $expectedPropertyValue): void + { + $phpCode = sprintf( + <<<'PHP' + %s; + } + PHP, + $propertyName, + ); + + $reflector = new DefaultReflector(new AggregateSourceLocator([ + new StringSourceLocator($phpCode, $this->astLocator), + new PhpInternalSourceLocator($this->astLocator, $this->sourceStubber), + ])); + $classInfo = $reflector->reflectClass('Bat'); + self::assertSame($expectedPropertyValue, $classInfo->getConstant('ONE_VALUE')); + } + + public function testEnumPropertyValueThrowsExceptionWhenNoEnum(): void + { + $phpCode = <<<'PHP' + value; + } + PHP; + + $reflector = new DefaultReflector(new StringSourceLocator($phpCode, $this->astLocator)); + $classInfo = $reflector->reflectClass('Bat'); + + self::expectException(UnableToCompileNode::class); + $classInfo->getConstant('ONE_VALUE'); + } + + public function testEnumPropertyValueThrowsExceptionWhenCaseDoesNotExist(): void + { + $phpCode = <<<'PHP' + value; + } + PHP; + + $reflector = new DefaultReflector(new AggregateSourceLocator([ + new StringSourceLocator($phpCode, $this->astLocator), + new PhpInternalSourceLocator($this->astLocator, $this->sourceStubber), + ])); + $classInfo = $reflector->reflectClass('Bat'); + + self::expectException(UnableToCompileNode::class); + $classInfo->getConstant('TWO_VALUE'); + } + + public function testEnumPropertyValueThrowsExceptionWhenPropertyDoesNotExist(): void + { + $phpCode = <<<'PHP' + missing; + } + PHP; + + $reflector = new DefaultReflector(new AggregateSourceLocator([ + new StringSourceLocator($phpCode, $this->astLocator), + new PhpInternalSourceLocator($this->astLocator, $this->sourceStubber), + ])); + $classInfo = $reflector->reflectClass('Bat'); + + self::expectException(UnableToCompileNode::class); + $classInfo->getConstant('ONE_VALUE'); + } + /** @return list */ public function magicConstantsWithoutNamespaceProvider(): array { diff --git a/test/unit/NodeCompiler/Exception/UnableToCompileNodeTest.php b/test/unit/NodeCompiler/Exception/UnableToCompileNodeTest.php index 3c95f7f75..22d64d322 100644 --- a/test/unit/NodeCompiler/Exception/UnableToCompileNodeTest.php +++ b/test/unit/NodeCompiler/Exception/UnableToCompileNodeTest.php @@ -7,6 +7,7 @@ use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Identifier; use PhpParser\Node\Name; @@ -100,6 +101,35 @@ public function testBecauseOfNotFoundClassConstantReference(CompilerContext $con ); } + /** @dataProvider supportedContextTypes */ + public function testBecauseOfOfInvalidEnumCasePropertyFetch(CompilerContext $context, string $contextName): void + { + $targetClass = $this->createMock(ReflectionClass::class); + + $targetClass + ->expects(self::any()) + ->method('getName') + ->willReturn('An\\Example'); + + self::assertSame( + sprintf( + 'Could not get An\Example::SOME_CONSTANT->value while trying to evaluate constant expression in %s in file "" (line -1)', + $contextName, + ), + UnableToCompileNode::becauseOfInvalidEnumCasePropertyFetch( + $context, + $targetClass, + new PropertyFetch( + new ClassConstFetch( + new Name\FullyQualified('A'), + new Identifier('SOME_CONSTANT'), + ), + 'value', + ), + )->getMessage(), + ); + } + /** @dataProvider supportedContextTypes */ public function testForUnRecognizedExpressionInContext(CompilerContext $context, string $contextName): void { diff --git a/test/unit/Reflection/Adapter/ReflectionClassTest.php b/test/unit/Reflection/Adapter/ReflectionClassTest.php index 036431fc6..5a756a733 100644 --- a/test/unit/Reflection/Adapter/ReflectionClassTest.php +++ b/test/unit/Reflection/Adapter/ReflectionClassTest.php @@ -98,6 +98,7 @@ public function methodExpectationProvider(): array ['isTrait', [], true, null, true, null], ['isAbstract', [], true, null, true, null], ['isFinal', [], true, null, true, null], + ['isReadOnly', [], true, null, true, null], ['getModifiers', [], 123, null, 123, null], ['isInstance', [new stdClass()], true, null, true, null], ['newInstance', [], null, NotImplemented::class, null, null], diff --git a/test/unit/Reflection/Adapter/ReflectionEnumTest.php b/test/unit/Reflection/Adapter/ReflectionEnumTest.php index 47d86844a..f53173599 100644 --- a/test/unit/Reflection/Adapter/ReflectionEnumTest.php +++ b/test/unit/Reflection/Adapter/ReflectionEnumTest.php @@ -105,6 +105,7 @@ public function methodExpectationProvider(): array ['isTrait', [], true, null, true, null], ['isAbstract', [], true, null, true, null], ['isFinal', [], true, null, true, null], + ['isReadOnly', [], true, null, true, null], ['getModifiers', [], 123, null, 123, null], ['isInstance', [new stdClass()], true, null, true, null], ['newInstance', [], null, NotImplemented::class, null, null], diff --git a/test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php b/test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php index 82e7dbdb2..30f7640de 100644 --- a/test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php +++ b/test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php @@ -53,6 +53,8 @@ public function dataNoNullabilityMarkerForMixed(): array return [ ['mixed'], ['MiXeD'], + ['null'], + ['nULl'], ]; } diff --git a/test/unit/Reflection/Adapter/ReflectionObjectTest.php b/test/unit/Reflection/Adapter/ReflectionObjectTest.php index 4610f32ca..4db0ccfce 100644 --- a/test/unit/Reflection/Adapter/ReflectionObjectTest.php +++ b/test/unit/Reflection/Adapter/ReflectionObjectTest.php @@ -97,6 +97,7 @@ public function methodExpectationProvider(): array ['isTrait', [], true, null, true, null], ['isAbstract', [], true, null, true, null], ['isFinal', [], true, null, true, null], + ['isReadOnly', [], true, null, true, null], ['getModifiers', [], 123, null, 123, null], ['isInstance', [new stdClass()], true, null, true, null], ['newInstance', [], null, NotImplemented::class, null, null], diff --git a/test/unit/Reflection/ReflectionClassTest.php b/test/unit/Reflection/ReflectionClassTest.php index de3ae9cbe..45c902dd4 100644 --- a/test/unit/Reflection/ReflectionClassTest.php +++ b/test/unit/Reflection/ReflectionClassTest.php @@ -16,7 +16,6 @@ use PhpParser\Node\Stmt\Class_; use PHPUnit\Framework\TestCase; use Qux; -use Reflection as CoreReflection; use ReflectionClass as CoreReflectionClass; use ReflectionMethod as CoreReflectionMethod; use ReflectionProperty as CoreReflectionProperty; @@ -74,6 +73,7 @@ use Roave\BetterReflectionTest\Fixture\InvalidInheritances; use Roave\BetterReflectionTest\Fixture\MethodsOrder; use Roave\BetterReflectionTest\Fixture\PureEnum; +use Roave\BetterReflectionTest\Fixture\ReadOnlyClass; use Roave\BetterReflectionTest\Fixture\StaticProperties; use Roave\BetterReflectionTest\Fixture\StaticPropertyGetSet; use Roave\BetterReflectionTest\Fixture\StringEnum; @@ -1130,22 +1130,33 @@ public function testIsFinalForEnum(): void self::assertTrue($classInfo->isFinal()); } - /** @return list}> */ + public function testIsReadOnly(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator( + __DIR__ . '/../Fixture/ExampleClass.php', + $this->astLocator, + )); + + $classInfo = $reflector->reflectClass(ReadOnlyClass::class); + self::assertTrue($classInfo->isReadOnly()); + + $classInfo = $reflector->reflectClass(ExampleClass::class); + self::assertFalse($classInfo->isReadOnly()); + } + + /** @return list */ public function modifierProvider(): array { return [ - ['ExampleClass', 0, []], - ['AbstractClass', CoreReflectionClass::IS_EXPLICIT_ABSTRACT, ['abstract']], - ['FinalClass', CoreReflectionClass::IS_FINAL, ['final']], + ['ExampleClass', 0], + ['AbstractClass', CoreReflectionClass::IS_EXPLICIT_ABSTRACT], + ['FinalClass', CoreReflectionClass::IS_FINAL], + ['ReadOnlyClass', ReflectionClass::IS_READONLY], ]; } - /** - * @param list $expectedModifierNames - * - * @dataProvider modifierProvider - */ - public function testGetModifiers(string $className, int $expectedModifier, array $expectedModifierNames): void + /** @dataProvider modifierProvider */ + public function testGetModifiers(string $className, int $expectedModifier): void { $reflector = new DefaultReflector(new SingleFileSourceLocator( __DIR__ . '/../Fixture/ExampleClass.php', @@ -1155,10 +1166,6 @@ public function testGetModifiers(string $className, int $expectedModifier, array $classInfo = $reflector->reflectClass('\Roave\BetterReflectionTest\Fixture\\' . $className); self::assertSame($expectedModifier, $classInfo->getModifiers()); - self::assertSame( - $expectedModifierNames, - CoreReflection::getModifierNames($classInfo->getModifiers()), - ); } public function testIsTrait(): void diff --git a/test/unit/Reflection/ReflectionNamedTypeTest.php b/test/unit/Reflection/ReflectionNamedTypeTest.php index 5a44ca839..3ea2f2f1f 100644 --- a/test/unit/Reflection/ReflectionNamedTypeTest.php +++ b/test/unit/Reflection/ReflectionNamedTypeTest.php @@ -51,15 +51,17 @@ public function testAllowsNull(): void } /** @return list */ - public function dataMixedAllowsNull(): array + public function dataAllowsNull(): array { return [ ['mixed'], ['MIXED'], + ['null'], + ['NuLl'], ]; } - /** @dataProvider dataMixedAllowsNull */ + /** @dataProvider dataAllowsNull */ public function testMixedAllowsNull(string $mixedType): void { $noNullType = $this->createType($mixedType); @@ -86,6 +88,8 @@ public function isBuildinProvider(): Generator yield ['FALSE']; yield ['true']; yield ['TRUE']; + yield ['null']; + yield ['NULL']; } /** @dataProvider isBuildinProvider */ diff --git a/test/unit/Reflection/ReflectionTypeTest.php b/test/unit/Reflection/ReflectionTypeTest.php index 89e7ddcad..e1b740af0 100644 --- a/test/unit/Reflection/ReflectionTypeTest.php +++ b/test/unit/Reflection/ReflectionTypeTest.php @@ -68,6 +68,10 @@ public function dataProvider(): array 'A|B|null', true, ], + [new Node\Name('null'), false, ReflectionNamedType::class, 'null', true], + [new Node\Name('null'), true, ReflectionNamedType::class, 'null', true], + [new Node\Name('mixed'), false, ReflectionNamedType::class, 'mixed', true], + [new Node\Name('mixed'), true, ReflectionNamedType::class, 'mixed', true], ]; } diff --git a/test/unit/Reflection/StringCast/ReflectionTypeStringCastTest.php b/test/unit/Reflection/StringCast/ReflectionTypeStringCastTest.php index e552d3979..0c7ccb2f4 100644 --- a/test/unit/Reflection/StringCast/ReflectionTypeStringCastTest.php +++ b/test/unit/Reflection/StringCast/ReflectionTypeStringCastTest.php @@ -34,6 +34,7 @@ function d(): A&B {} function e(): int {} function f(): null|int {} function g(): string|null|int {} +function h(): null {} PHP , BetterReflectionSingleton::instance() @@ -57,6 +58,7 @@ function g(): string|null|int {} [$returnTypeForFunction('e'), 'int'], [$returnTypeForFunction('f'), '?int'], [$returnTypeForFunction('g'), 'string|null|int'], + [$returnTypeForFunction('h'), 'null'], ]; } diff --git a/test/unit/Reflector/DefaultReflectorTest.php b/test/unit/Reflector/DefaultReflectorTest.php index 3b72045af..91cd3aa60 100644 --- a/test/unit/Reflector/DefaultReflectorTest.php +++ b/test/unit/Reflector/DefaultReflectorTest.php @@ -60,7 +60,7 @@ public function testReflectAllClasses(): void ))->reflectAllClasses(); self::assertContainsOnlyInstancesOf(ReflectionClass::class, $classes); - self::assertCount(10, $classes); + self::assertCount(11, $classes); } public function testReflectFunction(): void