From d46d4a892fb57e6a439a686197ce188659e9b69f Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 11 Nov 2024 22:46:02 +0300 Subject: [PATCH 1/2] Apply Rector suggestions + Improve Rector CI (#288) --- .github/workflows/rector.yml | 3 ++- composer.json | 4 ++-- rector.php | 2 +- src/Dumper.php | 2 +- src/FlattenException.php | 11 +++++++++++ tests/Support/Stub/BrokenProxyImplementation.php | 2 +- tests/Unit/FlattenExceptionTest.php | 6 +++--- 7 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 35411d0ae..457772afa 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -1,5 +1,5 @@ on: - pull_request: + pull_request_target: paths-ignore: - 'docs/**' - 'README.md' @@ -17,6 +17,7 @@ jobs: secrets: token: ${{ secrets.YIISOFT_GITHUB_TOKEN }} with: + repository: ${{ github.event.pull_request.head.repo.full_name }} os: >- ['ubuntu-latest'] php: >- diff --git a/composer.json b/composer.json index 8537a5c3d..a6d2a54b4 100644 --- a/composer.json +++ b/composer.json @@ -55,10 +55,10 @@ "maglnet/composer-require-checker": "^4.2", "nyholm/psr7": "^1.3", "phpunit/phpunit": "^10.5", - "rector/rector": "^1.0.0", + "rector/rector": "^1.2", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", - "vimeo/psalm": "^5.25", + "vimeo/psalm": "^5.26", "yiisoft/error-handler": "^3.0", "yiisoft/event-dispatcher": "^1.0", "yiisoft/log": "^2.0", diff --git a/rector.php b/rector.php index 5548304b4..582cc4ad5 100644 --- a/rector.php +++ b/rector.php @@ -17,7 +17,7 @@ // define sets of rules $rectorConfig->sets([ - LevelSetList::UP_TO_PHP_80, + LevelSetList::UP_TO_PHP_81, ]); $rectorConfig->skip([ diff --git a/src/Dumper.php b/src/Dumper.php index 0fdaee87f..e76b1aca3 100644 --- a/src/Dumper.php +++ b/src/Dumper.php @@ -14,7 +14,7 @@ final class Dumper private array $objects = []; private static ?ClosureExporter $closureExporter = null; - private array $excludedClasses; + private readonly array $excludedClasses; private function __construct( private readonly mixed $variable, diff --git a/src/FlattenException.php b/src/FlattenException.php index 3e63a5331..3e6ab1c65 100644 --- a/src/FlattenException.php +++ b/src/FlattenException.php @@ -163,6 +163,17 @@ public function getTrace(): array /** * @param array $trace the Exception stack trace as an array. + * + * @psalm-param list< + * array{ + * args?: array, + * class?: class-string, + * file?: string, + * function?: string, + * line?: int, + * type?: string + * } + * > $trace */ private function setTrace(array $trace): void { diff --git a/tests/Support/Stub/BrokenProxyImplementation.php b/tests/Support/Stub/BrokenProxyImplementation.php index e1490f462..526bebd2d 100644 --- a/tests/Support/Stub/BrokenProxyImplementation.php +++ b/tests/Support/Stub/BrokenProxyImplementation.php @@ -8,7 +8,7 @@ class BrokenProxyImplementation implements Interface1 { - public function __construct(private Interface1 $decorated) + public function __construct(private readonly Interface1 $decorated) { throw new Exception('Broken proxy'); } diff --git a/tests/Unit/FlattenExceptionTest.php b/tests/Unit/FlattenExceptionTest.php index 63329ec2c..aa852442f 100644 --- a/tests/Unit/FlattenExceptionTest.php +++ b/tests/Unit/FlattenExceptionTest.php @@ -78,7 +78,7 @@ public function testClass(): void $this->assertEquals('Exception', (new FlattenException(new Exception()))->getClass()); } - public function testArguments(): void + public function testArguments(): never { $this->markTestSkipped('Should be fixed'); @@ -151,7 +151,7 @@ public function testClosureSerialize(): void $this->assertStringContainsString(Closure::class, serialize($flattened)); } - public function testRecursionInArguments(): void + public function testRecursionInArguments(): never { $this->markTestSkipped('Should be fixed'); @@ -164,7 +164,7 @@ public function testRecursionInArguments(): void $this->assertStringContainsString('*DEEP NESTED ARRAY*', serialize($trace)); } - public function testTooBigArray(): void + public function testTooBigArray(): never { $this->markTestSkipped('Should be fixed'); From b532c445ba09d52dfbe878303b00b9cfc8b7acc1 Mon Sep 17 00:00:00 2001 From: olegbaturin Date: Sun, 24 Nov 2024 00:55:10 +0700 Subject: [PATCH 2/2] 282 discover all nested objects (#287) * add tests * update dumper * fix styles * improve + tests * fix phpdoc * fix * fix --------- Co-authored-by: Sergei Predvoditelev --- src/Dumper.php | 48 +++---- tests/Unit/DumperTest.php | 255 +++++++++++++++++++++++++++++++++++++- 2 files changed, 280 insertions(+), 23 deletions(-) diff --git a/src/Dumper.php b/src/Dumper.php index e76b1aca3..17c882386 100644 --- a/src/Dumper.php +++ b/src/Dumper.php @@ -9,6 +9,10 @@ use ReflectionException; use Yiisoft\VarDumper\ClosureExporter; +use function array_key_exists; +use function is_array; +use function is_object; + final class Dumper { private array $objects = []; @@ -47,23 +51,21 @@ public function asJson(int $depth = 50, bool $format = false): string /** * Export variable as JSON summary of topmost items. + * Dumper goes into the variable full depth to search all objects. * - * @param int $depth Maximum depth that the dumper should go into the variable. + * @param int $depth Maximum depth that the dumper should print out arrays. * @param bool $prettyPrint Whatever to format exported code. * * @return string JSON string containing summary. */ public function asJsonObjectsMap(int $depth = 50, bool $prettyPrint = false): string { - $this->buildObjectsCache($this->variable, $depth); - return $this->asJsonInternal($this->objects, $prettyPrint, $depth, 1, true); + $this->buildObjectsCache($this->variable); + return $this->asJsonInternal($this->objects, $prettyPrint, $depth + 2, 1, true); } - private function buildObjectsCache(mixed $variable, int $depth, int $level = 0): void + private function buildObjectsCache(mixed $variable, ?int $depth = null, int $level = 0): void { - if ($depth <= $level) { - return; - } if (is_object($variable)) { if (array_key_exists($variable::class, $this->excludedClasses) || array_key_exists($objectDescription = $this->getObjectDescription($variable), $this->objects) @@ -71,18 +73,22 @@ private function buildObjectsCache(mixed $variable, int $depth, int $level = 0): return; } $this->objects[$objectDescription] = $variable; - $variable = $this->getObjectProperties($variable); + } + $nextLevel = $level + 1; + if ($depth !== null && $depth <= $nextLevel) { + return; + } + + if (is_object($variable)) { + $variable = $this->getObjectProperties($variable); foreach ($variable as $value) { - $this->buildObjectsCache($value, $depth, 0); + $this->buildObjectsCache($value, $depth, $nextLevel); } return; } + if (is_array($variable)) { - $nextLevel = $level + 1; - if ($depth <= $nextLevel) { - return; - } foreach ($variable as $value) { $this->buildObjectsCache($value, $depth, $nextLevel); } @@ -149,10 +155,6 @@ private function dumpNestedInternal( break; case 'object': $objectDescription = $this->getObjectDescription($variable); - if ($depth <= $level || array_key_exists($variable::class, $this->excludedClasses)) { - $output = $objectDescription . ' (...)'; - break; - } if ($variable instanceof Closure) { $output = $inlineObject @@ -161,13 +163,17 @@ private function dumpNestedInternal( break; } - if (!array_key_exists($objectDescription, $this->objects)) { + if ($objectCollapseLevel < $level && array_key_exists($objectDescription, $this->objects)) { $output = 'object@' . $objectDescription; - $this->objects[$objectDescription] = $variable; break; } - if ($objectCollapseLevel < $level) { - $output = 'object@' . $objectDescription; + + if ( + $depth <= $level + || array_key_exists($variable::class, $this->excludedClasses) + || !array_key_exists($objectDescription, $this->objects) + ) { + $output = $objectDescription . ' (...)'; break; } diff --git a/tests/Unit/DumperTest.php b/tests/Unit/DumperTest.php index 85c81f25f..05fd8e310 100644 --- a/tests/Unit/DumperTest.php +++ b/tests/Unit/DumperTest.php @@ -19,6 +19,256 @@ final class DumperTest extends TestCase { + public function testAsJsonObjectsMapLevelOne(): void + { + $object = new stdClass(); + $object->var = 'test'; + $objectId = spl_object_id($object); + + $this->assertSame( + <<asJsonObjectsMap(1, true) + ); + } + + public function testAsJsonObjectsMapNestedObject(): void + { + $nested2 = new stdClass(); + $nested2->name = 'nested2'; + $nested2Id = spl_object_id($nested2); + + $nested1 = new stdClass(); + $nested1->name = 'nested1'; + $nested1->var = $nested2; + $nested1Id = spl_object_id($nested1); + + $object = new stdClass(); + $object->name = 'root'; + $object->var = $nested1; + $objectId = spl_object_id($object); + + $this->assertSame( + <<asJsonObjectsMap(1, true) + ); + } + + public function testAsJsonObjectsMapArrayWithObject(): void + { + $nested2 = new stdClass(); + $nested2->name = 'nested2'; + $nested2Id = spl_object_id($nested2); + + $nested1 = new stdClass(); + $nested1->name = 'nested1'; + $nested1->var = [$nested2]; + $nested1Id = spl_object_id($nested1); + + $object = new stdClass(); + $object->name = 'root'; + $object->var = $nested1; + $objectId = spl_object_id($object); + + $this->assertSame( + <<asJsonObjectsMap(0, true) + ); + } + + /** + * @dataProvider loopAsJsonObjectMapDataProvider + */ + public function testLoopAsJsonObjectsMap(mixed $var, int $depth, $expectedResult): void + { + $exportResult = Dumper::create($var)->asJsonObjectsMap($depth, true); + $this->assertEquals($expectedResult, $exportResult); + } + + public static function loopAsJsonObjectMapDataProvider(): iterable + { + // parent->child->parent structure + $nested1 = new stdClass(); + $nested1->id = 'nested1'; + $nested2 = new stdClass(); + $nested2->id = 'nested2'; + $nested2->nested1 = $nested1; + $nested1->nested2 = $nested2; + + $nested1Id = spl_object_id($nested1); + $nested2Id = spl_object_id($nested2); + + // 5 is a min level to reproduce buggy dumping of parent->child->parent structure + [$object1, $ids1] = self::getNested(5, $nested1); + yield 'nested loop - object' => [ + $object1, + 5, + << [ + $object2, + 6, + <<id = 'lvl0'; + $object3->lv11 = [ + 'id' => 'lvl1', + 'loop' => $nested1, + ]; + $object3Id = spl_object_id($object3); + + yield 'nested loop to object->array' => [ + $object3, + 3, + <<id = 'lvl0'; + + for ($i = 1; $i < $depth; $i++) { + $nested = new stdClass(); + $nested->id = 'lvl' . $i; + $lvl->{'lvl' . $i} = $nested; + $lvl = $nested; + $objectIds[] = spl_object_id($nested); + } + $lvl->{'lvl' . $i} = $data; + + return [$head, $objectIds]; + } + public function testObjectExpanding(): void { $var = $this->createNested(10, [[[[[[[[['key' => 'end']]]]]]]]]); @@ -93,7 +343,7 @@ public function testObjectExpanding(): void } JSON; - $actualResult = Dumper::create($var)->asJsonObjectsMap(4, true); + $actualResult = Dumper::create($var)->asJsonObjectsMap(2, true); $this->assertEquals($expectedResult, $actualResult); } @@ -178,6 +428,7 @@ public function testCacheDoesNotCoversObjectOutOfDumpDepth(): void $object1 = new stdClass(); $object1Id = spl_object_id($object1); $object2 = new stdClass(); + $object2Id = spl_object_id($object2); $variable = [$object1, [[$object2]]]; $expectedResult = sprintf('["object@stdClass#%d",["array (1 item) [...]"]]', $object1Id); @@ -189,7 +440,7 @@ public function testCacheDoesNotCoversObjectOutOfDumpDepth(): void $map = $dumper->asJsonObjectsMap(2); $this->assertEqualsWithoutLE( <<