From 330090caed73de5a498cf84ebdea2773992201b0 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Wed, 20 May 2026 22:52:11 +0200 Subject: [PATCH 01/11] Add cache integration test --- phpunit.xml.dist | 1 - tests/Integration/CacheTest.php | 73 ++++++++++++++++++++++++++++++++ tests/fixtures/simple-config.php | 28 ++++++++++++ tests/fixtures/simple-input.php | 10 +++++ tests/fixtures/simple-output.php | 12 ++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/CacheTest.php create mode 100644 tests/fixtures/simple-config.php create mode 100644 tests/fixtures/simple-input.php create mode 100644 tests/fixtures/simple-output.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e1bc7ee..82f9318 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,6 @@ $fixerVersion] + ); + + $process->mustRun(); + + $output = $process->getErrorOutput() . $process->getOutput(); + + $this->assertFileEquals(__DIR__ . '/../fixtures/simple-output.php', self::$inputFile); + $this->assertStringContainsString($expectedOutput, $output); + } +} diff --git a/tests/fixtures/simple-config.php b/tests/fixtures/simple-config.php new file mode 100644 index 0000000..2445b73 --- /dev/null +++ b/tests/fixtures/simple-config.php @@ -0,0 +1,28 @@ +registerCustomFixers([new BlockStringFixer()]) + ->setRiskyAllowed(true) + ->setRules([ + BlockStringFixer::NAME => [ + 'formatters' => [ + 'JSON' => new class extends Formatter\AbstractStringFormatter { + public function __construct() + { + parent::__construct(getenv('TEST_FIXER_VERSION')); + } + + public function formatContent(string $original): string + { + return json_encode( + json_decode($original, false, 512, JSON_THROW_ON_ERROR), + JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + ); + } + }, + ], + ], + ]); diff --git a/tests/fixtures/simple-input.php b/tests/fixtures/simple-input.php new file mode 100644 index 0000000..64f74cd --- /dev/null +++ b/tests/fixtures/simple-input.php @@ -0,0 +1,10 @@ + Date: Thu, 21 May 2026 17:07:03 +0200 Subject: [PATCH 02/11] Minor improvements --- tests/Integration/CacheTest.php | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/Integration/CacheTest.php b/tests/Integration/CacheTest.php index 25a32dc..f09d3e5 100644 --- a/tests/Integration/CacheTest.php +++ b/tests/Integration/CacheTest.php @@ -26,11 +26,6 @@ public static function setUpBeforeClass(): void copy(__DIR__ . '/../fixtures/simple-input.php', self::$inputFile); } - protected function setUp(): void - { - touch(self::$inputFile); - } - public static function tearDownAfterClass(): void { @unlink(self::$inputFile); @@ -39,13 +34,19 @@ public static function tearDownAfterClass(): void } /** - * @testWith ["v1.0", "Fixed 1 of 1 files"] - * ["v1.0", "Fixed 0 of 1 files"] - * ["v1.1", "Fixed 1 of 1 files"] - * ["v1.1", "Fixed 0 of 1 files"] + * @testWith ["v1.0", "Fixed 1 of 1 files", "Cache file did not exist"] + * ["v1.0", "Fixed 0 of 1 files", "Cache file existed already"] + * ["v1.1", "Fixed 1 of 1 files", "Cache file existed already"] + * ["v1.1", "Fixed 0 of 1 files", "Cache file existed already"] */ - public function testCacheReuse(string $fixerVersion, string $expectedOutput): void - { + public function testCacheReuse( + string $fixerVersion, + string $expectedProcessOutput, + string $expectedCacheFileExistence + ): void { + $cacheFileExistence = file_exists(self::$cacheFile) + ? 'Cache file existed already' + : 'Cache file did not exist'; $process = new Process( [ 'php', @@ -67,7 +68,8 @@ public function testCacheReuse(string $fixerVersion, string $expectedOutput): vo $output = $process->getErrorOutput() . $process->getOutput(); + $this->assertSame($expectedCacheFileExistence, $cacheFileExistence); $this->assertFileEquals(__DIR__ . '/../fixtures/simple-output.php', self::$inputFile); - $this->assertStringContainsString($expectedOutput, $output); + $this->assertStringContainsString($expectedProcessOutput, $output); } } From 283a2d133dcfd76733e2d3cc5fc5ad6226780e66 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Thu, 21 May 2026 20:32:26 +0200 Subject: [PATCH 03/11] Switch to cache fingerprint concept; add cache test --- README.md | 6 +- composer.json | 2 +- src/CacheFingerprintableInterface.php | 14 +++++ src/Formatter/AbstractCodecFormatter.php | 12 ---- src/Formatter/AbstractFormatter.php | 38 ++++++++++-- src/Formatter/AbstractStringFormatter.php | 17 ++++-- src/Formatter/ChainFormatter.php | 7 ++- src/Formatter/CliPipeFormatter.php | 13 +++- src/Formatter/DockerPipeFormatter.php | 13 +++- src/Formatter/SimpleLineFormatter.php | 2 +- src/InterpolationCodec/CodecInterface.php | 3 +- .../GeneratedTokenCodec.php | 26 ++++++++ src/InterpolationCodec/PlainStringCodec.php | 6 +- .../DefaultNormalizer.php | 5 ++ .../NormalizerInterface.php | 4 +- tests/Integration/CacheTest.php | 60 +++++++++++++++++-- tests/fixtures/example-config.php | 2 +- tests/fixtures/simple-cache.json | 19 ++++++ tests/fixtures/simple-config.php | 2 +- 19 files changed, 204 insertions(+), 47 deletions(-) create mode 100644 src/CacheFingerprintableInterface.php delete mode 100644 src/Formatter/AbstractCodecFormatter.php create mode 100644 tests/fixtures/simple-cache.json diff --git a/README.md b/README.md index a81969a..0b43b16 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ return (new PhpCsFixer\Config()) new class extends Formatter\AbstractStringFormatter { public function __construct() { - parent::__construct('1.0', new GeneratedTokenCodec('"__PHP_VAR_%d__"')); + parent::__construct('ObjectKeySorter v1.0', new GeneratedTokenCodec('"__PHP_VAR_%d__"')); } public function formatContent(string $original): string @@ -326,10 +326,6 @@ Extending this class makes sense in two situations: 2. Or if, for whatever reason, the [`CodecInterface`] concept does not work for you and you want to write something from scratch. -### [AbstractCodecFormatter](./src/Formatter/AbstractCodecFormatter.php) - -_Deprecated in favour of [`AbstractStringFormatter`]._ - ### [AbstractStringFormatter](./src/Formatter/AbstractStringFormatter.php) This formatter base class is aware of string interpolation - it passes content through a codec before and after diff --git a/composer.json b/composer.json index a10dc6d..a6ef666 100644 --- a/composer.json +++ b/composer.json @@ -22,12 +22,12 @@ ], "require": { "php": "^7.4 || ^8.0", + "ext-json": "*", "symfony/process": "^5 || ^6 || ^7 || ^8", "friendsofphp/php-cs-fixer": "^3", "symfony/deprecation-contracts": "^2 || ^3" }, "require-dev": { - "ext-json": "*", "ergebnis/composer-normalize": "^2.7", "phpunit/phpunit": "^9.5", "phpstan/phpstan": "^2.1", diff --git a/src/CacheFingerprintableInterface.php b/src/CacheFingerprintableInterface.php new file mode 100644 index 0000000..2c3b34d --- /dev/null +++ b/src/CacheFingerprintableInterface.php @@ -0,0 +1,14 @@ +version = $version; + $this->cacheFingerprint = $cacheFingerprint; } /** * Format the provided BlockString accordingly and return a new one. */ abstract public function formatBlock(BlockString $blockString): BlockString; + + /** + * @return mixed + */ + final public function getCacheFingerprint() + { + return $this->cacheFingerprint; + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + final public function jsonSerialize() + { + return $this->getCacheFingerprint(); + } } diff --git a/src/Formatter/AbstractStringFormatter.php b/src/Formatter/AbstractStringFormatter.php index 4c67aac..8cd9e7f 100644 --- a/src/Formatter/AbstractStringFormatter.php +++ b/src/Formatter/AbstractStringFormatter.php @@ -65,16 +65,23 @@ abstract class AbstractStringFormatter extends AbstractFormatter */ private NormalizerInterface $lineEndingNormalizer; + /** + * @param mixed $cacheFingerprint {@see AbstractFormatter::__construct()} + */ public function __construct( - string $version, - ?CodecInterface $interpolationCodec=null, - ?NormalizerInterface $lineEndingNormalizer=null + $cacheFingerprint, + ?CodecInterface $interpolationCodec = null, + ?NormalizerInterface $lineEndingNormalizer = null ) { - parent::__construct($version); - $this->objectIndex = self::$objectCounter++; $this->interpolationCodec = $interpolationCodec ?? new PlainStringCodec(); $this->lineEndingNormalizer = $lineEndingNormalizer ?? new DefaultNormalizer(DefaultNormalizer::NO_CHANGE, DefaultNormalizer::NO_CHANGE); + + parent::__construct([ + $cacheFingerprint, + $this->interpolationCodec->getCacheFingerprint(), + $this->lineEndingNormalizer->getCacheFingerprint(), + ]); } final public function formatBlock(BlockString $blockString): BlockString diff --git a/src/Formatter/ChainFormatter.php b/src/Formatter/ChainFormatter.php index 00d95e8..d4c6454 100644 --- a/src/Formatter/ChainFormatter.php +++ b/src/Formatter/ChainFormatter.php @@ -29,9 +29,12 @@ final class ChainFormatter extends AbstractFormatter */ public function __construct(AbstractFormatter ...$formatters) { - parent::__construct('1.0'); - $this->formatters = array_values($formatters); + + parent::__construct([ + self::class, + array_map(static fn(AbstractFormatter $formatter) => $formatter->getCacheFingerprint(), $this->formatters), + ]); } public function formatBlock(BlockString $blockString): BlockString diff --git a/src/Formatter/CliPipeFormatter.php b/src/Formatter/CliPipeFormatter.php index 4d18595..ae4a288 100644 --- a/src/Formatter/CliPipeFormatter.php +++ b/src/Formatter/CliPipeFormatter.php @@ -67,9 +67,16 @@ public function __construct( } parent::__construct( - is_string($versionValueOrCommand) - ? $versionValueOrCommand - : $this->exec($versionValueOrCommand, null), + sprintf( + '%s: %s v%s', + static::class, + is_array($this->formatter['cmd']) + ? implode(' ', $this->formatter['cmd']) + : $this->formatter['cmd'], + is_string($versionValueOrCommand) + ? $versionValueOrCommand + : $this->exec($versionValueOrCommand, null) + ), $interpolationCodec, $lineEndingNormalizer ); diff --git a/src/Formatter/DockerPipeFormatter.php b/src/Formatter/DockerPipeFormatter.php index bed4429..1e0be30 100644 --- a/src/Formatter/DockerPipeFormatter.php +++ b/src/Formatter/DockerPipeFormatter.php @@ -94,7 +94,18 @@ public function __construct( } parent::__construct( - "{$this->imageDetails['platform']};{$this->imageDetails['digest']}", + sprintf( + '%s: %s', + static::class, + implode( + ' ', + array_merge( + ["{$this->imageDetails['platform']};{$this->imageDetails['digest']}"], + $this->options, + $this->command + ) + ) + ), $interpolationCodec, $lineEndingNormalizer ); diff --git a/src/Formatter/SimpleLineFormatter.php b/src/Formatter/SimpleLineFormatter.php index af8cf29..53e43f9 100644 --- a/src/Formatter/SimpleLineFormatter.php +++ b/src/Formatter/SimpleLineFormatter.php @@ -61,7 +61,7 @@ public function __construct( ); } - parent::__construct('1', $interpolationCodec, $lineEndingNormalizer); + parent::__construct(static::class . ' v1', $interpolationCodec, $lineEndingNormalizer); } protected function formatContent(string $original): string diff --git a/src/InterpolationCodec/CodecInterface.php b/src/InterpolationCodec/CodecInterface.php index bd47191..6e65c77 100644 --- a/src/InterpolationCodec/CodecInterface.php +++ b/src/InterpolationCodec/CodecInterface.php @@ -3,8 +3,9 @@ namespace uuf6429\PhpCsFixerBlockstring\InterpolationCodec; use uuf6429\PhpCsFixerBlockstring\BlockString\SegmentInterface; +use uuf6429\PhpCsFixerBlockstring\CacheFingerprintableInterface; -interface CodecInterface +interface CodecInterface extends CacheFingerprintableInterface { /** * @param list $segments diff --git a/src/InterpolationCodec/GeneratedTokenCodec.php b/src/InterpolationCodec/GeneratedTokenCodec.php index 1a05f1f..68d64ae 100644 --- a/src/InterpolationCodec/GeneratedTokenCodec.php +++ b/src/InterpolationCodec/GeneratedTokenCodec.php @@ -3,8 +3,10 @@ namespace uuf6429\PhpCsFixerBlockstring\InterpolationCodec; use LogicException; +use RuntimeException; use uuf6429\PhpCsFixerBlockstring\BlockString\InterpolationSegment; use uuf6429\PhpCsFixerBlockstring\BlockString\StringSegment; +use uuf6429\PhpCsFixerBlockstring\CacheFingerprintableInterface; final class GeneratedTokenCodec implements CodecInterface { @@ -107,4 +109,28 @@ public function decode(CodecResult $result): array return $segments; } + + public function getCacheFingerprint() + { + return [self::class, $this->tokenPattern, $this->getTokenFactoryCacheFingerprint()]; + } + + /** + * @return mixed + */ + private function getTokenFactoryCacheFingerprint() + { + if (!is_object($this->tokenFactory)) { + return $this->tokenFactory; + } + + if ($this->tokenFactory instanceof CacheFingerprintableInterface) { + return $this->tokenFactory->getCacheFingerprint(); + } + + throw new RuntimeException( + 'Token factory must implement CacheFingerprintableInterface - it cannot be a simple closure object.' + . ' A possibility is to make an anonymous object implementing that interface and an __invoke method.' + ); + } } diff --git a/src/InterpolationCodec/PlainStringCodec.php b/src/InterpolationCodec/PlainStringCodec.php index 13e4821..3ba40b7 100644 --- a/src/InterpolationCodec/PlainStringCodec.php +++ b/src/InterpolationCodec/PlainStringCodec.php @@ -3,7 +3,6 @@ namespace uuf6429\PhpCsFixerBlockstring\InterpolationCodec; use RuntimeException; -use uuf6429\PhpCsFixerBlockstring\BlockString\InterpolationSegment; use uuf6429\PhpCsFixerBlockstring\BlockString\StringSegment; final class PlainStringCodec implements CodecInterface @@ -21,4 +20,9 @@ public function decode(CodecResult $result): array { return [new StringSegment($result->content)]; } + + public function getCacheFingerprint() + { + return [self::class]; + } } diff --git a/src/LineEndingNormalizer/DefaultNormalizer.php b/src/LineEndingNormalizer/DefaultNormalizer.php index 597d8d3..9cf0036 100644 --- a/src/LineEndingNormalizer/DefaultNormalizer.php +++ b/src/LineEndingNormalizer/DefaultNormalizer.php @@ -43,6 +43,11 @@ public function normalize(string $formatted, string $original): string ); } + public function getCacheFingerprint() + { + return [static::class, $this->changeLinesTo, $this->changeFinalLineTo]; + } + private function normalizeLineEnding(string $text, string $original): string { switch ($this->changeLinesTo) { diff --git a/src/LineEndingNormalizer/NormalizerInterface.php b/src/LineEndingNormalizer/NormalizerInterface.php index 7a5fb11..5cbbb49 100644 --- a/src/LineEndingNormalizer/NormalizerInterface.php +++ b/src/LineEndingNormalizer/NormalizerInterface.php @@ -2,7 +2,9 @@ namespace uuf6429\PhpCsFixerBlockstring\LineEndingNormalizer; -interface NormalizerInterface +use uuf6429\PhpCsFixerBlockstring\CacheFingerprintableInterface; + +interface NormalizerInterface extends CacheFingerprintableInterface { public function normalize(string $formatted, string $original): string; } diff --git a/tests/Integration/CacheTest.php b/tests/Integration/CacheTest.php index f09d3e5..2a0e145 100644 --- a/tests/Integration/CacheTest.php +++ b/tests/Integration/CacheTest.php @@ -2,6 +2,7 @@ namespace uuf6429\PhpCsFixerBlockstringTests\Integration; +use JsonException; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; @@ -34,13 +35,15 @@ public static function tearDownAfterClass(): void } /** - * @testWith ["v1.0", "Fixed 1 of 1 files", "Cache file did not exist"] - * ["v1.0", "Fixed 0 of 1 files", "Cache file existed already"] - * ["v1.1", "Fixed 1 of 1 files", "Cache file existed already"] - * ["v1.1", "Fixed 0 of 1 files", "Cache file existed already"] + * @testWith ["Test v1", "Fixed 1 of 1 files", "Cache file did not exist"] + * ["Test v1", "Fixed 0 of 1 files", "Cache file existed already"] + * ["Test v2", "Fixed 1 of 1 files", "Cache file existed already"] + * ["Test v2", "Fixed 0 of 1 files", "Cache file existed already"] + * + * @throws JsonException */ public function testCacheReuse( - string $fixerVersion, + string $cacheFingerprint, string $expectedProcessOutput, string $expectedCacheFileExistence ): void { @@ -61,7 +64,7 @@ public function testCacheReuse( self::$inputFile, ], null, - ['TEST_FIXER_VERSION' => $fixerVersion] + ['TEST_FORMATTER_CACHE_FINGERPRINT' => $cacheFingerprint] ); $process->mustRun(); @@ -71,5 +74,50 @@ public function testCacheReuse( $this->assertSame($expectedCacheFileExistence, $cacheFileExistence); $this->assertFileEquals(__DIR__ . '/../fixtures/simple-output.php', self::$inputFile); $this->assertStringContainsString($expectedProcessOutput, $output); + $this->assertJsonFileWithinJsonFile( + __DIR__ . '/../fixtures/simple-cache.json', + self::$cacheFile, + ['TEST_FORMATTER_CACHE_FINGERPRINT' => $cacheFingerprint] + ); + } + + /** + * @param array $placeholders + * @throws JsonException + */ + private function assertJsonFileWithinJsonFile(string $expectedFile, string $actualSubsetFile, array $placeholders = []): void + { + $this->assertFileExists($expectedFile); + $this->assertFileExists($actualSubsetFile); + $this->assertArraySubsetRecursive( + (array)json_decode(strtr((string)file_get_contents($expectedFile), $placeholders), true, 512, JSON_THROW_ON_ERROR), + (array)json_decode((string)file_get_contents($actualSubsetFile), true, 512, JSON_THROW_ON_ERROR), + sprintf( + 'Failed asserting that %s contains %s', + (string)realpath($actualSubsetFile), + (string)realpath($expectedFile) + ) + ); + } + + /** + * @param array $expected + * @param array $actual + */ + private function assertArraySubsetRecursive(array $expected, array $actual, string $message = '', string $path = '$'): void + { + foreach ($expected as $key => $value) { + $this->assertArrayHasKey($key, $actual, ltrim("{$message}\nMissing key at path: {$path}")); + + if (!is_array($value) || !is_array($actual[$key])) { + $this->assertSame($value, $actual[$key], ltrim("{$message}\nMismatch at path: {$path}")); + continue; + } + + $subPath = is_string($key) && preg_match('/^\w+$/', $key) === 1 + ? "{$path}.{$key}" + : "{$path}[" . var_export($key, true) . "]"; + $this->assertArraySubsetRecursive($value, $actual[$key], $message, $subPath); + } } } diff --git a/tests/fixtures/example-config.php b/tests/fixtures/example-config.php index 7826568..29a3bf5 100644 --- a/tests/fixtures/example-config.php +++ b/tests/fixtures/example-config.php @@ -34,7 +34,7 @@ new class extends Formatter\AbstractStringFormatter { public function __construct() { - parent::__construct('1.0', new GeneratedTokenCodec('"__PHP_VAR_%d__"')); + parent::__construct('ObjectKeySorter v1.0', new GeneratedTokenCodec('"__PHP_VAR_%d__"')); } public function formatContent(string $original): string diff --git a/tests/fixtures/simple-cache.json b/tests/fixtures/simple-cache.json new file mode 100644 index 0000000..62b2932 --- /dev/null +++ b/tests/fixtures/simple-cache.json @@ -0,0 +1,19 @@ +{ + "rules": { + "Uuf6429\/block_string": { + "formatters": { + "JSON": [ + "TEST_FORMATTER_CACHE_FINGERPRINT", + [ + "uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec" + ], + [ + "uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer", + "noop", + "noop" + ] + ] + } + } + } +} diff --git a/tests/fixtures/simple-config.php b/tests/fixtures/simple-config.php index 2445b73..df82897 100644 --- a/tests/fixtures/simple-config.php +++ b/tests/fixtures/simple-config.php @@ -12,7 +12,7 @@ 'JSON' => new class extends Formatter\AbstractStringFormatter { public function __construct() { - parent::__construct(getenv('TEST_FIXER_VERSION')); + parent::__construct(getenv('TEST_FORMATTER_CACHE_FINGERPRINT')); } public function formatContent(string $original): string From 2b9563e2071b268d470935afdb0e7652d06e9408 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Thu, 21 May 2026 20:38:52 +0200 Subject: [PATCH 04/11] Tiny improvement --- README.md | 2 +- src/Readmerator/README.tpl.md | 2 +- tests/CustomAssertionsTrait.php | 52 +++++++++++++++++++++++ tests/Integration/CacheTest.php | 43 ++----------------- tests/Unit/Fixer/BlockStringFixerTest.php | 16 +++---- 5 files changed, 65 insertions(+), 50 deletions(-) create mode 100644 tests/CustomAssertionsTrait.php diff --git a/README.md b/README.md index 0b43b16..eed1bad 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ composer require uuf6429/php-cs-fixer-blockstring --dev Finally, register a custom fixer in your `.php-cs-fixer.php` config file (1️⃣) and then set up formatters (2️⃣): ```php - $placeholders + * @throws JsonException + */ + private function assertJsonFileWithinJsonFile(string $expectedFile, string $actualSubsetFile, array $placeholders = []): void + { + $this->assertFileExists($expectedFile); + $this->assertFileExists($actualSubsetFile); + $this->assertArraySubsetRecursive( + (array)json_decode(strtr((string)file_get_contents($expectedFile), $placeholders), true, 512, JSON_THROW_ON_ERROR), + (array)json_decode((string)file_get_contents($actualSubsetFile), true, 512, JSON_THROW_ON_ERROR), + sprintf( + 'Failed asserting that %s contains %s', + (string)realpath($actualSubsetFile), + (string)realpath($expectedFile) + ) + ); + } + + /** + * @param array $expected + * @param array $actual + */ + private function assertArraySubsetRecursive(array $expected, array $actual, string $message = '', string $path = '$'): void + { + foreach ($expected as $key => $value) { + $this->assertArrayHasKey($key, $actual, ltrim("{$message}\nMissing key at path: {$path}")); + + if (!is_array($value) || !is_array($actual[$key])) { + $this->assertSame($value, $actual[$key], ltrim("{$message}\nMismatch at path: {$path}")); + continue; + } + + $subPath = is_string($key) && preg_match('/^\w+$/', $key) === 1 + ? "{$path}.{$key}" + : "{$path}[" . var_export($key, true) . "]"; + $this->assertArraySubsetRecursive($value, $actual[$key], $message, $subPath); + } + } +} diff --git a/tests/Integration/CacheTest.php b/tests/Integration/CacheTest.php index 2a0e145..52b3f4b 100644 --- a/tests/Integration/CacheTest.php +++ b/tests/Integration/CacheTest.php @@ -5,12 +5,15 @@ use JsonException; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; +use uuf6429\PhpCsFixerBlockstringTests\CustomAssertionsTrait; /** * @internal */ final class CacheTest extends TestCase { + use CustomAssertionsTrait; + private const PCF_BINARY_PATH = __DIR__ . '/../../vendor/bin/php-cs-fixer'; private static string $workspace; @@ -80,44 +83,4 @@ public function testCacheReuse( ['TEST_FORMATTER_CACHE_FINGERPRINT' => $cacheFingerprint] ); } - - /** - * @param array $placeholders - * @throws JsonException - */ - private function assertJsonFileWithinJsonFile(string $expectedFile, string $actualSubsetFile, array $placeholders = []): void - { - $this->assertFileExists($expectedFile); - $this->assertFileExists($actualSubsetFile); - $this->assertArraySubsetRecursive( - (array)json_decode(strtr((string)file_get_contents($expectedFile), $placeholders), true, 512, JSON_THROW_ON_ERROR), - (array)json_decode((string)file_get_contents($actualSubsetFile), true, 512, JSON_THROW_ON_ERROR), - sprintf( - 'Failed asserting that %s contains %s', - (string)realpath($actualSubsetFile), - (string)realpath($expectedFile) - ) - ); - } - - /** - * @param array $expected - * @param array $actual - */ - private function assertArraySubsetRecursive(array $expected, array $actual, string $message = '', string $path = '$'): void - { - foreach ($expected as $key => $value) { - $this->assertArrayHasKey($key, $actual, ltrim("{$message}\nMissing key at path: {$path}")); - - if (!is_array($value) || !is_array($actual[$key])) { - $this->assertSame($value, $actual[$key], ltrim("{$message}\nMismatch at path: {$path}")); - continue; - } - - $subPath = is_string($key) && preg_match('/^\w+$/', $key) === 1 - ? "{$path}.{$key}" - : "{$path}[" . var_export($key, true) . "]"; - $this->assertArraySubsetRecursive($value, $actual[$key], $message, $subPath); - } - } } diff --git a/tests/Unit/Fixer/BlockStringFixerTest.php b/tests/Unit/Fixer/BlockStringFixerTest.php index 4e44d95..7f1321a 100644 --- a/tests/Unit/Fixer/BlockStringFixerTest.php +++ b/tests/Unit/Fixer/BlockStringFixerTest.php @@ -72,13 +72,13 @@ public static function provideFixCases(): iterable yield 'nowdoc with unregistered delimiter should be left unchanged' => [ 'config' => ['formatters' => []], 'input' => <<<'PHP' - Hello world! HTML; PHP, 'expected' => <<<'PHP' - Hello world! HTML; @@ -102,7 +102,7 @@ protected function formatContent(string $original): string ], ], 'input' => <<<'PHP' - Hello world1 HTML; @@ -114,7 +114,7 @@ protected function formatContent(string $original): string XML; PHP, 'expected' => <<<'PHP' - <<<'PHP' - <<<'PHP' - Hello world HTML; @@ -191,13 +191,13 @@ protected function formatContent(string $original): string ], ], 'input' => <<<'PHP' - Hello $planet! HTML; PHP, 'expected' => <<<'PHP' - Date: Thu, 21 May 2026 21:01:05 +0200 Subject: [PATCH 05/11] Improve code coverage --- .../Unit/Formatter/AbstractFormatterTest.php | 25 ++++++++++++ tests/Unit/Formatter/WslPipeFormatterTest.php | 38 +++++++++++++++++++ .../GeneratedTokenCodecTest.php | 36 ++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 tests/Unit/Formatter/AbstractFormatterTest.php create mode 100644 tests/Unit/Formatter/WslPipeFormatterTest.php diff --git a/tests/Unit/Formatter/AbstractFormatterTest.php b/tests/Unit/Formatter/AbstractFormatterTest.php new file mode 100644 index 0000000..9c48cea --- /dev/null +++ b/tests/Unit/Formatter/AbstractFormatterTest.php @@ -0,0 +1,25 @@ +assertSame('"fingerprint"', json_encode($formatter)); + } +} diff --git a/tests/Unit/Formatter/WslPipeFormatterTest.php b/tests/Unit/Formatter/WslPipeFormatterTest.php new file mode 100644 index 0000000..2a19f34 --- /dev/null +++ b/tests/Unit/Formatter/WslPipeFormatterTest.php @@ -0,0 +1,38 @@ +markTestSkipped( + 'GitHub actions are not able to run non-Windows docker images: https://github.com/orgs/community/discussions/138554' + ); + } + } + + public function testFormat(): void + { + $formatter = new WslPipeFormatter( + ['cmd' => 'php -v'], + ['cmd' => ['php', '-r', 'echo strrev(stream_get_contents(STDIN));']] + ); + $inputBlockString = new BlockString('', '', [new StringSegment('foobar')]); + + $outputBlockString = $formatter->formatBlock($inputBlockString); + + $this->assertSame('raboof', implode('', $outputBlockString->segments)); + } +} diff --git a/tests/Unit/InterpolationCodec/GeneratedTokenCodecTest.php b/tests/Unit/InterpolationCodec/GeneratedTokenCodecTest.php index 1e9253b..23a61bc 100644 --- a/tests/Unit/InterpolationCodec/GeneratedTokenCodecTest.php +++ b/tests/Unit/InterpolationCodec/GeneratedTokenCodecTest.php @@ -5,8 +5,10 @@ use LogicException; use PhpCsFixer\Tokenizer\Token; use PHPUnit\Framework\TestCase; +use RuntimeException; use uuf6429\PhpCsFixerBlockstring\BlockString\InterpolationSegment; use uuf6429\PhpCsFixerBlockstring\BlockString\StringSegment; +use uuf6429\PhpCsFixerBlockstring\CacheFingerprintableInterface; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\CodecResult; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\GeneratedTokenCodec; @@ -67,4 +69,38 @@ public function testThatDefaultBehaviourTriggeredWhenTokenFactoryReturnsNull(): $result ); } + + public function testThatTokenFactoryCanBeSimpleCallable(): void + { + /** @phpstan-ignore argument.type */ + $codec = new GeneratedTokenCodec('', 'array_map'); + + $this->assertSame([GeneratedTokenCodec::class, '', 'array_map'], $codec->getCacheFingerprint()); + } + + public function testThatTokenFactoryCannotBeBeSimpleClosure(): void + { + $codec = new GeneratedTokenCodec('', static fn() => null); + + $this->expectExceptionObject(new RuntimeException('Token factory must implement CacheFingerprintableInterface')); + + $codec->getCacheFingerprint(); + } + + public function testThatTokenFactoryCanBeCachableInvokable(): void + { + $codec = new GeneratedTokenCodec('', new class implements CacheFingerprintableInterface { + public function __invoke(): string + { + return 'xx'; + } + + public function getCacheFingerprint() + { + return 'fingerprint'; + } + }); + + $this->assertSame([GeneratedTokenCodec::class, '', 'fingerprint'], $codec->getCacheFingerprint()); + } } From 180d427d61a1bedc208a105abfb5b3964a59b317 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Thu, 21 May 2026 21:07:02 +0200 Subject: [PATCH 06/11] Tweak test deps --- tests/Unit/Formatter/WslPipeFormatterTest.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/Unit/Formatter/WslPipeFormatterTest.php b/tests/Unit/Formatter/WslPipeFormatterTest.php index 2a19f34..f620ae4 100644 --- a/tests/Unit/Formatter/WslPipeFormatterTest.php +++ b/tests/Unit/Formatter/WslPipeFormatterTest.php @@ -12,19 +12,15 @@ */ final class WslPipeFormatterTest extends TestCase { - protected function setUp(): void + public function testFormat(): void { - parent::setUp(); - - if (PHP_OS_FAMILY === 'Windows' && getenv('GITHUB_ACTIONS') === 'true') { - $this->markTestSkipped( - 'GitHub actions are not able to run non-Windows docker images: https://github.com/orgs/community/discussions/138554' - ); + if (PHP_OS_FAMILY !== 'Windows') { + $this->markTestSkipped('WSL is only available on Windows'); + } + if (getenv('GITHUB_ACTIONS') === 'true') { + $this->markTestSkipped('WSL on GitHub Actions is poorly supported and unusable'); } - } - public function testFormat(): void - { $formatter = new WslPipeFormatter( ['cmd' => 'php -v'], ['cmd' => ['php', '-r', 'echo strrev(stream_get_contents(STDIN));']] From dc60d1f4e00eb7855a28f4521649b02cea1f7738 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sat, 23 May 2026 08:49:25 +0200 Subject: [PATCH 07/11] Various fixes --- composer.json | 2 +- src/Fixer/BlockStringFixer.php | 41 ++++++++++++++----- src/Formatter/AbstractFormatter.php | 12 +----- tests/CustomAssertionsTrait.php | 18 ++++---- tests/Integration/CacheTest.php | 28 ++++++------- tests/Integration/ExampleTest.php | 28 ++++++++----- tests/fixtures/example-config.php | 8 ++-- tests/fixtures/simple-cache-v1.json | 7 ++++ tests/fixtures/simple-cache-v2.json | 7 ++++ tests/fixtures/simple-cache.json | 19 --------- tests/fixtures/simple-config-v1.php | 34 +++++++++++++++ tests/fixtures/simple-config-v2.php | 34 +++++++++++++++ tests/fixtures/simple-config.php | 28 ------------- ...simple-output.php => simple-output-v1.php} | 1 + tests/fixtures/simple-output-v2.php | 13 ++++++ 15 files changed, 172 insertions(+), 108 deletions(-) create mode 100644 tests/fixtures/simple-cache-v1.json create mode 100644 tests/fixtures/simple-cache-v2.json delete mode 100644 tests/fixtures/simple-cache.json create mode 100644 tests/fixtures/simple-config-v1.php create mode 100644 tests/fixtures/simple-config-v2.php delete mode 100644 tests/fixtures/simple-config.php rename tests/fixtures/{simple-output.php => simple-output-v1.php} (79%) create mode 100644 tests/fixtures/simple-output-v2.php diff --git a/composer.json b/composer.json index a6ef666..a10dc6d 100644 --- a/composer.json +++ b/composer.json @@ -22,12 +22,12 @@ ], "require": { "php": "^7.4 || ^8.0", - "ext-json": "*", "symfony/process": "^5 || ^6 || ^7 || ^8", "friendsofphp/php-cs-fixer": "^3", "symfony/deprecation-contracts": "^2 || ^3" }, "require-dev": { + "ext-json": "*", "ergebnis/composer-normalize": "^2.7", "phpunit/phpunit": "^9.5", "phpstan/phpstan": "^2.1", diff --git a/src/Fixer/BlockStringFixer.php b/src/Fixer/BlockStringFixer.php index 8677dbb..5770893 100644 --- a/src/Fixer/BlockStringFixer.php +++ b/src/Fixer/BlockStringFixer.php @@ -17,9 +17,11 @@ use const T_START_HEREDOC; /** - * @phpstan-type TFormatterConfig array{formatters: array<0|non-empty-string, AbstractFormatter>} + * @phpstan-type TFormatters array<0|non-empty-string, AbstractFormatter> + * @phpstan-type TDeserilizedConfig array{formatters: TFormatters} + * @phpstan-type TSerializedConfig array{formatters: string} * - * @implements ConfigurableFixerInterface + * @implements ConfigurableFixerInterface */ final class BlockStringFixer implements FixerInterface, ConfigurableFixerInterface { @@ -28,9 +30,9 @@ final class BlockStringFixer implements FixerInterface, ConfigurableFixerInterfa private ?FixerConfigurationResolverInterface $configurationDefinition = null; /** - * @var null|TFormatterConfig + * @var TDeserilizedConfig */ - private ?array $configuration = null; + private array $configuration; public function isRisky(): bool { @@ -75,18 +77,26 @@ public function getConfigurationDefinition(): FixerConfigurationResolverInterfac ]); } + /** + * @param TDeserilizedConfig $configuration + * @return void + */ public function configure(array $configuration): void { - // @phpstan-ignore assign.propertyType - $this->configuration = $this->getConfigurationDefinition()->resolve($configuration); + if (isset($configuration['formatters']) && !is_string($configuration['formatters'])) { + throw new InvalidArgumentException( + 'BlockStringFixer configuration is not valid. ' + . 'Did you set it up in your PHP CS Fixer config with `BlockStringFixer::config()`?' + ); + } + + $this->configuration = [ + 'formatters' => unserialize($configuration['formatters'] ?? 'a:0:{}', ['allowed_classes' => true]), + ]; } public function fix(SplFileInfo $file, Tokens $tokens): void { - if ($this->configuration === null) { - throw new InvalidArgumentException("Configuration for fixer {$this->getName()} is required."); - } - if (0 < $tokens->count() && $this->isCandidate($tokens) && $this->supports($file)) { $blockStringStream = TokenStream::fromPhpCsFixerTokens($tokens); while (($blockString = $blockStringStream->next()) !== null) { @@ -100,4 +110,15 @@ public function fix(SplFileInfo $file, Tokens $tokens): void } } } + + /** + * @param TFormatters $formatters + * @return TSerializedConfig + */ + public static function config(array $formatters): array + { + return [ + 'formatters' => serialize($formatters), + ]; + } } diff --git a/src/Formatter/AbstractFormatter.php b/src/Formatter/AbstractFormatter.php index 2d74f73..97c0788 100644 --- a/src/Formatter/AbstractFormatter.php +++ b/src/Formatter/AbstractFormatter.php @@ -2,7 +2,6 @@ namespace uuf6429\PhpCsFixerBlockstring\Formatter; -use JsonSerializable; use uuf6429\PhpCsFixerBlockstring\BlockString\BlockString; use uuf6429\PhpCsFixerBlockstring\CacheFingerprintableInterface; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\CodecInterface; @@ -18,7 +17,7 @@ * 2. Or if, for whatever reason, the {@see CodecInterface} concept does not work for you and you want to write * something from scratch. */ -abstract class AbstractFormatter implements CacheFingerprintableInterface, JsonSerializable +abstract class AbstractFormatter implements CacheFingerprintableInterface { /** * @var mixed @@ -53,13 +52,4 @@ final public function getCacheFingerprint() { return $this->cacheFingerprint; } - - /** - * @return mixed - */ - #[\ReturnTypeWillChange] - final public function jsonSerialize() - { - return $this->getCacheFingerprint(); - } } diff --git a/tests/CustomAssertionsTrait.php b/tests/CustomAssertionsTrait.php index 286dd4b..4f0a019 100644 --- a/tests/CustomAssertionsTrait.php +++ b/tests/CustomAssertionsTrait.php @@ -11,15 +11,14 @@ trait CustomAssertionsTrait { /** - * @param array $placeholders * @throws JsonException */ - private function assertJsonFileWithinJsonFile(string $expectedFile, string $actualSubsetFile, array $placeholders = []): void + private function assertJsonFileWithinJsonFile(string $expectedFile, string $actualSubsetFile): void { $this->assertFileExists($expectedFile); $this->assertFileExists($actualSubsetFile); $this->assertArraySubsetRecursive( - (array)json_decode(strtr((string)file_get_contents($expectedFile), $placeholders), true, 512, JSON_THROW_ON_ERROR), + (array)json_decode((string)file_get_contents($expectedFile), true, 512, JSON_THROW_ON_ERROR), (array)json_decode((string)file_get_contents($actualSubsetFile), true, 512, JSON_THROW_ON_ERROR), sprintf( 'Failed asserting that %s contains %s', @@ -36,17 +35,18 @@ private function assertJsonFileWithinJsonFile(string $expectedFile, string $actu private function assertArraySubsetRecursive(array $expected, array $actual, string $message = '', string $path = '$'): void { foreach ($expected as $key => $value) { - $this->assertArrayHasKey($key, $actual, ltrim("{$message}\nMissing key at path: {$path}")); + $currentPath = is_string($key) && preg_match('/^\w+$/', $key) === 1 + ? "{$path}.{$key}" + : "{$path}[" . var_export($key, true) . "]"; + + $this->assertArrayHasKey($key, $actual, ltrim("{$message}\nMissing key at path: {$currentPath}")); if (!is_array($value) || !is_array($actual[$key])) { - $this->assertSame($value, $actual[$key], ltrim("{$message}\nMismatch at path: {$path}")); + $this->assertSame($value, $actual[$key], ltrim("{$message}\nMismatch at path: {$currentPath}")); continue; } - $subPath = is_string($key) && preg_match('/^\w+$/', $key) === 1 - ? "{$path}.{$key}" - : "{$path}[" . var_export($key, true) . "]"; - $this->assertArraySubsetRecursive($value, $actual[$key], $message, $subPath); + $this->assertArraySubsetRecursive($value, $actual[$key], $message, $currentPath); } } } diff --git a/tests/Integration/CacheTest.php b/tests/Integration/CacheTest.php index 52b3f4b..b4913a7 100644 --- a/tests/Integration/CacheTest.php +++ b/tests/Integration/CacheTest.php @@ -38,15 +38,15 @@ public static function tearDownAfterClass(): void } /** - * @testWith ["Test v1", "Fixed 1 of 1 files", "Cache file did not exist"] - * ["Test v1", "Fixed 0 of 1 files", "Cache file existed already"] - * ["Test v2", "Fixed 1 of 1 files", "Cache file existed already"] - * ["Test v2", "Fixed 0 of 1 files", "Cache file existed already"] + * @testWith ["v1", "Fixed 1 of 1 files", "Cache file did not exist"] + * ["v1", "Fixed 0 of 1 files", "Cache file existed already"] + * ["v2", "Fixed 1 of 1 files", "Cache file existed already"] + * ["v2", "Fixed 0 of 1 files", "Cache file existed already"] * * @throws JsonException */ public function testCacheReuse( - string $cacheFingerprint, + string $formatterVersion, string $expectedProcessOutput, string $expectedCacheFileExistence ): void { @@ -55,10 +55,10 @@ public function testCacheReuse( : 'Cache file did not exist'; $process = new Process( [ - 'php', + PHP_BINARY, self::PCF_BINARY_PATH, 'fix', - '--config=' . __DIR__ . '/../fixtures/simple-config.php', + '--config=' . __DIR__ . "/../fixtures/simple-config-{$formatterVersion}.php", '--cache-file=' . self::$cacheFile, '--allow-unsupported-php-version=yes', '--show-progress=none', @@ -66,8 +66,10 @@ public function testCacheReuse( '-vvv', self::$inputFile, ], - null, - ['TEST_FORMATTER_CACHE_FINGERPRINT' => $cacheFingerprint] + self::$workspace, + [ + 'PHP_CS_FIXER_ALLOW_XDEBUG' => 1, + ] ); $process->mustRun(); @@ -75,12 +77,8 @@ public function testCacheReuse( $output = $process->getErrorOutput() . $process->getOutput(); $this->assertSame($expectedCacheFileExistence, $cacheFileExistence); - $this->assertFileEquals(__DIR__ . '/../fixtures/simple-output.php', self::$inputFile); + $this->assertFileEquals(__DIR__ . "/../fixtures/simple-output-{$formatterVersion}.php", self::$inputFile); $this->assertStringContainsString($expectedProcessOutput, $output); - $this->assertJsonFileWithinJsonFile( - __DIR__ . '/../fixtures/simple-cache.json', - self::$cacheFile, - ['TEST_FORMATTER_CACHE_FINGERPRINT' => $cacheFingerprint] - ); + $this->assertJsonFileWithinJsonFile(__DIR__ . "/../fixtures/simple-cache-{$formatterVersion}.json", self::$cacheFile); } } diff --git a/tests/Integration/ExampleTest.php b/tests/Integration/ExampleTest.php index cce0058..671d186 100644 --- a/tests/Integration/ExampleTest.php +++ b/tests/Integration/ExampleTest.php @@ -24,17 +24,23 @@ public function testExample(): void try { copy(__DIR__ . '/../fixtures/example-input.php', $tempFile); - $process = new Process([ - 'php', - self::PCF_BINARY_PATH, - 'fix', - '--using-cache=no', - '--config=' . __DIR__ . '/../fixtures/example-config.php', - '--sequential', - '-vvv', - '--diff', - $tempFile, - ]); + $process = new Process( + [ + PHP_BINARY, + self::PCF_BINARY_PATH, + 'fix', + '--using-cache=no', + '--config=' . __DIR__ . '/../fixtures/example-config.php', + '--sequential', + '-vvv', + '--diff', + $tempFile, + ], + null, + [ + 'PHP_CS_FIXER_ALLOW_XDEBUG' => 1, + ] + ); $process->mustRun(); diff --git a/tests/fixtures/example-config.php b/tests/fixtures/example-config.php index 29a3bf5..5262b6f 100644 --- a/tests/fixtures/example-config.php +++ b/tests/fixtures/example-config.php @@ -8,8 +8,8 @@ ->registerCustomFixers([new BlockStringFixer()]) ->setRiskyAllowed(true) ->setRules([ - BlockStringFixer::NAME => [ - 'formatters' => [ + BlockStringFixer::NAME => BlockStringFixer::config( + [ // 1️⃣ SimpleLineFormatter // Normalizes indentation of any block not explicitly configured below @@ -80,6 +80,6 @@ private function sortObjectKeysRecursively($value) ), ), - ], - ], + ] + ), ]); diff --git a/tests/fixtures/simple-cache-v1.json b/tests/fixtures/simple-cache-v1.json new file mode 100644 index 0000000..33f7226 --- /dev/null +++ b/tests/fixtures/simple-cache-v1.json @@ -0,0 +1,7 @@ +{ + "rules": { + "Uuf6429\/block_string": { + "formatters": "a:1:{s:4:\"JSON\";O:15:\"JsonFormatterV1\":4:{s:76:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000objectIndex\";i:0;s:21:\"\u0000*\u0000interpolationCodec\";O:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\":0:{}s:85:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000lineEndingNormalizer\";O:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\":2:{s:83:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeLinesTo\";s:4:\"noop\";s:87:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeFinalLineTo\";s:4:\"noop\";}s:75:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractFormatter\u0000cacheFingerprint\";a:3:{i:0;s:15:\"JsonFormatterV1\";i:1;a:1:{i:0;s:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\";}i:2;a:3:{i:0;s:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\";i:1;s:4:\"noop\";i:2;s:4:\"noop\";}}}}" + } + } +} diff --git a/tests/fixtures/simple-cache-v2.json b/tests/fixtures/simple-cache-v2.json new file mode 100644 index 0000000..dc1cd03 --- /dev/null +++ b/tests/fixtures/simple-cache-v2.json @@ -0,0 +1,7 @@ +{ + "rules": { + "Uuf6429\/block_string": { + "formatters": "a:1:{s:4:\"JSON\";O:15:\"JsonFormatterV2\":4:{s:76:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000objectIndex\";i:0;s:21:\"\u0000*\u0000interpolationCodec\";O:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\":0:{}s:85:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000lineEndingNormalizer\";O:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\":2:{s:83:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeLinesTo\";s:4:\"noop\";s:87:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeFinalLineTo\";s:4:\"noop\";}s:75:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractFormatter\u0000cacheFingerprint\";a:3:{i:0;s:15:\"JsonFormatterV2\";i:1;a:1:{i:0;s:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\";}i:2;a:3:{i:0;s:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\";i:1;s:4:\"noop\";i:2;s:4:\"noop\";}}}}" + } + } +} diff --git a/tests/fixtures/simple-cache.json b/tests/fixtures/simple-cache.json deleted file mode 100644 index 62b2932..0000000 --- a/tests/fixtures/simple-cache.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "rules": { - "Uuf6429\/block_string": { - "formatters": { - "JSON": [ - "TEST_FORMATTER_CACHE_FINGERPRINT", - [ - "uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec" - ], - [ - "uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer", - "noop", - "noop" - ] - ] - } - } - } -} diff --git a/tests/fixtures/simple-config-v1.php b/tests/fixtures/simple-config-v1.php new file mode 100644 index 0000000..b59eb8a --- /dev/null +++ b/tests/fixtures/simple-config-v1.php @@ -0,0 +1,34 @@ + self::class], + (array)json_decode($original, false, 512, JSON_THROW_ON_ERROR) + ), + JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + ); + } +} + +return (new PhpCsFixer\Config()) + ->registerCustomFixers([new BlockStringFixer()]) + ->setRiskyAllowed(true) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config( + [ + 'JSON' => new JsonFormatterV1(), + ] + ) + ]); diff --git a/tests/fixtures/simple-config-v2.php b/tests/fixtures/simple-config-v2.php new file mode 100644 index 0000000..eb2dced --- /dev/null +++ b/tests/fixtures/simple-config-v2.php @@ -0,0 +1,34 @@ + self::class], + (array)json_decode($original, false, 512, JSON_THROW_ON_ERROR) + ), + JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + ); + } +} + +return (new PhpCsFixer\Config()) + ->registerCustomFixers([new BlockStringFixer()]) + ->setRiskyAllowed(true) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config( + [ + 'JSON' => new JsonFormatterV2(), + ] + ) + ]); diff --git a/tests/fixtures/simple-config.php b/tests/fixtures/simple-config.php deleted file mode 100644 index df82897..0000000 --- a/tests/fixtures/simple-config.php +++ /dev/null @@ -1,28 +0,0 @@ -registerCustomFixers([new BlockStringFixer()]) - ->setRiskyAllowed(true) - ->setRules([ - BlockStringFixer::NAME => [ - 'formatters' => [ - 'JSON' => new class extends Formatter\AbstractStringFormatter { - public function __construct() - { - parent::__construct(getenv('TEST_FORMATTER_CACHE_FINGERPRINT')); - } - - public function formatContent(string $original): string - { - return json_encode( - json_decode($original, false, 512, JSON_THROW_ON_ERROR), - JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT - ); - } - }, - ], - ], - ]); diff --git a/tests/fixtures/simple-output.php b/tests/fixtures/simple-output-v1.php similarity index 79% rename from tests/fixtures/simple-output.php rename to tests/fixtures/simple-output-v1.php index 486de50..36be182 100644 --- a/tests/fixtures/simple-output.php +++ b/tests/fixtures/simple-output-v1.php @@ -2,6 +2,7 @@ echo <<<'JSON' { + "_comment": "JsonFormatterV1", "users": [], "ascending": false } diff --git a/tests/fixtures/simple-output-v2.php b/tests/fixtures/simple-output-v2.php new file mode 100644 index 0000000..01acd90 --- /dev/null +++ b/tests/fixtures/simple-output-v2.php @@ -0,0 +1,13 @@ + Date: Sat, 23 May 2026 11:41:50 +0200 Subject: [PATCH 08/11] Fixed logic issue in tests; various improvements; major restructuring --- README.md | 169 +++++++++--------- src/Fixer/BlockStringFixer.php | 18 +- src/Formatter/AbstractStringFormatter.php | 31 ++-- src/Formatter/ChainFormatter.php | 16 +- src/Formatter/CliPipeFormatter.php | 22 ++- src/Formatter/DockerPipeFormatter.php | 28 ++- src/Formatter/SimpleLineFormatter.php | 22 ++- src/Readmerator/Readmerator.php | 6 +- tests/Fixtures/Formatters/HtmlTagStripper.php | 22 +++ tests/Fixtures/Formatters/JsonCommenter.php | 31 ++++ .../Formatters/JsonObjectKeySorter.php | 51 ++++++ tests/Fixtures/Formatters/TagWrapper.php | 25 +++ .../Fixtures/Scenarios/caching/cache-v1.json | 7 + .../Fixtures/Scenarios/caching/cache-v2.json | 7 + .../Fixtures/Scenarios/caching/config-v1.php | 15 ++ .../Fixtures/Scenarios/caching/config-v2.php | 15 ++ .../Scenarios/caching/input.php} | 0 .../Scenarios/caching/output-v1.php} | 4 +- .../Scenarios/caching/output-v2.php} | 4 +- .../Scenarios/example/config.php} | 42 +---- .../Scenarios/example/input.php} | 0 .../Scenarios/example/output.php} | 0 .../Scenarios/example}/sqlformat.php | 0 tests/Integration/CacheTest.php | 15 +- tests/Integration/ExampleTest.php | 6 +- tests/Unit/Fixer/BlockStringFixerTest.php | 122 +++++-------- .../Unit/Formatter/AbstractFormatterTest.php | 2 +- tests/fixtures/simple-cache-v1.json | 7 - tests/fixtures/simple-cache-v2.json | 7 - tests/fixtures/simple-config-v1.php | 34 ---- tests/fixtures/simple-config-v2.php | 34 ---- 31 files changed, 406 insertions(+), 356 deletions(-) create mode 100644 tests/Fixtures/Formatters/HtmlTagStripper.php create mode 100644 tests/Fixtures/Formatters/JsonCommenter.php create mode 100644 tests/Fixtures/Formatters/JsonObjectKeySorter.php create mode 100644 tests/Fixtures/Formatters/TagWrapper.php create mode 100644 tests/Fixtures/Scenarios/caching/cache-v1.json create mode 100644 tests/Fixtures/Scenarios/caching/cache-v2.json create mode 100644 tests/Fixtures/Scenarios/caching/config-v1.php create mode 100644 tests/Fixtures/Scenarios/caching/config-v2.php rename tests/{fixtures/simple-input.php => Fixtures/Scenarios/caching/input.php} (100%) rename tests/{fixtures/simple-output-v1.php => Fixtures/Scenarios/caching/output-v1.php} (64%) rename tests/{fixtures/simple-output-v2.php => Fixtures/Scenarios/caching/output-v2.php} (64%) rename tests/{fixtures/example-config.php => Fixtures/Scenarios/example/config.php} (63%) rename tests/{fixtures/example-input.php => Fixtures/Scenarios/example/input.php} (100%) rename tests/{fixtures/example-output.php => Fixtures/Scenarios/example/output.php} (100%) rename tests/{fixtures => Fixtures/Scenarios/example}/sqlformat.php (100%) delete mode 100644 tests/fixtures/simple-cache-v1.json delete mode 100644 tests/fixtures/simple-cache-v2.json delete mode 100644 tests/fixtures/simple-config-v1.php delete mode 100644 tests/fixtures/simple-config-v2.php diff --git a/README.md b/README.md index eed1bad..2b9c49b 100644 --- a/README.md +++ b/README.md @@ -147,13 +147,14 @@ reverses them back, but since this seems like an uncommon usecase, there aren't use uuf6429\PhpCsFixerBlockstring\Fixer\BlockStringFixer; use uuf6429\PhpCsFixerBlockstring\Formatter; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\GeneratedTokenCodec; +use uuf6429\PhpCsFixerBlockstringTests\Fixtures; return (new PhpCsFixer\Config()) ->registerCustomFixers([new BlockStringFixer()]) ->setRiskyAllowed(true) ->setRules([ - BlockStringFixer::NAME => [ - 'formatters' => [ + BlockStringFixer::NAME => BlockStringFixer::config( + [ // 1️⃣ SimpleLineFormatter // Normalizes indentation of any block not explicitly configured below @@ -175,46 +176,7 @@ return (new PhpCsFixer\Config()) // 1. A custom formatter that sorts object keys. // 2. A docker-based formatter that runs the json through jq. 'JSON' => new Formatter\ChainFormatter( - new class extends Formatter\AbstractStringFormatter { - public function __construct() - { - parent::__construct('ObjectKeySorter v1.0', new GeneratedTokenCodec('"__PHP_VAR_%d__"')); - } - - public function formatContent(string $original): string - { - return json_encode( - $this->sortObjectKeysRecursively( - json_decode( - $original, - false, - 512, - JSON_THROW_ON_ERROR - ) - ), - JSON_THROW_ON_ERROR - ); - } - - /** - * @param mixed $value - * @return mixed - */ - private function sortObjectKeysRecursively($value) - { - if (is_object($value)) { - $value = get_object_vars($value); - ksort($value); - return (object)$value; - } - - if (is_array($value)) { - return array_map([$this, 'sortObjectKeysRecursively'], $value); - } - - return $value; - } - }, + new Fixtures\Formatters\JsonObjectKeySorter(), new Formatter\DockerPipeFormatter( 'ghcr.io/jqlang/jq', // image [], // options @@ -224,8 +186,8 @@ return (new PhpCsFixer\Config()) ), ), - ], - ], + ] + ), ]); ``` @@ -340,27 +302,28 @@ Example with your own custom class: ```php final class MyFormatter extends AbstractStringFormatter { + public function __construct() + { + // It is required to pass a value to the parent constructor as a first argument. This value is used to + // invalidate the cache when your formatter logic is changed (e.g. versioning) or its settings change. + // The bare minimum is to pass the class name, but you can also pass a more complex value (e.g. an array + // of settings). Avoid passing objects though, especially non-serializable ones. + parent::__construct(self::class); + } + protected function formatContent(string $original): string { return 'new content'; } } -['formatters' => [ new MyFormatter('1.0') ]] -``` - -Example with an anonymous class: - -```php -['formatters' => [ - new class ('1.0') extends AbstractStringFormatter - { - protected function formatContent(string $original): string - { - return 'new content'; - } - } -]] +return (new PhpCsFixer\Config()) + ->registerCustomFixers([new BlockStringFixer()]) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config([ + 'TEXT' => new MyFormatter(), + ]), + ]); ``` ### [ChainFormatter](./src/Formatter/ChainFormatter.php) @@ -370,11 +333,17 @@ input of the next one. Example: - ```php - ['formatters' => [ new ChainFormatter( - new FirstFormatter(), - new SecondFormatter(), - ) ]] +```php +return (new PhpCsFixer\Config()) + ->registerCustomFixers([new BlockStringFixer()]) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config([ + 'JSON' => new ChainFormatter( + new FirstFormatter(), + new SecondFormatter(), + ), + ]), + ]); ``` ### [CliPipeFormatter](./src/Formatter/CliPipeFormatter.php) @@ -385,12 +354,22 @@ to such external executables. Example: ```php -['formatters' => [ new CliPipeFormatter( - versionValueOrCommand: '1.0', // Either a version as a string, or the command to get the version (as an array). - formatCommand: ['cmd' => 'jfmt -'], // An array defining the external command to do the formatting. - interpolationCodec: new PlainStringCodec(), // A codec for handling interpolations; depends on the content being formatted. - lineEndingNormalizer: null, // A normalizer for handling end-of-line characters. -) ]] +return (new PhpCsFixer\Config()) + ->registerCustomFixers([new BlockStringFixer()]) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config([ + 'J' => new CliPipeFormatter( + // Either a version as a string, or the command to get the version (as an array). + versionValueOrCommand: '1.0', + // An array defining the external command to do the formatting. + formatCommand: ['cmd' => 'jfmt -'], + // A codec for handling placeholers in template strings; depends on the content being formatted. + interpolationCodec: new PlainStringCodec(), + // A normalizer for handling end-of-line characters. + lineEndingNormalizer: null + ) + ]), + ]); ``` The command definition (for version detection or formatting) is an array with the following structure: @@ -407,14 +386,26 @@ tools. This formatter exists to take advantage of that. Example: ```php -['formatters' => [ new DockerPipeFormatter( - image: 'ghcr.io/jqlang/jq', // The docker image; might contain url, tag or even the digest. - options: ['-e', 'SOME_ENV=value'], // Optional docker arguments, such as for setting env vars. - command: ['bin/tool', '--dry-run', '-'], // The command to run within the container, including any arguments. - pullMode: 'always', // How/when the image should be pulled: 'never', 'always' or 'missing'. - interpolationCodec: new PlainStringCodec(), // A codec for handling interpolations; depends on the content being formatted. - lineEndingNormalizer: null, // A normalizer for handling end-of-line characters. -) ]] +return (new PhpCsFixer\Config()) + ->registerCustomFixers([new BlockStringFixer()]) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config([ + 'JSON' => new DockerPipeFormatter( + // The docker image; might contain url, tag or even the digest. + image: 'ghcr.io/jqlang/jq', + // Optional docker arguments, such as for setting env vars. + options: ['-e', 'SOME_ENV=value'], + // The command to run within the container, including any arguments. + command: ['bin/tool', '--dry-run', '-'], + // How/when the image should be pulled: 'never', 'always' or 'missing'. + pullMode: 'always', + // A codec for handling interpolations; depends on the content being formatted. + interpolationCodec: new PlainStringCodec(), + // A normalizer for handling end-of-line characters. + lineEndingNormalizer: null, + ) + ]), + ]); ``` ### [SimpleLineFormatter](./src/Formatter/SimpleLineFormatter.php) @@ -424,12 +415,22 @@ A formatter that normalizes indentation and removes any trailing whitespace at t Example: ```php -['formatters' => [ new SimpleLineFormatter( - indentSize: 4, // The number of spaces defining one indentation level in your project. - indentChar: "\t", // The actual character used for indentation (space or tab). - interpolationCodec: new PlainStringCodec(), // A codec for handling interpolations; depends on the content being formatted. - lineEndingNormalizer: null, // A normalizer for handling end-of-line characters. -) ]] +return (new PhpCsFixer\Config()) + ->registerCustomFixers([new BlockStringFixer()]) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config([ + 'TEXT' => new SimpleLineFormatter( + // The number of spaces defining one indentation level in your project. + indentSize: 4, + // The actual character used for indentation (space or tab). + indentChar: "\t", + // A codec for handling interpolations; depends on the content being formatted. + interpolationCodec: new PlainStringCodec(), + // A normalizer for handling end-of-line characters. + lineEndingNormalizer: null, + ) + ]), + ]); ``` ### [WslPipeFormatter](./src/Formatter/WslPipeFormatter.php) diff --git a/src/Fixer/BlockStringFixer.php b/src/Fixer/BlockStringFixer.php index 5770893..0dcd84a 100644 --- a/src/Fixer/BlockStringFixer.php +++ b/src/Fixer/BlockStringFixer.php @@ -18,10 +18,10 @@ /** * @phpstan-type TFormatters array<0|non-empty-string, AbstractFormatter> - * @phpstan-type TDeserilizedConfig array{formatters: TFormatters} - * @phpstan-type TSerializedConfig array{formatters: string} + * @phpstan-type TDeserializedConfig array{formatters: TFormatters} + * @phpstan-type TSerializedConfig array{formatters?: string} * - * @implements ConfigurableFixerInterface + * @implements ConfigurableFixerInterface */ final class BlockStringFixer implements FixerInterface, ConfigurableFixerInterface { @@ -30,7 +30,7 @@ final class BlockStringFixer implements FixerInterface, ConfigurableFixerInterfa private ?FixerConfigurationResolverInterface $configurationDefinition = null; /** - * @var TDeserilizedConfig + * @var TDeserializedConfig */ private array $configuration; @@ -77,21 +77,19 @@ public function getConfigurationDefinition(): FixerConfigurationResolverInterfac ]); } - /** - * @param TDeserilizedConfig $configuration - * @return void - */ public function configure(array $configuration): void { - if (isset($configuration['formatters']) && !is_string($configuration['formatters'])) { + $formatters = $configuration['formatters'] ?? 'a:0:{}'; + if (($formatters = @unserialize($formatters, ['allowed_classes' => true])) === false) { throw new InvalidArgumentException( 'BlockStringFixer configuration is not valid. ' . 'Did you set it up in your PHP CS Fixer config with `BlockStringFixer::config()`?' ); } + // @phpstan-ignore assign.propertyType $this->configuration = [ - 'formatters' => unserialize($configuration['formatters'] ?? 'a:0:{}', ['allowed_classes' => true]), + 'formatters' => $formatters, ]; } diff --git a/src/Formatter/AbstractStringFormatter.php b/src/Formatter/AbstractStringFormatter.php index 8cd9e7f..75f56ca 100644 --- a/src/Formatter/AbstractStringFormatter.php +++ b/src/Formatter/AbstractStringFormatter.php @@ -21,27 +21,28 @@ * ```php * final class MyFormatter extends AbstractStringFormatter * { + * public function __construct() + * { + * // It is required to pass a value to the parent constructor as a first argument. This value is used to + * // invalidate the cache when your formatter logic is changed (e.g. versioning) or its settings change. + * // The bare minimum is to pass the class name, but you can also pass a more complex value (e.g. an array + * // of settings). Avoid passing objects though, especially non-serializable ones. + * parent::__construct(self::class); + * } + * * protected function formatContent(string $original): string * { * return 'new content'; * } * } * - * ['formatters' => [ new MyFormatter('1.0') ]] - * ``` - * - * Example with an anonymous class: - * - * ```php - * ['formatters' => [ - * new class ('1.0') extends AbstractStringFormatter - * { - * protected function formatContent(string $original): string - * { - * return 'new content'; - * } - * } - * ]] + * return (new PhpCsFixer\Config()) + * ->registerCustomFixers([new BlockStringFixer()]) + * ->setRules([ + * BlockStringFixer::NAME => BlockStringFixer::config([ + * 'TEXT' => new MyFormatter(), + * ]), + * ]); * ``` */ abstract class AbstractStringFormatter extends AbstractFormatter diff --git a/src/Formatter/ChainFormatter.php b/src/Formatter/ChainFormatter.php index d4c6454..859894a 100644 --- a/src/Formatter/ChainFormatter.php +++ b/src/Formatter/ChainFormatter.php @@ -10,11 +10,17 @@ * * Example: * - * ```php - * ['formatters' => [ new ChainFormatter( - * new FirstFormatter(), - * new SecondFormatter(), - * ) ]] + * ```php + * return (new PhpCsFixer\Config()) + * ->registerCustomFixers([new BlockStringFixer()]) + * ->setRules([ + * BlockStringFixer::NAME => BlockStringFixer::config([ + * 'JSON' => new ChainFormatter( + * new FirstFormatter(), + * new SecondFormatter(), + * ), + * ]), + * ]); * ``` */ final class ChainFormatter extends AbstractFormatter diff --git a/src/Formatter/CliPipeFormatter.php b/src/Formatter/CliPipeFormatter.php index ae4a288..8195e8a 100644 --- a/src/Formatter/CliPipeFormatter.php +++ b/src/Formatter/CliPipeFormatter.php @@ -14,12 +14,22 @@ * Example: * * ```php - * ['formatters' => [ new CliPipeFormatter( - * versionValueOrCommand: '1.0', // Either a version as a string, or the command to get the version (as an array). - * formatCommand: ['cmd' => 'jfmt -'], // An array defining the external command to do the formatting. - * interpolationCodec: new PlainStringCodec(), // A codec for handling interpolations; depends on the content being formatted. - * lineEndingNormalizer: null, // A normalizer for handling end-of-line characters. - * ) ]] + * return (new PhpCsFixer\Config()) + * ->registerCustomFixers([new BlockStringFixer()]) + * ->setRules([ + * BlockStringFixer::NAME => BlockStringFixer::config([ + * 'J' => new CliPipeFormatter( + * // Either a version as a string, or the command to get the version (as an array). + * versionValueOrCommand: '1.0', + * // An array defining the external command to do the formatting. + * formatCommand: ['cmd' => 'jfmt -'], + * // A codec for handling placeholers in template strings; depends on the content being formatted. + * interpolationCodec: new PlainStringCodec(), + * // A normalizer for handling end-of-line characters. + * lineEndingNormalizer: null + * ) + * ]), + * ]); * ``` * * The command definition (for version detection or formatting) is an array with the following structure: diff --git a/src/Formatter/DockerPipeFormatter.php b/src/Formatter/DockerPipeFormatter.php index 1e0be30..ff7d2db 100644 --- a/src/Formatter/DockerPipeFormatter.php +++ b/src/Formatter/DockerPipeFormatter.php @@ -17,14 +17,26 @@ * Example: * * ```php - * ['formatters' => [ new DockerPipeFormatter( - * image: 'ghcr.io/jqlang/jq', // The docker image; might contain url, tag or even the digest. - * options: ['-e', 'SOME_ENV=value'], // Optional docker arguments, such as for setting env vars. - * command: ['bin/tool', '--dry-run', '-'], // The command to run within the container, including any arguments. - * pullMode: 'always', // How/when the image should be pulled: 'never', 'always' or 'missing'. - * interpolationCodec: new PlainStringCodec(), // A codec for handling interpolations; depends on the content being formatted. - * lineEndingNormalizer: null, // A normalizer for handling end-of-line characters. - * ) ]] + * return (new PhpCsFixer\Config()) + * ->registerCustomFixers([new BlockStringFixer()]) + * ->setRules([ + * BlockStringFixer::NAME => BlockStringFixer::config([ + * 'JSON' => new DockerPipeFormatter( + * // The docker image; might contain url, tag or even the digest. + * image: 'ghcr.io/jqlang/jq', + * // Optional docker arguments, such as for setting env vars. + * options: ['-e', 'SOME_ENV=value'], + * // The command to run within the container, including any arguments. + * command: ['bin/tool', '--dry-run', '-'], + * // How/when the image should be pulled: 'never', 'always' or 'missing'. + * pullMode: 'always', + * // A codec for handling interpolations; depends on the content being formatted. + * interpolationCodec: new PlainStringCodec(), + * // A normalizer for handling end-of-line characters. + * lineEndingNormalizer: null, + * ) + * ]), + * ]); * ``` * * @phpstan-type TDockerImageDetails array{platform: string, digest: string} diff --git a/src/Formatter/SimpleLineFormatter.php b/src/Formatter/SimpleLineFormatter.php index 53e43f9..f7fa093 100644 --- a/src/Formatter/SimpleLineFormatter.php +++ b/src/Formatter/SimpleLineFormatter.php @@ -12,12 +12,22 @@ * Example: * * ```php - * ['formatters' => [ new SimpleLineFormatter( - * indentSize: 4, // The number of spaces defining one indentation level in your project. - * indentChar: "\t", // The actual character used for indentation (space or tab). - * interpolationCodec: new PlainStringCodec(), // A codec for handling interpolations; depends on the content being formatted. - * lineEndingNormalizer: null, // A normalizer for handling end-of-line characters. - * ) ]] + * return (new PhpCsFixer\Config()) + * ->registerCustomFixers([new BlockStringFixer()]) + * ->setRules([ + * BlockStringFixer::NAME => BlockStringFixer::config([ + * 'TEXT' => new SimpleLineFormatter( + * // The number of spaces defining one indentation level in your project. + * indentSize: 4, + * // The actual character used for indentation (space or tab). + * indentChar: "\t", + * // A codec for handling interpolations; depends on the content being formatted. + * interpolationCodec: new PlainStringCodec(), + * // A normalizer for handling end-of-line characters. + * lineEndingNormalizer: null, + * ) + * ]), + * ]); * ``` */ class SimpleLineFormatter extends AbstractStringFormatter diff --git a/src/Readmerator/Readmerator.php b/src/Readmerator/Readmerator.php index 2761010..23b6cd5 100644 --- a/src/Readmerator/Readmerator.php +++ b/src/Readmerator/Readmerator.php @@ -19,16 +19,16 @@ public static function render(): void self::readFile(self::README_TEMPLATE), [ '{{PROJECT_NAME}}' => 'uuf6429/php-cs-fixer-blockstring', - '{{EXAMPLE_CONFIG}}' => file_get_contents("$projectRoot/tests/fixtures/example-config.php"), + '{{EXAMPLE_CONFIG}}' => file_get_contents("$projectRoot/tests/Fixtures/Scenarios/example/config.php"), '{{EXAMPLE_INPUT}}' => str_replace( [' ', "\t"], ['·', '───→'], - self::readFile("$projectRoot/tests/fixtures/example-input.php"), + self::readFile("$projectRoot/tests/Fixtures/Scenarios/example/input.php"), ), '{{EXAMPLE_OUTPUT}}' => str_replace( [' ', "\t"], ['·', '───→'], - self::readFile("$projectRoot/tests/fixtures/example-output.php") + self::readFile("$projectRoot/tests/Fixtures/Scenarios/example/output.php") ), '{{FORMATTERS}}' => rtrim(implode( "\n", diff --git a/tests/Fixtures/Formatters/HtmlTagStripper.php b/tests/Fixtures/Formatters/HtmlTagStripper.php new file mode 100644 index 0000000..4dba4b3 --- /dev/null +++ b/tests/Fixtures/Formatters/HtmlTagStripper.php @@ -0,0 +1,22 @@ +comment = $comment; + } + + public function formatContent(string $original): string + { + return json_encode( + array_merge( + (array)json_decode($original, false, 512, JSON_THROW_ON_ERROR), + ['_comment' => $this->comment] + ), + JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + ); + } +} diff --git a/tests/Fixtures/Formatters/JsonObjectKeySorter.php b/tests/Fixtures/Formatters/JsonObjectKeySorter.php new file mode 100644 index 0000000..7b38bae --- /dev/null +++ b/tests/Fixtures/Formatters/JsonObjectKeySorter.php @@ -0,0 +1,51 @@ +sortObjectKeysRecursively( + json_decode( + $original, + false, + 512, + JSON_THROW_ON_ERROR + ) + ), + JSON_THROW_ON_ERROR + ); + } + + /** + * @param mixed $value + * @return mixed + */ + private function sortObjectKeysRecursively($value) + { + if (is_object($value)) { + $value = get_object_vars($value); + ksort($value); + return (object)$value; + } + + if (is_array($value)) { + return array_map([$this, 'sortObjectKeysRecursively'], $value); + } + + return $value; + } +} diff --git a/tests/Fixtures/Formatters/TagWrapper.php b/tests/Fixtures/Formatters/TagWrapper.php new file mode 100644 index 0000000..edc4f0f --- /dev/null +++ b/tests/Fixtures/Formatters/TagWrapper.php @@ -0,0 +1,25 @@ +tagName = $tagName; + } + + protected function formatContent(string $original): string + { + return "<{$this->tagName}>$originaltagName}>"; + } +} diff --git a/tests/Fixtures/Scenarios/caching/cache-v1.json b/tests/Fixtures/Scenarios/caching/cache-v1.json new file mode 100644 index 0000000..9c4e8d0 --- /dev/null +++ b/tests/Fixtures/Scenarios/caching/cache-v1.json @@ -0,0 +1,7 @@ +{ + "rules": { + "Uuf6429\/block_string": { + "formatters": "a:1:{s:4:\"JSON\";O:68:\"uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter\":5:{s:77:\"\u0000uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter\u0000comment\";s:16:\"JsonCommenter v1\";s:76:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000objectIndex\";i:0;s:21:\"\u0000*\u0000interpolationCodec\";O:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\":0:{}s:85:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000lineEndingNormalizer\";O:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\":2:{s:83:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeLinesTo\";s:4:\"noop\";s:87:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeFinalLineTo\";s:4:\"noop\";}s:75:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractFormatter\u0000cacheFingerprint\";a:3:{i:0;s:87:\"uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter (JsonCommenter v1)\";i:1;a:1:{i:0;s:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\";}i:2;a:3:{i:0;s:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\";i:1;s:4:\"noop\";i:2;s:4:\"noop\";}}}}" + } + } +} diff --git a/tests/Fixtures/Scenarios/caching/cache-v2.json b/tests/Fixtures/Scenarios/caching/cache-v2.json new file mode 100644 index 0000000..d87141a --- /dev/null +++ b/tests/Fixtures/Scenarios/caching/cache-v2.json @@ -0,0 +1,7 @@ +{ + "rules": { + "Uuf6429\/block_string": { + "formatters": "a:1:{s:4:\"JSON\";O:68:\"uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter\":5:{s:77:\"\u0000uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter\u0000comment\";s:16:\"JsonCommenter v2\";s:76:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000objectIndex\";i:0;s:21:\"\u0000*\u0000interpolationCodec\";O:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\":0:{}s:85:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000lineEndingNormalizer\";O:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\":2:{s:83:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeLinesTo\";s:4:\"noop\";s:87:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeFinalLineTo\";s:4:\"noop\";}s:75:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractFormatter\u0000cacheFingerprint\";a:3:{i:0;s:87:\"uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter (JsonCommenter v2)\";i:1;a:1:{i:0;s:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\";}i:2;a:3:{i:0;s:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\";i:1;s:4:\"noop\";i:2;s:4:\"noop\";}}}}" + } + } +} diff --git a/tests/Fixtures/Scenarios/caching/config-v1.php b/tests/Fixtures/Scenarios/caching/config-v1.php new file mode 100644 index 0000000..c55e623 --- /dev/null +++ b/tests/Fixtures/Scenarios/caching/config-v1.php @@ -0,0 +1,15 @@ +registerCustomFixers([new BlockStringFixer()]) + ->setRiskyAllowed(true) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config( + [ + 'JSON' => new JsonCommenter('JsonCommenter v1'), + ] + ) + ]); diff --git a/tests/Fixtures/Scenarios/caching/config-v2.php b/tests/Fixtures/Scenarios/caching/config-v2.php new file mode 100644 index 0000000..e3d604e --- /dev/null +++ b/tests/Fixtures/Scenarios/caching/config-v2.php @@ -0,0 +1,15 @@ +registerCustomFixers([new BlockStringFixer()]) + ->setRiskyAllowed(true) + ->setRules([ + BlockStringFixer::NAME => BlockStringFixer::config( + [ + 'JSON' => new JsonCommenter('JsonCommenter v2'), + ] + ) + ]); diff --git a/tests/fixtures/simple-input.php b/tests/Fixtures/Scenarios/caching/input.php similarity index 100% rename from tests/fixtures/simple-input.php rename to tests/Fixtures/Scenarios/caching/input.php diff --git a/tests/fixtures/simple-output-v1.php b/tests/Fixtures/Scenarios/caching/output-v1.php similarity index 64% rename from tests/fixtures/simple-output-v1.php rename to tests/Fixtures/Scenarios/caching/output-v1.php index 36be182..999d7c3 100644 --- a/tests/fixtures/simple-output-v1.php +++ b/tests/Fixtures/Scenarios/caching/output-v1.php @@ -2,9 +2,9 @@ echo <<<'JSON' { - "_comment": "JsonFormatterV1", "users": [], - "ascending": false + "ascending": false, + "_comment": "JsonCommenter v1" } JSON; diff --git a/tests/fixtures/simple-output-v2.php b/tests/Fixtures/Scenarios/caching/output-v2.php similarity index 64% rename from tests/fixtures/simple-output-v2.php rename to tests/Fixtures/Scenarios/caching/output-v2.php index 01acd90..6897ea5 100644 --- a/tests/fixtures/simple-output-v2.php +++ b/tests/Fixtures/Scenarios/caching/output-v2.php @@ -2,9 +2,9 @@ echo <<<'JSON' { - "_comment": "JsonFormatterV2", "users": [], - "ascending": false + "ascending": false, + "_comment": "JsonCommenter v2" } JSON; diff --git a/tests/fixtures/example-config.php b/tests/Fixtures/Scenarios/example/config.php similarity index 63% rename from tests/fixtures/example-config.php rename to tests/Fixtures/Scenarios/example/config.php index 5262b6f..61904f0 100644 --- a/tests/fixtures/example-config.php +++ b/tests/Fixtures/Scenarios/example/config.php @@ -3,6 +3,7 @@ use uuf6429\PhpCsFixerBlockstring\Fixer\BlockStringFixer; use uuf6429\PhpCsFixerBlockstring\Formatter; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\GeneratedTokenCodec; +use uuf6429\PhpCsFixerBlockstringTests\Fixtures; return (new PhpCsFixer\Config()) ->registerCustomFixers([new BlockStringFixer()]) @@ -31,46 +32,7 @@ // 1. A custom formatter that sorts object keys. // 2. A docker-based formatter that runs the json through jq. 'JSON' => new Formatter\ChainFormatter( - new class extends Formatter\AbstractStringFormatter { - public function __construct() - { - parent::__construct('ObjectKeySorter v1.0', new GeneratedTokenCodec('"__PHP_VAR_%d__"')); - } - - public function formatContent(string $original): string - { - return json_encode( - $this->sortObjectKeysRecursively( - json_decode( - $original, - false, - 512, - JSON_THROW_ON_ERROR - ) - ), - JSON_THROW_ON_ERROR - ); - } - - /** - * @param mixed $value - * @return mixed - */ - private function sortObjectKeysRecursively($value) - { - if (is_object($value)) { - $value = get_object_vars($value); - ksort($value); - return (object)$value; - } - - if (is_array($value)) { - return array_map([$this, 'sortObjectKeysRecursively'], $value); - } - - return $value; - } - }, + new Fixtures\Formatters\JsonObjectKeySorter(), new Formatter\DockerPipeFormatter( 'ghcr.io/jqlang/jq', // image [], // options diff --git a/tests/fixtures/example-input.php b/tests/Fixtures/Scenarios/example/input.php similarity index 100% rename from tests/fixtures/example-input.php rename to tests/Fixtures/Scenarios/example/input.php diff --git a/tests/fixtures/example-output.php b/tests/Fixtures/Scenarios/example/output.php similarity index 100% rename from tests/fixtures/example-output.php rename to tests/Fixtures/Scenarios/example/output.php diff --git a/tests/fixtures/sqlformat.php b/tests/Fixtures/Scenarios/example/sqlformat.php similarity index 100% rename from tests/fixtures/sqlformat.php rename to tests/Fixtures/Scenarios/example/sqlformat.php diff --git a/tests/Integration/CacheTest.php b/tests/Integration/CacheTest.php index b4913a7..e4b49eb 100644 --- a/tests/Integration/CacheTest.php +++ b/tests/Integration/CacheTest.php @@ -27,14 +27,14 @@ public static function setUpBeforeClass(): void self::$cacheFile = self::$workspace . '/cache.json'; mkdir(self::$workspace); - copy(__DIR__ . '/../fixtures/simple-input.php', self::$inputFile); + copy(__DIR__ . '/../Fixtures/Scenarios/caching/input.php', self::$inputFile); } public static function tearDownAfterClass(): void { - @unlink(self::$inputFile); - @unlink(self::$cacheFile); - @rmdir(self::$workspace); + unlink(self::$inputFile); + unlink(self::$cacheFile); + rmdir(self::$workspace); } /** @@ -58,7 +58,7 @@ public function testCacheReuse( PHP_BINARY, self::PCF_BINARY_PATH, 'fix', - '--config=' . __DIR__ . "/../fixtures/simple-config-{$formatterVersion}.php", + '--config=' . __DIR__ . "/../Fixtures/Scenarios/caching/config-{$formatterVersion}.php", '--cache-file=' . self::$cacheFile, '--allow-unsupported-php-version=yes', '--show-progress=none', @@ -73,12 +73,11 @@ public function testCacheReuse( ); $process->mustRun(); - $output = $process->getErrorOutput() . $process->getOutput(); $this->assertSame($expectedCacheFileExistence, $cacheFileExistence); - $this->assertFileEquals(__DIR__ . "/../fixtures/simple-output-{$formatterVersion}.php", self::$inputFile); + $this->assertFileEquals(__DIR__ . "/../Fixtures/Scenarios/caching/output-{$formatterVersion}.php", self::$inputFile); $this->assertStringContainsString($expectedProcessOutput, $output); - $this->assertJsonFileWithinJsonFile(__DIR__ . "/../fixtures/simple-cache-{$formatterVersion}.json", self::$cacheFile); + $this->assertJsonFileWithinJsonFile(__DIR__ . "/../Fixtures/Scenarios/caching/cache-{$formatterVersion}.json", self::$cacheFile); } } diff --git a/tests/Integration/ExampleTest.php b/tests/Integration/ExampleTest.php index 671d186..4385cda 100644 --- a/tests/Integration/ExampleTest.php +++ b/tests/Integration/ExampleTest.php @@ -23,14 +23,14 @@ public function testExample(): void $tempFile = tempnam(sys_get_temp_dir(), ''); try { - copy(__DIR__ . '/../fixtures/example-input.php', $tempFile); + copy(__DIR__ . '/../Fixtures/Scenarios/example/input.php', $tempFile); $process = new Process( [ PHP_BINARY, self::PCF_BINARY_PATH, 'fix', '--using-cache=no', - '--config=' . __DIR__ . '/../fixtures/example-config.php', + '--config=' . __DIR__ . '/../Fixtures/Scenarios/example/config.php', '--sequential', '-vvv', '--diff', @@ -44,7 +44,7 @@ public function testExample(): void $process->mustRun(); - $this->assertFileEquals(__DIR__ . '/../fixtures/example-output.php', $tempFile); + $this->assertFileEquals(__DIR__ . '/../Fixtures/Scenarios/example/output.php', $tempFile); } finally { @unlink($tempFile); } diff --git a/tests/Unit/Fixer/BlockStringFixerTest.php b/tests/Unit/Fixer/BlockStringFixerTest.php index 7f1321a..cc9a81b 100644 --- a/tests/Unit/Fixer/BlockStringFixerTest.php +++ b/tests/Unit/Fixer/BlockStringFixerTest.php @@ -7,13 +7,13 @@ use PHPUnit\Framework\TestCase; use SplFileInfo; use uuf6429\PhpCsFixerBlockstring\Fixer\BlockStringFixer; -use uuf6429\PhpCsFixerBlockstring\Formatter\AbstractStringFormatter; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\GeneratedTokenCodec; +use uuf6429\PhpCsFixerBlockstringTests\Fixtures; /** * @internal * - * @phpstan-import-type TFormatterConfig from BlockStringFixer + * @phpstan-import-type TSerializedConfig from BlockStringFixer */ final class BlockStringFixerTest extends TestCase { @@ -39,17 +39,27 @@ public function testGetDefinition(): void (new BlockStringFixer())->getDefinition(); } - public function testConfigurationIsRequired(): void + public function testGetConfigurationDefinition(): void { - $this->expectExceptionObject( - new InvalidArgumentException('Configuration for fixer Uuf6429/block_string is required.') - ); + $this->expectNotToPerformAssertions(); + + (new BlockStringFixer())->getConfigurationDefinition(); + } + + public function testConfigureWithInvalidConfiguration(): void + { + $this->expectExceptionObject(new InvalidArgumentException('BlockStringFixer configuration is not valid.')); + + (new BlockStringFixer())->configure(['formatters' => 'invalid']); + } - (new BlockStringFixer())->fix(new SplFileInfo('test.php'), new Tokens()); + public function testConfig(): void + { + $this->assertSame(['formatters' => 'a:0:{}'], BlockStringFixer::config([])); } /** - * @param TFormatterConfig $config + * @param TSerializedConfig $config * @dataProvider provideFixCases */ public function testApplyFix(array $config, string $input, string $expected): void @@ -65,12 +75,12 @@ public function testApplyFix(array $config, string $input, string $expected): vo } /** - * @return iterable + * @return iterable */ public static function provideFixCases(): iterable { yield 'nowdoc with unregistered delimiter should be left unchanged' => [ - 'config' => ['formatters' => []], + 'config' => BlockStringFixer::config([]), 'input' => <<<'PHP' [ - 'config' => [ - 'formatters' => [ - 'HTML' => new class extends AbstractStringFormatter { - public function __construct() - { - parent::__construct('1'); - } - - protected function formatContent(string $original): string - { - return strip_tags($original); - } - }, - ], - ], + 'config' => BlockStringFixer::config( + [ + 'HTML' => new fixtures\Formatters\HtmlTagStripper(null), + ] + ), 'input' => <<<'PHP' [ - 'config' => [ - 'formatters' => [ - new class extends AbstractStringFormatter { - public function __construct() - { - parent::__construct('2'); - } - - protected function formatContent(string $original): string - { - return "$original"; - } - }, - 'HTML' => new class extends AbstractStringFormatter { - public function __construct() - { - parent::__construct('3'); - } - - protected function formatContent(string $original): string - { - return "$original"; - } - }, - ], - ], + 'config' => BlockStringFixer::config( + [ + new fixtures\Formatters\TagWrapper('def'), + 'HTML' => new fixtures\Formatters\TagWrapper('htm'), + ] + ), 'input' => <<<'PHP' [ - 'config' => [ - 'formatters' => [ - 'HTML' => new class extends AbstractStringFormatter { - public function __construct() - { - parent::__construct('4', new GeneratedTokenCodec()); - } - - protected function formatContent(string $original): string - { - return strip_tags($original); - } - }, - ], - ], + 'config' => BlockStringFixer::config( + [ + 'HTML' => new fixtures\Formatters\HtmlTagStripper(new GeneratedTokenCodec()), + ] + ), 'input' => <<<'PHP' [ - 'config' => [ - 'formatters' => [ - 'HTML' => new class extends AbstractStringFormatter { - public function __construct() - { - parent::__construct('5'); - } - - protected function formatContent(string $original): string - { - return strip_tags($original); - } - }, - ], - ], + 'config' => BlockStringFixer::config( + [ + 'HTML' => new fixtures\Formatters\HtmlTagStripper(null), + ] + ), 'input' => "Hello world!\r\n HTML;\r\n", 'expected' => "assertSame('"fingerprint"', json_encode($formatter)); + $this->assertSame('fingerprint', $formatter->getCacheFingerprint()); } } diff --git a/tests/fixtures/simple-cache-v1.json b/tests/fixtures/simple-cache-v1.json deleted file mode 100644 index 33f7226..0000000 --- a/tests/fixtures/simple-cache-v1.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rules": { - "Uuf6429\/block_string": { - "formatters": "a:1:{s:4:\"JSON\";O:15:\"JsonFormatterV1\":4:{s:76:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000objectIndex\";i:0;s:21:\"\u0000*\u0000interpolationCodec\";O:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\":0:{}s:85:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000lineEndingNormalizer\";O:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\":2:{s:83:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeLinesTo\";s:4:\"noop\";s:87:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeFinalLineTo\";s:4:\"noop\";}s:75:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractFormatter\u0000cacheFingerprint\";a:3:{i:0;s:15:\"JsonFormatterV1\";i:1;a:1:{i:0;s:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\";}i:2;a:3:{i:0;s:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\";i:1;s:4:\"noop\";i:2;s:4:\"noop\";}}}}" - } - } -} diff --git a/tests/fixtures/simple-cache-v2.json b/tests/fixtures/simple-cache-v2.json deleted file mode 100644 index dc1cd03..0000000 --- a/tests/fixtures/simple-cache-v2.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rules": { - "Uuf6429\/block_string": { - "formatters": "a:1:{s:4:\"JSON\";O:15:\"JsonFormatterV2\":4:{s:76:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000objectIndex\";i:0;s:21:\"\u0000*\u0000interpolationCodec\";O:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\":0:{}s:85:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000lineEndingNormalizer\";O:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\":2:{s:83:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeLinesTo\";s:4:\"noop\";s:87:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeFinalLineTo\";s:4:\"noop\";}s:75:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractFormatter\u0000cacheFingerprint\";a:3:{i:0;s:15:\"JsonFormatterV2\";i:1;a:1:{i:0;s:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\";}i:2;a:3:{i:0;s:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\";i:1;s:4:\"noop\";i:2;s:4:\"noop\";}}}}" - } - } -} diff --git a/tests/fixtures/simple-config-v1.php b/tests/fixtures/simple-config-v1.php deleted file mode 100644 index b59eb8a..0000000 --- a/tests/fixtures/simple-config-v1.php +++ /dev/null @@ -1,34 +0,0 @@ - self::class], - (array)json_decode($original, false, 512, JSON_THROW_ON_ERROR) - ), - JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT - ); - } -} - -return (new PhpCsFixer\Config()) - ->registerCustomFixers([new BlockStringFixer()]) - ->setRiskyAllowed(true) - ->setRules([ - BlockStringFixer::NAME => BlockStringFixer::config( - [ - 'JSON' => new JsonFormatterV1(), - ] - ) - ]); diff --git a/tests/fixtures/simple-config-v2.php b/tests/fixtures/simple-config-v2.php deleted file mode 100644 index eb2dced..0000000 --- a/tests/fixtures/simple-config-v2.php +++ /dev/null @@ -1,34 +0,0 @@ - self::class], - (array)json_decode($original, false, 512, JSON_THROW_ON_ERROR) - ), - JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT - ); - } -} - -return (new PhpCsFixer\Config()) - ->registerCustomFixers([new BlockStringFixer()]) - ->setRiskyAllowed(true) - ->setRules([ - BlockStringFixer::NAME => BlockStringFixer::config( - [ - 'JSON' => new JsonFormatterV2(), - ] - ) - ]); From 561e5c438e732ccd2272a24219c7af326a2906b9 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sat, 23 May 2026 12:00:14 +0200 Subject: [PATCH 09/11] Fix readme snippets --- README.md | 6 +++--- src/Formatter/ChainFormatter.php | 6 +++--- tests/Integration/ReadmeSnippetsTest.php | 7 +++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2b9c49b..6395761 100644 --- a/README.md +++ b/README.md @@ -339,12 +339,12 @@ return (new PhpCsFixer\Config()) ->setRules([ BlockStringFixer::NAME => BlockStringFixer::config([ 'JSON' => new ChainFormatter( - new FirstFormatter(), - new SecondFormatter(), + new CliPipeFormatter('v1', ['cmd' => 'some-old-tool']), + new SimpleLineFormatter(4, "\t"), ), ]), ]); - ``` +``` ### [CliPipeFormatter](./src/Formatter/CliPipeFormatter.php) diff --git a/src/Formatter/ChainFormatter.php b/src/Formatter/ChainFormatter.php index 859894a..9f02f61 100644 --- a/src/Formatter/ChainFormatter.php +++ b/src/Formatter/ChainFormatter.php @@ -16,12 +16,12 @@ * ->setRules([ * BlockStringFixer::NAME => BlockStringFixer::config([ * 'JSON' => new ChainFormatter( - * new FirstFormatter(), - * new SecondFormatter(), + * new CliPipeFormatter('v1', ['cmd' => 'some-old-tool']), + * new SimpleLineFormatter(4, "\t"), * ), * ]), * ]); - * ``` + * ``` */ final class ChainFormatter extends AbstractFormatter { diff --git a/tests/Integration/ReadmeSnippetsTest.php b/tests/Integration/ReadmeSnippetsTest.php index 06a4484..0c618a2 100644 --- a/tests/Integration/ReadmeSnippetsTest.php +++ b/tests/Integration/ReadmeSnippetsTest.php @@ -3,6 +3,7 @@ namespace uuf6429\PhpCsFixerBlockstringTests\Integration; use PHPUnit\Framework\TestCase; +use RuntimeException; /** * @internal @@ -32,11 +33,13 @@ protected function setUp(): void public function testSnippet(string $snippet): void { $snippet = <<<"PHP" + use uuf6429\PhpCsFixerBlockstring\Fixer\BlockStringFixer; use uuf6429\PhpCsFixerBlockstring\Formatter\AbstractFormatter; use uuf6429\PhpCsFixerBlockstring\Formatter\AbstractStringFormatter; + use uuf6429\PhpCsFixerBlockstring\Formatter\ChainFormatter; + use uuf6429\PhpCsFixerBlockstring\Formatter\CliPipeFormatter; use uuf6429\PhpCsFixerBlockstring\Formatter\DockerPipeFormatter; use uuf6429\PhpCsFixerBlockstring\Formatter\SimpleLineFormatter; - use uuf6429\PhpCsFixerBlockstring\Formatter\CliPipeFormatter; use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\PlainStringCodec; $snippet; @@ -54,7 +57,7 @@ public static function provideSnippets(): iterable { $content = file_get_contents(self::README_FILE); if ($content === false) { - throw new \RuntimeException('File could not be read: ' . self::README_FILE); + throw new RuntimeException('File could not be read: ' . self::README_FILE); } $content = explode("\n## ⭐️ Formatters\n", $content, 2); From 4312cabfa39bc588ae1d72929da8a834cec39c34 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sat, 23 May 2026 12:06:10 +0200 Subject: [PATCH 10/11] Simplify cache test to avoid platform-specific caveat --- tests/CustomAssertionsTrait.php | 52 ------------------- .../Fixtures/Scenarios/caching/cache-v1.json | 7 --- .../Fixtures/Scenarios/caching/cache-v2.json | 7 --- tests/Integration/CacheTest.php | 6 +-- 4 files changed, 2 insertions(+), 70 deletions(-) delete mode 100644 tests/CustomAssertionsTrait.php delete mode 100644 tests/Fixtures/Scenarios/caching/cache-v1.json delete mode 100644 tests/Fixtures/Scenarios/caching/cache-v2.json diff --git a/tests/CustomAssertionsTrait.php b/tests/CustomAssertionsTrait.php deleted file mode 100644 index 4f0a019..0000000 --- a/tests/CustomAssertionsTrait.php +++ /dev/null @@ -1,52 +0,0 @@ -assertFileExists($expectedFile); - $this->assertFileExists($actualSubsetFile); - $this->assertArraySubsetRecursive( - (array)json_decode((string)file_get_contents($expectedFile), true, 512, JSON_THROW_ON_ERROR), - (array)json_decode((string)file_get_contents($actualSubsetFile), true, 512, JSON_THROW_ON_ERROR), - sprintf( - 'Failed asserting that %s contains %s', - (string)realpath($actualSubsetFile), - (string)realpath($expectedFile) - ) - ); - } - - /** - * @param array $expected - * @param array $actual - */ - private function assertArraySubsetRecursive(array $expected, array $actual, string $message = '', string $path = '$'): void - { - foreach ($expected as $key => $value) { - $currentPath = is_string($key) && preg_match('/^\w+$/', $key) === 1 - ? "{$path}.{$key}" - : "{$path}[" . var_export($key, true) . "]"; - - $this->assertArrayHasKey($key, $actual, ltrim("{$message}\nMissing key at path: {$currentPath}")); - - if (!is_array($value) || !is_array($actual[$key])) { - $this->assertSame($value, $actual[$key], ltrim("{$message}\nMismatch at path: {$currentPath}")); - continue; - } - - $this->assertArraySubsetRecursive($value, $actual[$key], $message, $currentPath); - } - } -} diff --git a/tests/Fixtures/Scenarios/caching/cache-v1.json b/tests/Fixtures/Scenarios/caching/cache-v1.json deleted file mode 100644 index 9c4e8d0..0000000 --- a/tests/Fixtures/Scenarios/caching/cache-v1.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rules": { - "Uuf6429\/block_string": { - "formatters": "a:1:{s:4:\"JSON\";O:68:\"uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter\":5:{s:77:\"\u0000uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter\u0000comment\";s:16:\"JsonCommenter v1\";s:76:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000objectIndex\";i:0;s:21:\"\u0000*\u0000interpolationCodec\";O:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\":0:{}s:85:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000lineEndingNormalizer\";O:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\":2:{s:83:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeLinesTo\";s:4:\"noop\";s:87:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeFinalLineTo\";s:4:\"noop\";}s:75:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractFormatter\u0000cacheFingerprint\";a:3:{i:0;s:87:\"uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter (JsonCommenter v1)\";i:1;a:1:{i:0;s:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\";}i:2;a:3:{i:0;s:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\";i:1;s:4:\"noop\";i:2;s:4:\"noop\";}}}}" - } - } -} diff --git a/tests/Fixtures/Scenarios/caching/cache-v2.json b/tests/Fixtures/Scenarios/caching/cache-v2.json deleted file mode 100644 index d87141a..0000000 --- a/tests/Fixtures/Scenarios/caching/cache-v2.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rules": { - "Uuf6429\/block_string": { - "formatters": "a:1:{s:4:\"JSON\";O:68:\"uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter\":5:{s:77:\"\u0000uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter\u0000comment\";s:16:\"JsonCommenter v2\";s:76:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000objectIndex\";i:0;s:21:\"\u0000*\u0000interpolationCodec\";O:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\":0:{}s:85:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractStringFormatter\u0000lineEndingNormalizer\";O:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\":2:{s:83:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeLinesTo\";s:4:\"noop\";s:87:\"\u0000uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\u0000changeFinalLineTo\";s:4:\"noop\";}s:75:\"\u0000uuf6429\\PhpCsFixerBlockstring\\Formatter\\AbstractFormatter\u0000cacheFingerprint\";a:3:{i:0;s:87:\"uuf6429\\PhpCsFixerBlockstringTests\\Fixtures\\Formatters\\JsonCommenter (JsonCommenter v2)\";i:1;a:1:{i:0;s:65:\"uuf6429\\PhpCsFixerBlockstring\\InterpolationCodec\\PlainStringCodec\";}i:2;a:3:{i:0;s:68:\"uuf6429\\PhpCsFixerBlockstring\\LineEndingNormalizer\\DefaultNormalizer\";i:1;s:4:\"noop\";i:2;s:4:\"noop\";}}}}" - } - } -} diff --git a/tests/Integration/CacheTest.php b/tests/Integration/CacheTest.php index e4b49eb..d8b4ea0 100644 --- a/tests/Integration/CacheTest.php +++ b/tests/Integration/CacheTest.php @@ -5,15 +5,12 @@ use JsonException; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; -use uuf6429\PhpCsFixerBlockstringTests\CustomAssertionsTrait; /** * @internal */ final class CacheTest extends TestCase { - use CustomAssertionsTrait; - private const PCF_BINARY_PATH = __DIR__ . '/../../vendor/bin/php-cs-fixer'; private static string $workspace; @@ -78,6 +75,7 @@ public function testCacheReuse( $this->assertSame($expectedCacheFileExistence, $cacheFileExistence); $this->assertFileEquals(__DIR__ . "/../Fixtures/Scenarios/caching/output-{$formatterVersion}.php", self::$inputFile); $this->assertStringContainsString($expectedProcessOutput, $output); - $this->assertJsonFileWithinJsonFile(__DIR__ . "/../Fixtures/Scenarios/caching/cache-{$formatterVersion}.json", self::$cacheFile); + $this->assertFileExists(self::$cacheFile); + $this->assertStringContainsString("JsonCommenter (JsonCommenter {$formatterVersion})", (string)file_get_contents(self::$cacheFile)); } } From e926b2e2a342205db0f33847811b249c364ecb07 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sat, 23 May 2026 12:11:19 +0200 Subject: [PATCH 11/11] Fix PHPStan config --- phpstan.dist.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.dist.neon b/phpstan.dist.neon index fb96261..bdc9c26 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -9,6 +9,6 @@ parameters: - ./tests - ./bootstrap.php excludePaths: - - ./tests/fixtures/* + - ./tests/Fixtures/Scenarios/* ignoreErrors: - '#Dynamic call to static method PHPUnit\\Framework\\.*#'