Hi!
When the Uuf6429/block_string rule is enabled with a formatter object (e.g. CliPipeFormatter) in its configuration, php-cs-fixer's signature check fails on every subsequent run, so the cache file is rewritten but never produces a hit. Removing the rule restores normal cache behaviour.
Environment
uuf6429/php-cs-fixer-blockstring: 1.0.5
friendsofphp/php-cs-fixer: 3.95.1
- PHP:
8.5.4
- OS: Linux (Docker)
Reproduction
.php-cs-fixer.dist.php (minimised):
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
use uuf6429\PhpCsFixerBlockstring\Fixer\BlockStringFixer;
use uuf6429\PhpCsFixerBlockstring\Formatter\CliPipeFormatter;
use uuf6429\PhpCsFixerBlockstring\InterpolationCodec\GeneratedTokenCodec;
return new Config()
->registerCustomFixers([new BlockStringFixer()])
->setCacheFile(__DIR__ . '/var/cache/php-cs-fixer/.php-cs-fixer.cache')
->setFinder(new Finder()->in([__DIR__]))
->setRiskyAllowed(true)
->setRules([
'Uuf6429/block_string' => [
'formatters' => [
'SQL' => new CliPipeFormatter(
['cmd' => ['sqlfluff', '--version']],
['cmd' => ['sqlfluff', 'fix', '-']],
new GeneratedTokenCodec(),
true,
),
],
],
]);
Steps:
vendor/bin/php-cs-fixer fix — runs against the whole tree, writes cache file.
vendor/bin/php-cs-fixer fix — should re-use the cache and skip unchanged files.
Expected: second run skips files (cache hit).
Actual: second run re-processes every file. Cache file is rewritten but never consulted.
Root cause
php-cs-fixer stores the cache as JSON (FileHandler → json_encode($signature)), and compares the stored signature to the freshly computed one via Signature::equals(), which does an == on the rules array.
The Uuf6429/block_string rule's value contains object instances (CliPipeFormatter, GeneratedTokenCodec, deprecated bool that the constructor replaces with new DefaultNormalizer(...)). When the signature is JSON-encoded:
- These instances don't implement
JsonSerializable and don't define __serialize / jsonSerialize.
json_encode therefore emits {} (or the public-property projection), and json_decode returns it as a plain stdClass / array.
- After deserialization, the rule's value is no longer the original object graph, so the
== comparison against the freshly constructed CliPipeFormatter always fails.
In our cache file, the Uuf6429/block_string entry is dropped entirely — cache.json lists 144 rules but the block_string key is absent. Either way, the stored signature can never equal the runtime one.
Quick proof (after a run):
$ jq '.rules | keys | map(select(test("block_string|Uuf6429"; "i")))' var/cache/php-cs-fixer/.php-cs-fixer.cache
[]
The rule is configured, runs against every file, yet leaves no trace in the persisted signature.
Impact
- Every CI / pre-commit run pays the full php-cs-fixer cost, even on a clean tree.
- Disabling the rule (or stripping its
formatters config) restores cache hits.
Suggested fix
The formatters embedded in the rule configuration need to participate in the signature in a way that survives json_encode → json_decode → ==. A few options:
- Implement
JsonSerializable on AbstractStringFormatter / CliPipeFormatter / GeneratedTokenCodec, emitting a deterministic associative array (e.g. ['class' => static::class, 'version' => $this->version, 'formatter' => $this->formatter]). Provide a matching restoration path, or document that the rule's signature contribution is a scalar fingerprint rather than the object itself.
- Have
BlockStringFixer expose a custom configurationDescription / signature hook that reduces the formatter graph to a stable scalar (e.g. sha1 of a normalized array) before the signature is computed.
- Document the limitation and refuse to register objects in the rule configuration; require users to pass an identifier/factory key that the fixer resolves internally.
Option (1) seems the smallest change and keeps the user-facing API unchanged.
Hi!
When the
Uuf6429/block_stringrule is enabled with a formatter object (e.g.CliPipeFormatter) in its configuration, php-cs-fixer's signature check fails on every subsequent run, so the cache file is rewritten but never produces a hit. Removing the rule restores normal cache behaviour.Environment
uuf6429/php-cs-fixer-blockstring:1.0.5friendsofphp/php-cs-fixer:3.95.18.5.4Reproduction
.php-cs-fixer.dist.php(minimised):Steps:
vendor/bin/php-cs-fixer fix— runs against the whole tree, writes cache file.vendor/bin/php-cs-fixer fix— should re-use the cache and skip unchanged files.Expected: second run skips files (cache hit).
Actual: second run re-processes every file. Cache file is rewritten but never consulted.
Root cause
php-cs-fixer stores the cache as JSON (
FileHandler→json_encode($signature)), and compares the stored signature to the freshly computed one viaSignature::equals(), which does an==on therulesarray.The
Uuf6429/block_stringrule's value contains object instances (CliPipeFormatter,GeneratedTokenCodec, deprecated bool that the constructor replaces withnew DefaultNormalizer(...)). When the signature is JSON-encoded:JsonSerializableand don't define__serialize/jsonSerialize.json_encodetherefore emits{}(or the public-property projection), andjson_decodereturns it as a plainstdClass/ array.==comparison against the freshly constructedCliPipeFormatteralways fails.In our cache file, the
Uuf6429/block_stringentry is dropped entirely —cache.jsonlists 144 rules but theblock_stringkey is absent. Either way, the stored signature can never equal the runtime one.Quick proof (after a run):
$ jq '.rules | keys | map(select(test("block_string|Uuf6429"; "i")))' var/cache/php-cs-fixer/.php-cs-fixer.cache []The rule is configured, runs against every file, yet leaves no trace in the persisted signature.
Impact
formattersconfig) restores cache hits.Suggested fix
The formatters embedded in the rule configuration need to participate in the signature in a way that survives
json_encode→json_decode→==. A few options:JsonSerializableonAbstractStringFormatter/CliPipeFormatter/GeneratedTokenCodec, emitting a deterministic associative array (e.g.['class' => static::class, 'version' => $this->version, 'formatter' => $this->formatter]). Provide a matching restoration path, or document that the rule's signature contribution is a scalar fingerprint rather than the object itself.BlockStringFixerexpose a customconfigurationDescription/ signature hook that reduces the formatter graph to a stable scalar (e.g.sha1of a normalized array) before the signature is computed.Option (1) seems the smallest change and keeps the user-facing API unchanged.