From 629ff1b356f53742f6593969cd0fe992d2a7c981 Mon Sep 17 00:00:00 2001 From: marshmallowfox Date: Mon, 2 Jun 2025 11:52:22 +0600 Subject: [PATCH 1/5] improvements for handle of php 8.4 --- src/formatReflectedFunction.fn.php | 36 ++++++++++++++--------------- src/formatReflectedParameter.fn.php | 4 +++- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/formatReflectedFunction.fn.php b/src/formatReflectedFunction.fn.php index 6f4d0f0..a6e43fd 100644 --- a/src/formatReflectedFunction.fn.php +++ b/src/formatReflectedFunction.fn.php @@ -10,35 +10,35 @@ */ function formatReflectedFunction(\ReflectionFunctionAbstract $function): string { - if ($function instanceof \ReflectionMethod) { - return \sprintf('%s::%s()', formatClass($function->class), $function->name); - } + if (str_contains($function->getName(), '{closure')) { + $file = $function->getFileName(); - if (!str_starts_with($function->name, '{closure')) { - $class = $function->getClosureCalledClass(); + if ($file === false) { + return 'function()'; + } - if ($class !== null) { - return \sprintf('%s::%s()', formatReflectedClass($class), $function->name); + if (preg_match('/^(.*)\((\d+)\)/', $file, $matches)) { + return \sprintf('function@%s:%d()', $matches[1], $matches[2]); } - return $function->name . '()'; - } + $line = $function->getStartLine(); - $file = $function->getFileName(); + if ($line === false) { + return \sprintf('function@%s()', $file); + } - if ($file === false) { - return 'function()'; + return \sprintf('function@%s:%d()', $file, $line); } - if (preg_match('/^(.*)\((\d+)\)/', $file, $matches)) { - return \sprintf('function@%s:%d()', $matches[1], $matches[2]); + if ($function instanceof \ReflectionMethod) { + return \sprintf('%s::%s()', formatClass($function->class), $function->getName()); } - $line = $function->getStartLine(); + $class = $function->getClosureCalledClass(); - if ($line === false) { - return \sprintf('function@%s()', $file); + if ($class !== null) { + return \sprintf('%s::%s()', formatReflectedClass($class), $function->getName()); } - return \sprintf('function@%s:%d()', $file, $line); + return $function->name . '()'; } diff --git a/src/formatReflectedParameter.fn.php b/src/formatReflectedParameter.fn.php index 303b939..3f98c67 100644 --- a/src/formatReflectedParameter.fn.php +++ b/src/formatReflectedParameter.fn.php @@ -10,5 +10,7 @@ */ function formatReflectedParameter(\ReflectionParameter $parameter): string { - return \sprintf('%s($%s)', formatReflectedFunction($parameter->getDeclaringFunction()), $parameter->name); + $formattedFunction = formatReflectedFunction($parameter->getDeclaringFunction()); + + return \sprintf('%s$%s)', substr($formattedFunction, 0, -1), $parameter->getName()); } From fdb7b24c8bfc1ba577fd9631c71dfbdb65db8f29 Mon Sep 17 00:00:00 2001 From: marshmallowfox Date: Mon, 2 Jun 2025 17:51:04 +0600 Subject: [PATCH 2/5] tests --- tests/.gitignore | 0 tests/FormatClassTest.php | 61 +++++++++++++++++ tests/FormatFunctionTest.php | 69 +++++++++++++++++++ tests/FormatParameterTest.php | 23 +++++++ tests/FormatPropertyTest.php | 25 +++++++ tests/FormatReflectedClassTest.php | 24 +++++++ tests/FormatReflectedFunctionTest.php | 93 ++++++++++++++++++++++++++ tests/FormatReflectedParameterTest.php | 24 +++++++ tests/FormatReflectedPropertyTest.php | 22 ++++++ tests/FormatReflectedTypeTest.php | 46 +++++++++++++ tests/FormatTest.php | 58 ++++++++++++++++ 11 files changed, 445 insertions(+) delete mode 100644 tests/.gitignore create mode 100644 tests/FormatClassTest.php create mode 100644 tests/FormatFunctionTest.php create mode 100644 tests/FormatParameterTest.php create mode 100644 tests/FormatPropertyTest.php create mode 100644 tests/FormatReflectedClassTest.php create mode 100644 tests/FormatReflectedFunctionTest.php create mode 100644 tests/FormatReflectedParameterTest.php create mode 100644 tests/FormatReflectedPropertyTest.php create mode 100644 tests/FormatReflectedTypeTest.php create mode 100644 tests/FormatTest.php diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/tests/FormatClassTest.php b/tests/FormatClassTest.php new file mode 100644 index 0000000..3b33e61 --- /dev/null +++ b/tests/FormatClassTest.php @@ -0,0 +1,61 @@ + + */ + public static function cases(): \Generator + { + yield 'from object' => [new \stdClass(), \stdClass::class]; + yield 'from class-string' => [\stdClass::class, \stdClass::class]; + yield 'from anonymous object' => [new class {}, \sprintf('class@%s:%d', __FILE__, __LINE__)]; + yield 'from extended class' => [new class extends \ArrayObject {}, \sprintf('ArrayObject@%s:%d', __FILE__, __LINE__)]; + yield 'from implemented class' => [ + new class implements \IteratorAggregate { + public function getIterator(): \Traversable + { + return new \ArrayIterator(); + } + }, + \sprintf('IteratorAggregate@%s:%d', __FILE__, __LINE__ - 6), + ]; + yield 'from eval object' => [(object) eval('return new \stdClass();'), \stdClass::class]; + yield 'from eval anonymous object' => [(object) eval('return new class {};'), \sprintf('class@%s:%d', __FILE__, __LINE__)]; + } + + /** + * @param class-string|object $class + * @param non-empty-string $expectedFormattedClass + */ + #[DataProvider('cases')] + public function testFormatClass(string|object $class, string $expectedFormattedClass): void + { + $formatted = formatClass($class); + + self::assertSame($expectedFormattedClass, $formatted); + } + + /** + * @param class-string|object $class + * @param non-empty-string $expectedFormattedClass + * @throws \ReflectionException + */ + #[DataProvider('cases')] + public function testFormatReflectedClass(string|object $class, string $expectedFormattedClass): void + { + $formatted = formatReflectedClass(new \ReflectionClass($class)); + + self::assertSame($expectedFormattedClass, $formatted); + } +} diff --git a/tests/FormatFunctionTest.php b/tests/FormatFunctionTest.php new file mode 100644 index 0000000..d0d7dca --- /dev/null +++ b/tests/FormatFunctionTest.php @@ -0,0 +1,69 @@ + + */ + public static function cases(): \Generator + { + yield 'brackets to string with ::' => [ + \sprintf('%s::cases', self::class), + \sprintf('%s::cases()', self::class), + ]; + yield 'brackets to string' => [ + 'strlen', + 'strlen()', + ]; + yield 'from array' => [ + [ + new class { + public function foo(): void {} + }, + 'foo', + ], + \sprintf('class@%s:%d::foo()', __FILE__, __LINE__ - 5), + ]; + yield 'from closure' => [ + static function (): void {}, + \sprintf('function@%s:%d()', __FILE__, __LINE__ - 1), + ]; + yield 'from invokable object' => [ + new class { + public function __invoke(): void {} + }, + \sprintf('class@%s:%d()', __FILE__, __LINE__ - 3), + ]; + yield 'from eval closure' => [ + (static function (): callable { + /** + * @var callable $closure + */ + $closure = eval('return function() {};'); + + return $closure; + })(), + \sprintf('function@%s:%d()', __FILE__, __LINE__ - 4), + ]; + } + + /** + * @param callable $closure + */ + #[DataProvider('cases')] + public function testFormatFunction(mixed $closure, string $expectedFormattedFunction): void + { + $formatted = formatFunction($closure); + + self::assertSame($expectedFormattedFunction, $formatted); + } +} diff --git a/tests/FormatParameterTest.php b/tests/FormatParameterTest.php new file mode 100644 index 0000000..930fefc --- /dev/null +++ b/tests/FormatParameterTest.php @@ -0,0 +1,23 @@ + + * @throws \ReflectionException + */ + public static function cases(): \Generator + { + yield 'from object method' => [ + new \ReflectionMethod(new self('name'), 'testFormatReflectedFunction'), + self::class . '::testFormatReflectedFunction()', + ]; + yield 'from class method' => [ + new \ReflectionMethod(new self('name'), 'cases'), + self::class . '::cases()', + ]; + yield 'from named method' => [new \ReflectionFunction('trim'), 'trim()']; + yield 'from closure method' => [ + new \ReflectionFunction((new self('name'))->testFormatReflectedFunction(...)), + \sprintf('%s::testFormatReflectedFunction()', __CLASS__), + ]; + yield 'from eval closure method' => [ + (static function (): \ReflectionFunction { + /** + * @var \Closure $closure + */ + $closure = eval('return function() {};'); + + return new \ReflectionFunction($closure); + })(), + \sprintf('function@%s:%d()', __FILE__, __LINE__ - 4), + ]; + } + + /** + * @param non-empty-string $expectedFormattedFunction + */ + #[DataProvider('cases')] + public function testFormatReflectedFunction(\ReflectionFunctionAbstract $function, string $expectedFormattedFunction): void + { + $formatted = formatReflectedFunction($function); + + self::assertSame($expectedFormattedFunction, $formatted); + } + + public function testFormatReflectedFunctionWhenFileNameIsUnavailable(): void + { + $mock = $this->createMock(\ReflectionFunction::class); + $mock->method('getName')->willReturn('{closure}'); + $mock->method('getFileName')->willReturn(false); + + $formatted = formatReflectedFunction($mock); + + self::assertSame('function()', $formatted); + } + + public function testFormatReflectedFunctionWhenStartLineIsUnavailable(): void + { + $mock = $this->createMock(\ReflectionFunction::class); + $mock->method('getName')->willReturn('{closure}'); + $mock->method('getFileName')->willReturn('Path'); + $mock->method('getStartLine')->willReturn(false); + $expectedFormattedFunction = \sprintf('function@%s()', $mock->getFileName()); + + $formatted = formatReflectedFunction($mock); + + self::assertSame($expectedFormattedFunction, $formatted); + } + + public function testFormatReflectedFunctionWhenAllAvailable(): void + { + $mock = $this->createMock(\ReflectionFunction::class); + $mock->method('getName')->willReturn('{closure}'); + $mock->method('getFileName')->willReturn('Path'); + $mock->method('getStartLine')->willReturn(5); + $expectedFormattedFunction = \sprintf('function@%s:%d()', $mock->getFileName(), $mock->getStartLine()); + + $formatted = formatReflectedFunction($mock); + + self::assertSame($expectedFormattedFunction, $formatted); + } +} diff --git a/tests/FormatReflectedParameterTest.php b/tests/FormatReflectedParameterTest.php new file mode 100644 index 0000000..8714331 --- /dev/null +++ b/tests/FormatReflectedParameterTest.php @@ -0,0 +1,24 @@ + + * @throws \ReflectionException + */ + public static function cases(): \Generator + { + yield 'from null' => [null, '']; + yield 'from exact type' => [ + (new \ReflectionFunction(static fn(int $a) => null))->getParameters()[0]->getType(), + 'int', + ]; + yield 'from nullable type' => [ + (new \ReflectionFunction(static fn(?int $a) => null))->getParameters()[0]->getType(), + '?int', + ]; + yield 'from union type' => [ + (new \ReflectionFunction(static fn(string|int $a) => null))->getParameters()[0]->getType(), + 'string|int', + ]; + yield 'from intersection type' => [ + (new \ReflectionFunction(static fn(\IteratorAggregate&\Iterator $a) => null))->getParameters()[0]->getType(), + 'IteratorAggregate&Iterator', + ]; + } + + #[DataProvider('cases')] + public function testFormatReflectedType(?\ReflectionType $type, string $expectedFormattedReflectionType): void + { + $formatted = formatReflectedType($type); + + self::assertSame($expectedFormattedReflectionType, $formatted); + } +} diff --git a/tests/FormatTest.php b/tests/FormatTest.php new file mode 100644 index 0000000..70a57ed --- /dev/null +++ b/tests/FormatTest.php @@ -0,0 +1,58 @@ + + */ + public static function cases(): \Generator + { + yield 'from null' => [null, 'null']; + yield 'from false' => [true, 'true']; + yield 'from true' => [false, 'false']; + yield 'from int' => [127, '127']; + yield 'from float' => [1.27, '1.27']; + yield 'from string' => ['formatter', "'formatter'"]; + yield 'from empty list' => [[], 'list{}']; + yield 'from list' => [[1, 2, 3], 'list{1, 2, 3}']; + yield 'from array' => [['a' => 1, 'b' => 2], 'array{a: 1, b: 2}']; + yield 'from array with quota key' => [["'" => 1], "array{'\\'': 1}"]; + yield 'from stdObject' => [(static function (): \stdClass { + $object = new \stdClass(); + $object->x = 10; + $object->y = 20; + + return $object; + })(), 'object{x: 10, y: 20}']; + yield 'from enum' => [Color::RED, 'Typhoon\Formatter\Color::RED']; + yield 'from closure' => [static function (): void {}, \sprintf('function@%s:%d()', __FILE__, __LINE__)]; + yield 'from object' => [new class {}, \sprintf('class@%s:%d', __FILE__, __LINE__)]; + yield 'from resource' => [fopen('php://memory', 'r'), 'resource']; + } + + #[DataProvider('cases')] + public function testFormat(mixed $value, string $expectedFormattedValue): void + { + $formatted = format($value); + + self::assertSame($expectedFormattedValue, $formatted); + + if (\is_resource($value)) { + fclose($value); + } + } +} From 69bea9b1d7e75124f2adc2a114dc31e3d53cf8d3 Mon Sep 17 00:00:00 2001 From: marshmallowfox Date: Mon, 2 Jun 2025 17:51:04 +0600 Subject: [PATCH 3/5] tests --- tests/FormatReflectedClassTest.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/FormatReflectedClassTest.php b/tests/FormatReflectedClassTest.php index 8418d17..f5d29c6 100644 --- a/tests/FormatReflectedClassTest.php +++ b/tests/FormatReflectedClassTest.php @@ -7,18 +7,15 @@ use PHPUnit\Framework\Attributes\CoversFunction; use PHPUnit\Framework\TestCase; -#[CoversFunction('Typhoon\Formatter\formatProperty')] -#[CoversFunction('Typhoon\Formatter\formatReflectedProperty')] -final class FormatPropertyTest extends TestCase +#[CoversFunction('Typhoon\Formatter\formatReflectedClass')] +final class FormatReflectedClassTest extends TestCase { - public function testFormatProperty(): void + public function testFormatReflectedClass(): void { - $expectedFormattedProperty = \sprintf('class@%s:%d::$x', __FILE__, __LINE__ + 2); + $expectedFormattedClass = formatReflectedClass(new \ReflectionClass(\ReflectionClass::class)); - $formatted = formatProperty(new class { - public int $x = 0; - }, 'x'); + $formatted = formatClass(\ReflectionClass::class); - self::assertSame($formatted, $expectedFormattedProperty); + self::assertSame($formatted, $expectedFormattedClass); } -} +} \ No newline at end of file From ae1ce94618f910ddf9e0245e569668033aeaa474 Mon Sep 17 00:00:00 2001 From: MarshmallowFox Date: Mon, 2 Jun 2025 21:34:04 +0500 Subject: [PATCH 4/5] disable infection in CI --- .github/workflows/check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6a2b684..f830f8a 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -8,3 +8,5 @@ jobs: check: uses: typhoon-php/.github/.github/workflows/check.yml@main secrets: inherit + with: + infection: false From 60d07aef8ddff6fb1382cd505fa702c2caa05647 Mon Sep 17 00:00:00 2001 From: MarshmallowFox Date: Mon, 2 Jun 2025 21:53:35 +0500 Subject: [PATCH 5/5] CI & rector --- tests/FormatParameterTest.php | 3 +-- tests/FormatReflectedClassTest.php | 2 +- tests/FormatReflectedFunctionTest.php | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/FormatParameterTest.php b/tests/FormatParameterTest.php index 930fefc..0adef25 100644 --- a/tests/FormatParameterTest.php +++ b/tests/FormatParameterTest.php @@ -15,8 +15,7 @@ public function testFormatParameter(): void { $expectedFormattedParameter = \sprintf('function@%s:%s($x)', __FILE__, __LINE__ + 2); - $formatted = formatParameter(static function (int $x): void { - }, 'x'); + $formatted = formatParameter(static function (int $x): void {}, 'x'); self::assertSame($expectedFormattedParameter, $formatted); } diff --git a/tests/FormatReflectedClassTest.php b/tests/FormatReflectedClassTest.php index f5d29c6..16d5748 100644 --- a/tests/FormatReflectedClassTest.php +++ b/tests/FormatReflectedClassTest.php @@ -18,4 +18,4 @@ public function testFormatReflectedClass(): void self::assertSame($formatted, $expectedFormattedClass); } -} \ No newline at end of file +} diff --git a/tests/FormatReflectedFunctionTest.php b/tests/FormatReflectedFunctionTest.php index d1e165b..219488b 100644 --- a/tests/FormatReflectedFunctionTest.php +++ b/tests/FormatReflectedFunctionTest.php @@ -28,7 +28,7 @@ public static function cases(): \Generator yield 'from named method' => [new \ReflectionFunction('trim'), 'trim()']; yield 'from closure method' => [ new \ReflectionFunction((new self('name'))->testFormatReflectedFunction(...)), - \sprintf('%s::testFormatReflectedFunction()', __CLASS__), + \sprintf('%s::testFormatReflectedFunction()', self::class), ]; yield 'from eval closure method' => [ (static function (): \ReflectionFunction {