From 6bcf76bcacca70913674ed3d1a9c86c02001d6da Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Sun, 14 Apr 2024 23:06:12 +0300 Subject: [PATCH 1/3] Add broken functional test with complex types --- .../src/Application/ClassWithComplexTypes.php | 27 +++++++++++++++++++ .../project/src/Aspect/WeavingAspect.php | 6 +++++ tests/Go/Functional/ClassWeavingTest.php | 21 +++++++++++---- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 tests/Fixtures/project/src/Application/ClassWithComplexTypes.php diff --git a/tests/Fixtures/project/src/Application/ClassWithComplexTypes.php b/tests/Fixtures/project/src/Application/ClassWithComplexTypes.php new file mode 100644 index 00000000..953c9ef7 --- /dev/null +++ b/tests/Fixtures/project/src/Application/ClassWithComplexTypes.php @@ -0,0 +1,27 @@ +*(*))")] + public function afterComplexTypeMethods(): void + { + echo 'It intercepts methods with complex types'; + } } diff --git a/tests/Go/Functional/ClassWeavingTest.php b/tests/Go/Functional/ClassWeavingTest.php index d9c0477d..a58f549a 100644 --- a/tests/Go/Functional/ClassWeavingTest.php +++ b/tests/Go/Functional/ClassWeavingTest.php @@ -13,13 +13,14 @@ namespace Go\Functional; use Go\Tests\TestProject\Application\AbstractBar; +use Go\Tests\TestProject\Application\ClassWithComplexTypes; use Go\Tests\TestProject\Application\FinalClass; use Go\Tests\TestProject\Application\FooInterface; use Go\Tests\TestProject\Application\Main; class ClassWeavingTest extends BaseFunctionalTestCase { - public function testPropertyWeaving() + public function testPropertyWeaving(): void { // it weaves Main class public and protected properties $this->assertPropertyWoven(Main::class, 'publicClassProperty', 'Go\\Tests\\TestProject\\Aspect\\PropertyInterceptAspect->interceptClassProperty'); @@ -32,7 +33,7 @@ public function testPropertyWeaving() /** * test for https://github.com/goaop/framework/issues/335 */ - public function testItDoesNotWeaveAbstractMethods() + public function testItDoesNotWeaveAbstractMethods(): void { // it weaves Main class $this->assertClassIsWoven(Main::class); @@ -46,13 +47,13 @@ public function testItDoesNotWeaveAbstractMethods() $this->assertClassIsNotWoven(AbstractBar::class); } - public function testClassInitializationWeaving() + public function testClassInitializationWeaving(): void { $this->assertClassInitializationWoven(Main::class, 'Go\\Tests\\TestProject\\Aspect\\InitializationAspect->beforeInstanceInitialization'); $this->assertClassStaticInitializationWoven(Main::class, 'Go\\Tests\\TestProject\\Aspect\\InitializationAspect->afterClassStaticInitialization'); } - public function testItWeavesFinalClasses() + public function testItWeavesFinalClasses(): void { // it weaves FinalClass class $this->assertClassIsWoven(FinalClass::class); @@ -70,8 +71,18 @@ public function testItWeavesFinalClasses() $this->assertMethodNotWoven(FinalClass::class, 'someFinalParentMethod'); } - public function testItDoesNotWeaveInterfaces() + public function testItDoesNotWeaveInterfaces(): void { $this->assertClassIsNotWoven(FooInterface::class); } + + public function testItDoesWeaveMethodWithComplexTypes(): void + { + // it weaves ClassWithComplexTypes class + $this->assertClassIsWoven(ClassWithComplexTypes::class); + + $this->assertMethodWoven(ClassWithComplexTypes::class, 'publicMethodWithUnionTypeReturn'); + $this->assertMethodWoven(ClassWithComplexTypes::class, 'publicMethodWithIntersectionTypeReturn'); + $this->assertMethodWoven(ClassWithComplexTypes::class, 'publicMethodWithDNFTypeReturn'); + } } From 452a0234d1ed74fdebb936c9f739021312b14f65 Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Mon, 15 Apr 2024 00:39:08 +0300 Subject: [PATCH 2/3] Add broken unit tests for PHP8.0-PHP8.2 files and SelfValueTransformer --- .../Transformer/SelfValueTransformerTest.php | 66 +++++---- .../Transformer/_files/php80-file.php | 131 ++++++++++++++++++ .../Transformer/_files/php81-file.php | 119 ++++++++++++++++ .../Transformer/_files/php82-file.php | 94 +++++++++++++ 4 files changed, 386 insertions(+), 24 deletions(-) create mode 100644 tests/Go/Instrument/Transformer/_files/php80-file.php create mode 100644 tests/Go/Instrument/Transformer/_files/php81-file.php create mode 100644 tests/Go/Instrument/Transformer/_files/php82-file.php diff --git a/tests/Go/Instrument/Transformer/SelfValueTransformerTest.php b/tests/Go/Instrument/Transformer/SelfValueTransformerTest.php index 98f4b991..570805bb 100644 --- a/tests/Go/Instrument/Transformer/SelfValueTransformerTest.php +++ b/tests/Go/Instrument/Transformer/SelfValueTransformerTest.php @@ -14,6 +14,7 @@ use Go\Core\AspectContainer; use Go\Core\AspectKernel; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -43,15 +44,7 @@ public function setUp(): void */ protected function getKernelMock(array $options): AspectKernel { - $mock = $this->getMockForAbstractClass( - AspectKernel::class, - [], - '', - false, - true, - true, - ['getOptions', 'getContainer'] - ); + $mock = $this->createMock(AspectKernel::class); $mock ->method('getOptions') ->willReturn($options); @@ -63,23 +56,48 @@ protected function getKernelMock(array $options): AspectKernel return $mock; } - public function testTransformerReplacesAllSelfPlaces(): void - { - $testFile = fopen(__DIR__ . '/_files/file-with-self.php', 'rb'); - $content = stream_get_contents($testFile); - $metadata = new StreamMetaData($testFile, $content); - $this->transformer->transform($metadata); - $expected = file_get_contents(__DIR__ . '/_files/file-with-self-transformed.php'); - $this->assertSame($expected, (string) $metadata->source); + #[DataProvider("filesDataProvider")] + public function testTransformerProcessFiles( + string $sourceFileWithContent, + string $fileWithExpectedContent, + ): void { + try { + $sourceFile = fopen($sourceFileWithContent, 'rb'); + $sourceContent = stream_get_contents($sourceFile); + $sourceMetadata = new StreamMetaData($sourceFile, $sourceContent); + $this->transformer->transform($sourceMetadata); + + $expected = file_get_contents($fileWithExpectedContent); + $this->assertSame($expected, $sourceMetadata->source); + + } finally { + if (isset($sourceFile) && is_resource($sourceFile)) { + fclose($sourceFile); + } + } } - public function testTransformerReplacesAllSelfPlacesWithoutNamespace(): void + public static function filesDataProvider(): \Generator { - $testFile = fopen(__DIR__ . '/_files/file-with-self-no-namespace.php', 'rb'); - $content = stream_get_contents($testFile); - $metadata = new StreamMetaData($testFile, $content); - $this->transformer->transform($metadata); - $expected = file_get_contents(__DIR__ . '/_files/file-with-self-no-namespace-transformed.php'); - $this->assertSame($expected, (string) $metadata->source); + yield 'file-with-self.php' => [ + __DIR__ . '/_files/file-with-self.php', + __DIR__ . '/_files/file-with-self-transformed.php' + ]; + yield 'file-with-self-no-namespace.php' => [ + __DIR__ . '/_files/file-with-self-no-namespace.php', + __DIR__ . '/_files/file-with-self-no-namespace-transformed.php' + ]; + yield 'php80-file.php' => [ + __DIR__ . '/_files/php80-file.php', + __DIR__ . '/_files/php80-file-transformed.php' + ]; + yield 'php81-file.php' => [ + __DIR__ . '/_files/php81-file.php', + __DIR__ . '/_files/php81-file-transformed.php' + ]; + yield 'php82-file.php' => [ + __DIR__ . '/_files/php82-file.php', + __DIR__ . '/_files/php82-file-transformed.php' + ]; } } diff --git a/tests/Go/Instrument/Transformer/_files/php80-file.php b/tests/Go/Instrument/Transformer/_files/php80-file.php new file mode 100644 index 00000000..8b4ca732 --- /dev/null +++ b/tests/Go/Instrument/Transformer/_files/php80-file.php @@ -0,0 +1,131 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection\Stub; + +use Attribute; +use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P}; + +class ClassWithPhp80Features +{ + public function acceptsStringArrayDefaultToNull(array|string $iterable = null) : array {} +} + +/** + * @see https://php.watch/versions/8.0/named-parameters + */ +class ClassWithPHP80NamedCall +{ + public static function foo(string $key1 = '', string $key2 = ''): string + { + return $key1 . ':' . $key2; + } + + public static function namedCall(): array + { + return [ + 'key1' => self::foo(key1: 'bar'), + 'key2' => self::foo(key2: 'baz'), + 'keys' => self::foo(key1: 'A', key2: 'B'), + 'reverseKeys' => self::foo(key2: 'A', key1: 'B'), + 'unpack' => self::foo(...['key1' => 'C', 'key2' => 'D']), + ]; + } +} + +/** + * @see https://php.watch/versions/8.0/attributes + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +readonly class ClassPHP80Attribute +{ + private string $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public function getValue(): string + { + return $this->value; + } +} + +/** + * @see https://php.watch/versions/8.0/attributes + */ +#[ClassPHP80Attribute('class')] +class ClassPHP80WithAttribute +{ + #[ClassPHP80Attribute('first')] + #[ClassPHP80Attribute('second')] + public const PUBLIC_CONST = 1; + + #[ClassPHP80Attribute('property')] + private string $privateProperty = 'foo'; + + #[ClassPHP80Attribute('method')] + public function bar(#[ClassPHP80Attribute('parameter')] $parameter) + {} +} + +/** + * @see https://php.watch/versions/8.0/constructor-property-promotion + */ +class ClassPHP80WithPropertyPromotion +{ + public function __construct( + private string $privateStringValue, + private $privateNonTypedValue, + protected int $protectedIntValue = 42, + public array $publicArrayValue = [M_PI, M_E], + ) {} +} + +/** + * @see https://php.watch/versions/8.0/union-types + */ +class ClassWithPHP80UnionTypes +{ + public string|int|float|bool $scalarValue; + + public array|object|null $complexValueOrNull = null; + + /** + * Special case, internally iterable should be replaced with Traversable|array + */ + public iterable|object $iterableOrObject; + + public static function returnsUnionType(): object|array|null {} + + public static function acceptsUnionType(\stdClass|\Traversable|array $iterable): void {} +} + +/** + * @see https://php.watch/versions/8.0/mixed-type + */ +class ClassWithPHP80MixedType +{ + public mixed $someMixedPublicProperty; + + public static function returnsMixed(): mixed {} + + public static function acceptsMixed(mixed $value): void {} +} + +/** + * @see https://php.watch/versions/8.0/static-return-type + */ +class ClassWithPHP80StaticReturnType +{ + public static function create(): static {} +} \ No newline at end of file diff --git a/tests/Go/Instrument/Transformer/_files/php81-file.php b/tests/Go/Instrument/Transformer/_files/php81-file.php new file mode 100644 index 00000000..b0809015 --- /dev/null +++ b/tests/Go/Instrument/Transformer/_files/php81-file.php @@ -0,0 +1,119 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection\Stub; + +/** + * @see https://php.watch/versions/8.1/readonly + */ +class ClassWithPhp81ReadOnlyProperties +{ + public readonly int $publicReadonlyInt; + + protected readonly array $protectedReadonlyArray; + + private readonly object $privateReadonlyObject; +} + +/** + * @see https://php.watch/versions/8.1/enums + */ +enum SimplePhp81EnumWithSuit { + case Clubs; + case Diamonds; + case Hearts; + case Spades; +} + +/** + * @see https://php.watch/versions/8.1/enums#enums-backed + */ +enum BackedPhp81EnumHTTPMethods: string +{ + case GET = 'get'; + case POST = 'post'; +} + +/** + * @see https://php.watch/versions/8.1/enums#enum-methods + */ +enum BackedPhp81EnumHTTPStatusWithMethod: int +{ + case OK = 200; + case ACCESS_DENIED = 403; + case NOT_FOUND = 404; + + public function label(): string { + return static::getLabel($this); + } + + public static function getLabel(self $value): string { + return match ($value) { + self::OK => 'OK', + self::ACCESS_DENIED => 'Access Denied', + self::NOT_FOUND => 'Page Not Found', + }; + } +} + +/** + * @see https://php.watch/versions/8.1/intersection-types + */ +class ClassWithPhp81IntersectionType implements \Countable +{ + private \Iterator&\Countable $countableIterator; + + public function __construct(\Iterator&\Countable $countableIterator) + { + $this->countableIterator = $countableIterator; + } + + public function count(): int + { + return count($this->countableIterator); + } +} + +/** + * @see https://php.watch/versions/8.1/intersection-types + */ +function functionWithPhp81IntersectionType(\Iterator&\Countable $value): \Iterator&\Countable { + foreach($value as $val) {} + count($value); + + return $value; +} + +/** + * @see https://php.watch/versions/8.1/never-return-type + */ +class ClassWithPhp81NeverReturnType +{ + public static function doThis(): never + { + throw new \RuntimeException('Not implemented'); + } +} + +/** + * @see https://php.watch/versions/8.1/never-return-type + */ +function functionWithPhp81NeverReturnType(): never +{ + throw new \RuntimeException('Not implemented'); +} + +/** + * @see https://php.watch/versions/8.1/final-class-const + */ +class ClassWithPhp81FinalClassConst { + final public const TEST = '1'; +} diff --git a/tests/Go/Instrument/Transformer/_files/php82-file.php b/tests/Go/Instrument/Transformer/_files/php82-file.php new file mode 100644 index 00000000..678ccc6f --- /dev/null +++ b/tests/Go/Instrument/Transformer/_files/php82-file.php @@ -0,0 +1,94 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection\Stub; + +/** + * @see https://php.watch/versions/8.2/readonly-classes + */ +readonly class ClassWithPhp82ReadOnlyFlag +{ + public int $publicInt; +} + +/** + * @see https://php.watch/versions/8.2/dnf-types + */ +class ClassWithPhp82DNFType +{ + private (JSONResponse&SuccessResponse)|HTMLResponse|string $respond; + + public function __construct((JSONResponse&SuccessResponse)|HTMLResponse|string $respond) + { + $this->respond = $respond; + } + + public function respond(): (JSONResponse&SuccessResponse)|HTMLResponse|string + { + return $this->respond; + } +} + +/** + * @see https://php.watch/versions/8.2/null-false-types + * @see https://php.watch/versions/8.2/true-type + */ +class ClassWithPhp82NullFalseTypes +{ + private true $isTrue = true; + private false $isFalse = false; + private null $isNull = null; + + public function returnsFalse(): false + { + return false; + } + + public function returnsTrue(): true + { + return true; + } + + public function returnsNullExplicitly(): null + { + return null; + } + + public function acceptsTrue(true $acceptsTrue): void {} + public function acceptsFalse(false $acceptsFalse): void {} + public function acceptsNull(null $acceptsNull): void {} +} + +/** + * @see https://php.watch/versions/8.2/constants-in-traits + */ +trait TraitWithPhp82Constant +{ + protected const CURRENT_VERSION = '2.6'; + final protected const MIN_VERSION = '2.5'; + + protected function ensureVersion(): void + { + if (self::CURRENT_VERSION < self::MIN_VERSION) { + throw new \Exception('Current version is too old'); + } + } +} + +class ClassWithPhp82SensitiveAttribute +{ + private string $secret; + + public function __construct(#[\SensitiveParameter] string $secret = 'password') + { + $this->secret = $secret; + } +} \ No newline at end of file From e20f450190c25842edb5dd3599e1b422a8d0f34a Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 7 May 2024 19:21:59 +0700 Subject: [PATCH 3/3] Add PHP 8.0+ Union and Intersection type support on SelfValueVisitor (#504) Add PHP 8.0+ Union and Intersection type support on SelfValueVisitor --- .../Transformer/SelfValueVisitor.php | 30 +++- .../Transformer/SelfValueTransformerTest.php | 4 + .../_files/anonymous-class-transformed.php | 27 ++++ .../Transformer/_files/anonymous-class.php | 27 ++++ .../_files/php80-file-transformed.php | 131 ++++++++++++++++++ .../_files/php81-file-transformed.php | 119 ++++++++++++++++ .../_files/php82-file-transformed.php | 94 +++++++++++++ 7 files changed, 426 insertions(+), 6 deletions(-) create mode 100644 tests/Go/Instrument/Transformer/_files/anonymous-class-transformed.php create mode 100644 tests/Go/Instrument/Transformer/_files/anonymous-class.php create mode 100644 tests/Go/Instrument/Transformer/_files/php80-file-transformed.php create mode 100644 tests/Go/Instrument/Transformer/_files/php81-file-transformed.php create mode 100644 tests/Go/Instrument/Transformer/_files/php82-file-transformed.php diff --git a/src/Instrument/Transformer/SelfValueVisitor.php b/src/Instrument/Transformer/SelfValueVisitor.php index a8339a6b..9c139381 100644 --- a/src/Instrument/Transformer/SelfValueVisitor.php +++ b/src/Instrument/Transformer/SelfValueVisitor.php @@ -19,15 +19,18 @@ use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; +use PhpParser\Node\IntersectionType; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\Param; use PhpParser\Node\Stmt\Catch_; -use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Property; +use PhpParser\Node\Stmt\Trait_; +use PhpParser\Node\UnionType; use PhpParser\NodeVisitorAbstract; use UnexpectedValueException; @@ -82,10 +85,6 @@ public function enterNode(Node $node) { if ($node instanceof Namespace_) { $this->namespace = !empty($node->name) ? $node->name->toString() : null; - } elseif ($node instanceof Class_) { - if ($node->name !== null) { - $this->className = new Name($node->name->toString()); - } } elseif ($node instanceof ClassMethod || $node instanceof Closure) { if (isset($node->returnType)) { $node->returnType = $this->resolveType($node->returnType); @@ -107,6 +106,12 @@ public function enterNode(Node $node) foreach ($node->types as &$type) { $type = $this->resolveClassName($type); } + } elseif ($node instanceof ClassLike) { + if (! $node instanceof Trait_) { + $this->className = !empty($node->name) ? new Name($node->name->toString()) : null; + } else { + $this->className = null; + } } return null; @@ -126,6 +131,10 @@ protected function resolveClassName(Name $name): Name return $name; } + if ($this->className === null) { + return $name; + } + // Save the original name $originalName = $name; $name = clone $originalName; @@ -142,7 +151,7 @@ protected function resolveClassName(Name $name): Name /** * Helper method for resolving type nodes * - * @return NullableType|Name|FullyQualified|Identifier + * @return NullableType|Name|FullyQualified|Identifier|UnionType|IntersectionType */ private function resolveType(Node $node) { @@ -157,6 +166,15 @@ private function resolveType(Node $node) return $node; } + if ($node instanceof UnionType || $node instanceof IntersectionType) { + $types = []; + foreach ($node->types as $type) { + $types[] = $this->resolveType($type); + } + $node->types = $types; + return $node; + } + throw new UnexpectedValueException('Unknown node type: ' . get_class($node)); } } diff --git a/tests/Go/Instrument/Transformer/SelfValueTransformerTest.php b/tests/Go/Instrument/Transformer/SelfValueTransformerTest.php index 570805bb..8985f9af 100644 --- a/tests/Go/Instrument/Transformer/SelfValueTransformerTest.php +++ b/tests/Go/Instrument/Transformer/SelfValueTransformerTest.php @@ -99,5 +99,9 @@ public static function filesDataProvider(): \Generator __DIR__ . '/_files/php82-file.php', __DIR__ . '/_files/php82-file-transformed.php' ]; + yield 'anonymous-class.php' => [ + __DIR__ . '/_files/anonymous-class.php', + __DIR__ . '/_files/anonymous-class-transformed.php' + ]; } } diff --git a/tests/Go/Instrument/Transformer/_files/anonymous-class-transformed.php b/tests/Go/Instrument/Transformer/_files/anonymous-class-transformed.php new file mode 100644 index 00000000..cc7e61ff --- /dev/null +++ b/tests/Go/Instrument/Transformer/_files/anonymous-class-transformed.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection\Stub; + +class InAnonymousClass +{ + public function respond() + { + new class { + public const FOO = 'foo'; + + public function run() + { + return self::FOO; + } + }; + } +} diff --git a/tests/Go/Instrument/Transformer/_files/anonymous-class.php b/tests/Go/Instrument/Transformer/_files/anonymous-class.php new file mode 100644 index 00000000..cc7e61ff --- /dev/null +++ b/tests/Go/Instrument/Transformer/_files/anonymous-class.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection\Stub; + +class InAnonymousClass +{ + public function respond() + { + new class { + public const FOO = 'foo'; + + public function run() + { + return self::FOO; + } + }; + } +} diff --git a/tests/Go/Instrument/Transformer/_files/php80-file-transformed.php b/tests/Go/Instrument/Transformer/_files/php80-file-transformed.php new file mode 100644 index 00000000..294381d3 --- /dev/null +++ b/tests/Go/Instrument/Transformer/_files/php80-file-transformed.php @@ -0,0 +1,131 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection\Stub; + +use Attribute; +use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P}; + +class ClassWithPhp80Features +{ + public function acceptsStringArrayDefaultToNull(array|string $iterable = null) : array {} +} + +/** + * @see https://php.watch/versions/8.0/named-parameters + */ +class ClassWithPHP80NamedCall +{ + public static function foo(string $key1 = '', string $key2 = ''): string + { + return $key1 . ':' . $key2; + } + + public static function namedCall(): array + { + return [ + 'key1' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key1: 'bar'), + 'key2' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key2: 'baz'), + 'keys' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key1: 'A', key2: 'B'), + 'reverseKeys' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(key2: 'A', key1: 'B'), + 'unpack' => \Go\ParserReflection\Stub\ClassWithPHP80NamedCall::foo(...['key1' => 'C', 'key2' => 'D']), + ]; + } +} + +/** + * @see https://php.watch/versions/8.0/attributes + */ +#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)] +readonly class ClassPHP80Attribute +{ + private string $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public function getValue(): string + { + return $this->value; + } +} + +/** + * @see https://php.watch/versions/8.0/attributes + */ +#[ClassPHP80Attribute('class')] +class ClassPHP80WithAttribute +{ + #[ClassPHP80Attribute('first')] + #[ClassPHP80Attribute('second')] + public const PUBLIC_CONST = 1; + + #[ClassPHP80Attribute('property')] + private string $privateProperty = 'foo'; + + #[ClassPHP80Attribute('method')] + public function bar(#[ClassPHP80Attribute('parameter')] $parameter) + {} +} + +/** + * @see https://php.watch/versions/8.0/constructor-property-promotion + */ +class ClassPHP80WithPropertyPromotion +{ + public function __construct( + private string $privateStringValue, + private $privateNonTypedValue, + protected int $protectedIntValue = 42, + public array $publicArrayValue = [M_PI, M_E], + ) {} +} + +/** + * @see https://php.watch/versions/8.0/union-types + */ +class ClassWithPHP80UnionTypes +{ + public string|int|float|bool $scalarValue; + + public array|object|null $complexValueOrNull = null; + + /** + * Special case, internally iterable should be replaced with Traversable|array + */ + public iterable|object $iterableOrObject; + + public static function returnsUnionType(): object|array|null {} + + public static function acceptsUnionType(\stdClass|\Traversable|array $iterable): void {} +} + +/** + * @see https://php.watch/versions/8.0/mixed-type + */ +class ClassWithPHP80MixedType +{ + public mixed $someMixedPublicProperty; + + public static function returnsMixed(): mixed {} + + public static function acceptsMixed(mixed $value): void {} +} + +/** + * @see https://php.watch/versions/8.0/static-return-type + */ +class ClassWithPHP80StaticReturnType +{ + public static function create(): static {} +} \ No newline at end of file diff --git a/tests/Go/Instrument/Transformer/_files/php81-file-transformed.php b/tests/Go/Instrument/Transformer/_files/php81-file-transformed.php new file mode 100644 index 00000000..3d75adec --- /dev/null +++ b/tests/Go/Instrument/Transformer/_files/php81-file-transformed.php @@ -0,0 +1,119 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection\Stub; + +/** + * @see https://php.watch/versions/8.1/readonly + */ +class ClassWithPhp81ReadOnlyProperties +{ + public readonly int $publicReadonlyInt; + + protected readonly array $protectedReadonlyArray; + + private readonly object $privateReadonlyObject; +} + +/** + * @see https://php.watch/versions/8.1/enums + */ +enum SimplePhp81EnumWithSuit { + case Clubs; + case Diamonds; + case Hearts; + case Spades; +} + +/** + * @see https://php.watch/versions/8.1/enums#enums-backed + */ +enum BackedPhp81EnumHTTPMethods: string +{ + case GET = 'get'; + case POST = 'post'; +} + +/** + * @see https://php.watch/versions/8.1/enums#enum-methods + */ +enum BackedPhp81EnumHTTPStatusWithMethod: int +{ + case OK = 200; + case ACCESS_DENIED = 403; + case NOT_FOUND = 404; + + public function label(): string { + return static::getLabel($this); + } + + public static function getLabel(\Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod $value): string { + return match ($value) { + \Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::OK => 'OK', + \Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::ACCESS_DENIED => 'Access Denied', + \Go\ParserReflection\Stub\BackedPhp81EnumHTTPStatusWithMethod::NOT_FOUND => 'Page Not Found', + }; + } +} + +/** + * @see https://php.watch/versions/8.1/intersection-types + */ +class ClassWithPhp81IntersectionType implements \Countable +{ + private \Iterator&\Countable $countableIterator; + + public function __construct(\Iterator&\Countable $countableIterator) + { + $this->countableIterator = $countableIterator; + } + + public function count(): int + { + return count($this->countableIterator); + } +} + +/** + * @see https://php.watch/versions/8.1/intersection-types + */ +function functionWithPhp81IntersectionType(\Iterator&\Countable $value): \Iterator&\Countable { + foreach($value as $val) {} + count($value); + + return $value; +} + +/** + * @see https://php.watch/versions/8.1/never-return-type + */ +class ClassWithPhp81NeverReturnType +{ + public static function doThis(): never + { + throw new \RuntimeException('Not implemented'); + } +} + +/** + * @see https://php.watch/versions/8.1/never-return-type + */ +function functionWithPhp81NeverReturnType(): never +{ + throw new \RuntimeException('Not implemented'); +} + +/** + * @see https://php.watch/versions/8.1/final-class-const + */ +class ClassWithPhp81FinalClassConst { + final public const TEST = '1'; +} diff --git a/tests/Go/Instrument/Transformer/_files/php82-file-transformed.php b/tests/Go/Instrument/Transformer/_files/php82-file-transformed.php new file mode 100644 index 00000000..678ccc6f --- /dev/null +++ b/tests/Go/Instrument/Transformer/_files/php82-file-transformed.php @@ -0,0 +1,94 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection\Stub; + +/** + * @see https://php.watch/versions/8.2/readonly-classes + */ +readonly class ClassWithPhp82ReadOnlyFlag +{ + public int $publicInt; +} + +/** + * @see https://php.watch/versions/8.2/dnf-types + */ +class ClassWithPhp82DNFType +{ + private (JSONResponse&SuccessResponse)|HTMLResponse|string $respond; + + public function __construct((JSONResponse&SuccessResponse)|HTMLResponse|string $respond) + { + $this->respond = $respond; + } + + public function respond(): (JSONResponse&SuccessResponse)|HTMLResponse|string + { + return $this->respond; + } +} + +/** + * @see https://php.watch/versions/8.2/null-false-types + * @see https://php.watch/versions/8.2/true-type + */ +class ClassWithPhp82NullFalseTypes +{ + private true $isTrue = true; + private false $isFalse = false; + private null $isNull = null; + + public function returnsFalse(): false + { + return false; + } + + public function returnsTrue(): true + { + return true; + } + + public function returnsNullExplicitly(): null + { + return null; + } + + public function acceptsTrue(true $acceptsTrue): void {} + public function acceptsFalse(false $acceptsFalse): void {} + public function acceptsNull(null $acceptsNull): void {} +} + +/** + * @see https://php.watch/versions/8.2/constants-in-traits + */ +trait TraitWithPhp82Constant +{ + protected const CURRENT_VERSION = '2.6'; + final protected const MIN_VERSION = '2.5'; + + protected function ensureVersion(): void + { + if (self::CURRENT_VERSION < self::MIN_VERSION) { + throw new \Exception('Current version is too old'); + } + } +} + +class ClassWithPhp82SensitiveAttribute +{ + private string $secret; + + public function __construct(#[\SensitiveParameter] string $secret = 'password') + { + $this->secret = $secret; + } +} \ No newline at end of file