From 144336f5e017c21795aa30d8108b88521b6d2482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 26 Oct 2026 18:42:18 -0300 Subject: [PATCH] feat: adopt independent PER policy and preserve PHPDoc --- .changelog/per-documentation-policy.md | 7 + .github/ReleaseChannelUpdater.php | 5 +- README.md | 2 +- composer.json | 2 +- composer.lock | 2 +- docs/architecture.md | 2 +- docs/policy.md | 13 ++ docs/resolved-preset.md | 83 ++++++-- src/Config/ECSConfig.php | 8 +- src/Console/Command/CheckCommand.php | 4 +- src/Console/Command/FixCommand.php | 4 +- src/Console/StandardsOperation.php | 3 +- src/Git/FileSelection.php | 6 +- src/Process/NativeProcessRunner.php | 4 +- src/Project/ProjectScope.php | 3 +- src/Project/ProjectScopeResolver.php | 4 +- src/Standards/StandardsRunner.php | 4 +- tests/Config/PresetTest.php | 2 +- tests/Fixtures/documentation.after.php.inc | 67 ++++++ tests/Fixtures/documentation.before.php.inc | 69 ++++++ tests/Fixtures/preset-rules.json | 201 +++++++++++++++--- tests/Fixtures/style.after.php.inc | 1 + tests/Integration/StandardsTest.php | 2 +- .../ReleaseChannel/ReleaseChannelFixture.php | 2 +- .../ReleaseChannelUpdaterTest.php | 8 +- tests/ServiceProvider/CompositionTest.php | 4 +- tests/Standards/StandardsRunnerTest.php | 5 +- 27 files changed, 418 insertions(+), 99 deletions(-) create mode 100644 .changelog/per-documentation-policy.md create mode 100644 tests/Fixtures/documentation.after.php.inc create mode 100644 tests/Fixtures/documentation.before.php.inc diff --git a/.changelog/per-documentation-policy.md b/.changelog/per-documentation-policy.md new file mode 100644 index 0000000..0f8ee43 --- /dev/null +++ b/.changelog/per-documentation-policy.md @@ -0,0 +1,7 @@ +--- +category: changed +type: minor +pull_request: 40 +--- + +Use ECS 13.3's native non-risky PER Coding Style 3.0 set in place of PSR-12, preserving explicit command-attribute spacing and callback-last overrides. Keep destructive PHPDoc cleanup rules skipped so descriptions, inheritance markers, analysis types and intentional annotations survive formatting. Remove inactive conversion/global-import skips without implicitly enabling those rules. diff --git a/.github/ReleaseChannelUpdater.php b/.github/ReleaseChannelUpdater.php index 35c2839..f4bb960 100644 --- a/.github/ReleaseChannelUpdater.php +++ b/.github/ReleaseChannelUpdater.php @@ -21,8 +21,7 @@ */ public function __construct( private HttpClientInterface $http, - ) { - } + ) {} /** * Advances only the selected major branch after proving published source and its old commit. @@ -388,7 +387,7 @@ private function catalog(string $repository, string $major): array } usort( $releases, - static fn (array $left, array $right): int => self::compare(self::version( + static fn(array $left, array $right): int => self::compare(self::version( $right['tag_name'], ), self::version( $left['tag_name'], diff --git a/README.md b/README.md index f690a8d..13193c9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Fast Forward Code Standards Focused PHP style and static syntax checks, independent of DevTools. The library -provides an extendable ECS PSR-12 preset; the CLI checks tracked PHP files or the files +provides an extendable ECS PER Coding Style 3.0 preset; the CLI checks tracked PHP files or the files changed by an exact PR head. This initial increment validates PHP syntax and style, not type correctness, application behavior or coverage. diff --git a/composer.json b/composer.json index 967ad8d..b730dd6 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "psr/container": "^1.1 || ^2.0", "symfony/console": "^7.4 || ^8.0", "symfony/process": "^7.4 || ^8.0", - "symplify/easy-coding-standard": "^13.0" + "symplify/easy-coding-standard": "^13.3.8" }, "require-dev": { "phpunit/phpunit": "^12.5", diff --git a/composer.lock b/composer.lock index e181612..30dcd4f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d4501df051e73a0498012c3f268844ba", + "content-hash": "a57a9274f932194060fca44700f0e7cb", "packages": [ { "name": "brick/varexporter", diff --git a/docs/architecture.md b/docs/architecture.md index 9b9094d..2853230 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -144,7 +144,7 @@ check/fix are separate invocable entrypoints and transport authority is explicit ## Consequences and next gates -This small slice validates syntax and PSR-12 style. Type analysis and semantic +This small slice validates syntax and PER Coding Style 3.0. Type analysis and semantic refactoring remain separate scoped issues with explicit opt-in and compatibility evidence. Changelog owns version PR/tag/release transactions; Code Standards does not duplicate its engine. Application tests, coverage and full reports remain diff --git a/docs/policy.md b/docs/policy.md index 4e866c4..04d0da3 100644 --- a/docs/policy.md +++ b/docs/policy.md @@ -61,3 +61,16 @@ The [PHPDoc comparison](phpdoc-comparison.md) records actual custom Rector activation and the fixtures required before adopting a documentation policy. The [static-analysis evaluation](static-analysis-evaluation.md) distinguishes candidate engines and their analysis scopes without deciding domain ownership. + + +## Selected PER evolution + +The current preset replaces prepared PSR-12 with ECS 13.3.8's native non-risky +PER Coding Style 3.0 set, then applies the owned preservation skips and explicit +spacing overrides. It does not combine duplicate PSR/PER/common/docblock sets. +The resolved snapshot records 63 rules:20 added and5 removed from the earlier 48-rule +snapshot, with explicit option changes captured for review. PER supersedes +several older checker choices; the two snapshots are not identical contracts. +No migration or risky rule is enabled. See the resolved preset and PHPDoc golden +fixture for information-preservation decisions. Earlier comparison tables above +remain the initial extraction evidence rather than a claim of current parity. diff --git a/docs/resolved-preset.md b/docs/resolved-preset.md index cafe0cf..c11979b 100644 --- a/docs/resolved-preset.md +++ b/docs/resolved-preset.md @@ -1,7 +1,7 @@ -# Resolved initial preset +# Resolved PER preset Snapshot: ECS 13.3.8 from this repository’s Composer lock. The compiled container -contains 48 distinct PHP-CS-Fixer rules, all reporting `isRisky() === false`. +contains 63 distinct PHP-CS-Fixer rules, all reporting `isRisky() === false`. This is the actual Code Standards preset, not a resolved DevTools or CoiSA preset. The fixture `tests/Fixtures/preset-rules.json` records the installed ECS version @@ -19,57 +19,72 @@ updating the fixture; do not regenerate it merely to silence a failure. | Rule | Explicit effective configuration | | --- | --- | -| `binary_operator_spaces` | `default` | +| `array_indentation` | `default` | +| `array_syntax` | `default` | +| `attribute_block_no_spaces` | `default` | +| `binary_operator_spaces` | `{"default":"at_least_single_space"}` | | `blank_line_after_namespace` | `default` | | `blank_line_after_opening_tag` | `default` | -| `braces_position` | `{"allow_single_line_empty_anonymous_classes":true}` | -| `class_definition` | `default` | +| `blank_line_between_import_groups` | `default` | +| `blank_lines_before_namespace` | `default` | +| `braces_position` | `{"allow_single_line_anonymous_functions":false,"allow_single_line_empty_anonymous_classes":true}` | +| `cast_spaces` | `default` | +| `class_definition` | `{"inline_constructor_arguments":false,"space_before_parenthesis":true}` | +| `compact_nullable_type_declaration` | `default` | | `concat_space` | `{"spacing":"one"}` | | `constant_case` | `default` | | `control_structure_braces` | `default` | | `control_structure_continuation_position` | `default` | -| `declare_equal_normalize` | `{"space":"none"}` | -| `declare_parentheses` | `default` | +| `declare_equal_normalize` | `default` | | `elseif` | `default` | | `encoding` | `default` | | `full_opening_tag` | `default` | -| `function_declaration` | `default` | +| `function_declaration` | `{"closure_fn_spacing":"none"}` | | `indentation_type` | `default` | | `line_ending` | `default` | | `lowercase_cast` | `default` | | `lowercase_keywords` | `default` | +| `lowercase_static_reference` | `default` | | `method_argument_space` | `{"on_multiline":"ignore"}` | -| `new_with_parentheses` | `default` | +| `modifier_keywords` | `default` | +| `new_with_parentheses` | `{"anonymous_class":false}` | | `no_blank_lines_after_class_opening` | `default` | | `no_break_comment` | `default` | | `no_closing_tag` | `default` | -| `no_extra_blank_lines` | `{"tokens":["curly_brace_block"]}` | +| `no_extra_blank_lines` | `{"tokens":["use"]}` | | `no_leading_import_slash` | `default` | | `no_multiple_statements_per_line` | `default` | -| `no_singleline_whitespace_before_semicolons` | `default` | +| `no_space_around_double_colon` | `default` | | `no_spaces_after_function_name` | `default` | | `no_trailing_whitespace` | `default` | | `no_trailing_whitespace_in_comment` | `default` | -| `no_whitespace_before_comma_in_array` | `default` | -| `ordered_imports` | `{"imports_order":["class","function","const"]}` | +| `no_whitespace_in_blank_line` | `default` | +| `nullable_type_declaration` | `default` | +| `operator_linebreak` | `default` | +| `ordered_class_elements` | `{"order":["use_trait"]}` | +| `ordered_imports` | `{"imports_order":["class","function","const"],"sort_algorithm":"none"}` | +| `ordered_types` | `{"null_adjustment":"always_last","sort_algorithm":"none"}` | | `phpdoc_align` | `{"align":"left"}` | | `return_type_declaration` | `default` | | `short_scalar_cast` | `default` | | `single_blank_line_at_eof` | `default` | -| `single_class_element_per_statement` | `{"elements":["property"]}` | +| `single_class_element_per_statement` | `default` | +| `single_import_per_statement` | `{"group_to_single_imports":false}` | | `single_line_after_imports` | `default` | -| `single_space_around_construct` | `default` | +| `single_line_empty_body` | `default` | +| `single_space_around_construct` | `{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const","const_import","do","else","elseif","enum","final","finally","for","foreach","function","function_import","if","insteadof","interface","match","named_argument","namespace","new","private","protected","public","readonly","static","switch","trait","try","type_colon","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]}` | +| `single_trait_insert_per_statement` | `default` | | `spaces_inside_parentheses` | `default` | | `statement_indentation` | `default` | | `switch_case_semicolon_to_colon` | `default` | | `switch_case_space` | `default` | | `ternary_operator_spaces` | `default` | -| `unary_operator_spaces` | `default` | -| `visibility_required` | `{"elements":["const","method","property"]}` | -| `whitespace_after_comma_in_array` | `default` | +| `trailing_comma_in_multiline` | `{"after_heredoc":true,"elements":["arguments","array_destructuring","arrays","match","parameters"]}` | +| `types_spaces` | `default` | +| `unary_operator_spaces` | `{"only_dec_inc":true}` | -The six preservation skips and callback-last precedence remain declared in the -preset. Some skipped rules are absent from this initial PSR-12 set; their absence +The four preservation skips and callback-last precedence remain declared in the +preset. The native PER set is ECS 13.3.8 `per-cs.php`, explicitly PER Coding Style 3.0 (not the moving PHP-CS-Fixer `@PER-CS` alias and not PER 3.1). Some skipped rules are absent from the PER set; their absence must not be presented as proof that broader profiles are safe. Native ECS builder `withSkip()` replaces the builder’s skip list, while configured rules of the same class use the last configuration. Consumers own their explicit overrides. @@ -89,3 +104,31 @@ Reviewed policy update: the attributed-command spacing regression requires the native method_argument_space multiline policy to preserve deliberate blank lines. The old snapshot failed on exactly this one option after refreshing the base; only that reviewed configuration entry was updated. No risk flag changed. + + +## PHPDoc preservation decision + +Keep `NoSuperfluousPhpdocTagsFixer`, `NoEmptyPhpdocFixer`, +`PhpdocNoEmptyReturnFixer` and `GeneralPhpdocAnnotationRemoveFixer` skipped. +The installed NoSuperfluous implementation removes redundant `@final` and +`@abstract` annotations without checking descriptions; its `remove_inheritdoc`, +unused/hidden-parameter and mixed options cannot protect that case. The installed +empty-return implementation removes a void/null annotation by type even when +it carries a description. General annotation removal deletes entire configured +tags. No custom cleanup wrapper is introduced to work around these limitations. +Redundant type-only PHPDoc therefore remains until selective preservation can +be proved; preserving information takes precedence over blanket cleanup. + +`PhpdocToCommentFixer` and `GlobalNamespaceImportFixer` are no longer skipped, +but neither belongs to the selected PER set: removing those skips does not +implicitly activate either fixer. A consumer can explicitly select and configure +them through the native builder. In particular, docblock-to-comment conversion +changes analyzer/documentation semantics and is not silently enabled here. +No broad common, docblock, Symfony, risky or migration set is added. + +The effective golden fixture preserves simple/multiline descriptions, inline +and standalone inheritance markers, virtual parameters, list/shape/class-string +types, explained unions and void/null returns, author/package/subpackage and +PHPStan/Psalm tags, described final/abstract markers and the pre-return @var. +Both fix idempotence and read-only check bytes are asserted by the real CLI. +No visual IDE equivalence was established, so inheritdoc remains untouched. diff --git a/src/Config/ECSConfig.php b/src/Config/ECSConfig.php index e557406..81d6701 100644 --- a/src/Config/ECSConfig.php +++ b/src/Config/ECSConfig.php @@ -5,17 +5,15 @@ namespace FastForward\CodeStandards\Config; use PhpCsFixer\Fixer\FunctionNotation\MethodArgumentSpaceFixer; -use PhpCsFixer\Fixer\Import\GlobalNamespaceImportFixer; use PhpCsFixer\Fixer\Phpdoc\GeneralPhpdocAnnotationRemoveFixer; use PhpCsFixer\Fixer\Phpdoc\NoEmptyPhpdocFixer; use PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer; use PhpCsFixer\Fixer\Phpdoc\PhpdocAlignFixer; use PhpCsFixer\Fixer\Phpdoc\PhpdocNoEmptyReturnFixer; -use PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer; use RuntimeException; use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder; -/** A deliberately non-risky PSR-12 starting point for extraction. */ +/** A deliberately non-risky PER Coding Style 3.0 preset for extraction. */ final class ECSConfig { /** @@ -28,13 +26,11 @@ final class ECSConfig public static function configure(?callable $customize = null): ECSConfigBuilder { $config = (new ECSConfigBuilder()) - ->withPreparedSets(psr12: true) + ->withPreparedSets(perCs: true) ->withSkip([ - PhpdocToCommentFixer::class, NoSuperfluousPhpdocTagsFixer::class, NoEmptyPhpdocFixer::class, PhpdocNoEmptyReturnFixer::class, - GlobalNamespaceImportFixer::class, GeneralPhpdocAnnotationRemoveFixer::class, ]) ->withConfiguredRule(MethodArgumentSpaceFixer::class, ['on_multiline' => 'ignore']) diff --git a/src/Console/Command/CheckCommand.php b/src/Console/Command/CheckCommand.php index 0962aa7..9e7c4a9 100644 --- a/src/Console/Command/CheckCommand.php +++ b/src/Console/Command/CheckCommand.php @@ -18,9 +18,7 @@ final readonly class CheckCommand { /** Injects the shared orchestration without parsing input or resolving a checkout. */ - public function __construct(private StandardsOperation $operation) - { - } + public function __construct(private StandardsOperation $operation) {} /** Returns the exact check status; this entrypoint never grants write authority. */ public function __invoke( diff --git a/src/Console/Command/FixCommand.php b/src/Console/Command/FixCommand.php index efeea1c..3e786e3 100644 --- a/src/Console/Command/FixCommand.php +++ b/src/Console/Command/FixCommand.php @@ -18,9 +18,7 @@ final readonly class FixCommand { /** Injects the shared orchestration without parsing input or resolving a checkout. */ - public function __construct(private StandardsOperation $operation) - { - } + public function __construct(private StandardsOperation $operation) {} /** Returns the exact fix status; callers must inspect the resulting source diff. */ public function __invoke( diff --git a/src/Console/StandardsOperation.php b/src/Console/StandardsOperation.php index ff82acb..6280695 100644 --- a/src/Console/StandardsOperation.php +++ b/src/Console/StandardsOperation.php @@ -23,8 +23,7 @@ public function __construct( private FileSelection $selection, private PhpFiles $phpFiles, private StandardsRunner $standards, - ) { - } + ) {} /** * Resolves and presents one selection, preserving tool status and rendering controlled errors to stderr. diff --git a/src/Git/FileSelection.php b/src/Git/FileSelection.php index d91dd73..4d230ee 100644 --- a/src/Git/FileSelection.php +++ b/src/Git/FileSelection.php @@ -10,9 +10,7 @@ final class FileSelection { /** Injects the Git process boundary. */ - public function __construct(private readonly ProcessRunner $process) - { - } + public function __construct(private readonly ProcessRunner $process) {} /** * Returns NUL-safe paths; config or dependency changes require a full scan. @@ -75,6 +73,6 @@ private function assertCheckoutRoot(string $directory): void /** @return list Decodes Git's binary path protocol without trimming file names. */ private function paths(string $output): array { - return array_values(array_filter(explode("\0", $output), static fn (string $path): bool => '' !== $path)); + return array_values(array_filter(explode("\0", $output), static fn(string $path): bool => '' !== $path)); } } diff --git a/src/Process/NativeProcessRunner.php b/src/Process/NativeProcessRunner.php index 2090c9c..f4f301c 100644 --- a/src/Process/NativeProcessRunner.php +++ b/src/Process/NativeProcessRunner.php @@ -10,9 +10,7 @@ final class NativeProcessRunner implements ProcessRunner { /** @param \Closure(string,string):void|null $write Explicit output sink; null keeps this library transport silent. */ - public function __construct(private readonly ?\Closure $write = null) - { - } + public function __construct(private readonly ?\Closure $write = null) {} /** Captures Git output as bytes, including NUL-delimited paths. */ public function capture(array $arguments, string $directory): string diff --git a/src/Project/ProjectScope.php b/src/Project/ProjectScope.php index 2270be1..65de576 100644 --- a/src/Project/ProjectScope.php +++ b/src/Project/ProjectScope.php @@ -20,6 +20,5 @@ public function __construct( public string $config, public ?string $base, public ?string $head, - ) { - } + ) {} } diff --git a/src/Project/ProjectScopeResolver.php b/src/Project/ProjectScopeResolver.php index 928c47a..b605e61 100644 --- a/src/Project/ProjectScopeResolver.php +++ b/src/Project/ProjectScopeResolver.php @@ -10,9 +10,7 @@ final readonly class ProjectScopeResolver { /** Injects the packaged preset location; resolution never discovers a competing consumer preset implicitly. */ - public function __construct(private string $defaultConfig) - { - } + public function __construct(private string $defaultConfig) {} /** * Requires exactly one scan mode and existing project/config paths, without writing or loading config PHP. diff --git a/src/Standards/StandardsRunner.php b/src/Standards/StandardsRunner.php index 35a7fe4..38d23bb 100644 --- a/src/Standards/StandardsRunner.php +++ b/src/Standards/StandardsRunner.php @@ -10,9 +10,7 @@ final class StandardsRunner { /** @param string $ecs Absolute ECS entrypoint from this tool's installation. */ - public function __construct(private readonly ProcessRunner $process, private readonly string $ecs) - { - } + public function __construct(private readonly ProcessRunner $process, private readonly string $ecs) {} /** * Checks syntax before style; only explicit fix permits ECS source writes. diff --git a/tests/Config/PresetTest.php b/tests/Config/PresetTest.php index 3bc8a24..a7babb2 100644 --- a/tests/Config/PresetTest.php +++ b/tests/Config/PresetTest.php @@ -17,7 +17,7 @@ public function testCompiledPresetMatchesReviewedRules(): void $vendor = dirname(__DIR__, 2) . '/vendor'; $packageRoot = realpath($vendor . '/symplify/easy-coding-standard'); $installed = json_decode(file_get_contents($vendor . '/composer/installed.json'), true, flags: JSON_THROW_ON_ERROR); - $package = array_values(array_filter($installed['packages'], static fn (array $entry): bool => 'symplify/easy-coding-standard' === $entry['name']))[0]; + $package = array_values(array_filter($installed['packages'], static fn(array $entry): bool => 'symplify/easy-coding-standard' === $entry['name']))[0]; require_once $packageRoot . '/vendor/squizlabs/php_codesniffer/autoload.php'; diff --git a/tests/Fixtures/documentation.after.php.inc b/tests/Fixtures/documentation.after.php.inc new file mode 100644 index 0000000..5a4768a --- /dev/null +++ b/tests/Fixtures/documentation.after.php.inc @@ -0,0 +1,67 @@ +} + * @psalm-type Payload array{items: list} + */ +final class DescribedContract +{ + /** + * @inheritDoc + * + * @param string $value Input description. + * @param list $items + * @param array{key: string} $options Rich shape contract. + * @param string $virtual Parameter handled by an external proxy. + * @return string|null A value when available, otherwise no result. + */ + public function describe(string $value, array $items, array $options): ?string + { + return $items[0] ?? $value; + } + + /** + * Prefix {@inheritDoc} Keep this local explanation after the inherited text. + * + * @param string $value + * @return void This notifies the external system; no response is returned. + */ + public function notify(string $value): void + { + echo $value; + } + + /** + * @return null Nothing is returned when no mapping exists. + */ + public function absent(): null + { + return null; + } + + /** + * @param string $value Multi-line description: + * preserve this continuation. + * @return class-string A class name rather than an arbitrary string. + */ + public function className(string $value): string + { + /** @var class-string $value Keep the analyzer assertion before return. */ + return $value; + } +} + +/** + * @abstract Preserve this described inheritance constraint. + */ +abstract class DescribedBase {} diff --git a/tests/Fixtures/documentation.before.php.inc b/tests/Fixtures/documentation.before.php.inc new file mode 100644 index 0000000..3e21eac --- /dev/null +++ b/tests/Fixtures/documentation.before.php.inc @@ -0,0 +1,69 @@ +} + * @psalm-type Payload array{items: list} + */ +final class DescribedContract +{ + /** + * @inheritDoc + * + * @param string $value Input description. + * @param list $items + * @param array{key: string} $options Rich shape contract. + * @param string $virtual Parameter handled by an external proxy. + * @return string|null A value when available, otherwise no result. + */ + public function describe(string $value, array $items, array $options): ?string + { + return $items[0] ?? $value; + } + + /** + * Prefix {@inheritDoc} Keep this local explanation after the inherited text. + * + * @param string $value + * @return void This notifies the external system; no response is returned. + */ + public function notify(string $value): void + { + echo $value; + } + + /** + * @return null Nothing is returned when no mapping exists. + */ + public function absent(): null + { + return null; + } + + /** + * @param string $value Multi-line description: + * preserve this continuation. + * @return class-string A class name rather than an arbitrary string. + */ + public function className(string $value): string + { + /** @var class-string $value Keep the analyzer assertion before return. */ + return $value; + } +} + +/** + * @abstract Preserve this described inheritance constraint. + */ +abstract class DescribedBase +{ +} diff --git a/tests/Fixtures/preset-rules.json b/tests/Fixtures/preset-rules.json index 807d3b8..d4e24e2 100644 --- a/tests/Fixtures/preset-rules.json +++ b/tests/Fixtures/preset-rules.json @@ -2,9 +2,26 @@ "ecs_version": "13.3.8", "ecs_reference": "0564c0b9b807f1740bf0f2cc965ab51551f4dc3b", "rules": { + "array_indentation": { + "class": "PhpCsFixer\\Fixer\\Whitespace\\ArrayIndentationFixer", + "configuration": [], + "risky": false + }, + "array_syntax": { + "class": "PhpCsFixer\\Fixer\\ArrayNotation\\ArraySyntaxFixer", + "configuration": [], + "risky": false + }, + "attribute_block_no_spaces": { + "class": "PhpCsFixer\\Fixer\\AttributeNotation\\AttributeBlockNoSpacesFixer", + "configuration": [], + "risky": false + }, "binary_operator_spaces": { "class": "PhpCsFixer\\Fixer\\Operator\\BinaryOperatorSpacesFixer", - "configuration": [], + "configuration": { + "default": "at_least_single_space" + }, "risky": false }, "blank_line_after_namespace": { @@ -17,15 +34,39 @@ "configuration": [], "risky": false }, + "blank_line_between_import_groups": { + "class": "PhpCsFixer\\Fixer\\Whitespace\\BlankLineBetweenImportGroupsFixer", + "configuration": [], + "risky": false + }, + "blank_lines_before_namespace": { + "class": "PhpCsFixer\\Fixer\\NamespaceNotation\\BlankLinesBeforeNamespaceFixer", + "configuration": [], + "risky": false + }, "braces_position": { "class": "PhpCsFixer\\Fixer\\Basic\\BracesPositionFixer", "configuration": { + "allow_single_line_anonymous_functions": false, "allow_single_line_empty_anonymous_classes": true }, "risky": false }, + "cast_spaces": { + "class": "PhpCsFixer\\Fixer\\CastNotation\\CastSpacesFixer", + "configuration": [], + "risky": false + }, "class_definition": { "class": "PhpCsFixer\\Fixer\\ClassNotation\\ClassDefinitionFixer", + "configuration": { + "inline_constructor_arguments": false, + "space_before_parenthesis": true + }, + "risky": false + }, + "compact_nullable_type_declaration": { + "class": "PhpCsFixer\\Fixer\\Whitespace\\CompactNullableTypeDeclarationFixer", "configuration": [], "risky": false }, @@ -53,13 +94,6 @@ }, "declare_equal_normalize": { "class": "PhpCsFixer\\Fixer\\LanguageConstruct\\DeclareEqualNormalizeFixer", - "configuration": { - "space": "none" - }, - "risky": false - }, - "declare_parentheses": { - "class": "PhpCsFixer\\Fixer\\LanguageConstruct\\DeclareParenthesesFixer", "configuration": [], "risky": false }, @@ -80,7 +114,9 @@ }, "function_declaration": { "class": "PhpCsFixer\\Fixer\\FunctionNotation\\FunctionDeclarationFixer", - "configuration": [], + "configuration": { + "closure_fn_spacing": "none" + }, "risky": false }, "indentation_type": { @@ -103,6 +139,11 @@ "configuration": [], "risky": false }, + "lowercase_static_reference": { + "class": "PhpCsFixer\\Fixer\\Casing\\LowercaseStaticReferenceFixer", + "configuration": [], + "risky": false + }, "method_argument_space": { "class": "PhpCsFixer\\Fixer\\FunctionNotation\\MethodArgumentSpaceFixer", "configuration": { @@ -110,9 +151,16 @@ }, "risky": false }, + "modifier_keywords": { + "class": "PhpCsFixer\\Fixer\\ClassNotation\\ModifierKeywordsFixer", + "configuration": [], + "risky": false + }, "new_with_parentheses": { "class": "PhpCsFixer\\Fixer\\Operator\\NewWithParenthesesFixer", - "configuration": [], + "configuration": { + "anonymous_class": false + }, "risky": false }, "no_blank_lines_after_class_opening": { @@ -134,7 +182,7 @@ "class": "PhpCsFixer\\Fixer\\Whitespace\\NoExtraBlankLinesFixer", "configuration": { "tokens": [ - "curly_brace_block" + "use" ] }, "risky": false @@ -149,8 +197,8 @@ "configuration": [], "risky": false }, - "no_singleline_whitespace_before_semicolons": { - "class": "PhpCsFixer\\Fixer\\Semicolon\\NoSinglelineWhitespaceBeforeSemicolonsFixer", + "no_space_around_double_colon": { + "class": "PhpCsFixer\\Fixer\\Operator\\NoSpaceAroundDoubleColonFixer", "configuration": [], "risky": false }, @@ -169,11 +217,30 @@ "configuration": [], "risky": false }, - "no_whitespace_before_comma_in_array": { - "class": "PhpCsFixer\\Fixer\\ArrayNotation\\NoWhitespaceBeforeCommaInArrayFixer", + "no_whitespace_in_blank_line": { + "class": "PhpCsFixer\\Fixer\\Whitespace\\NoWhitespaceInBlankLineFixer", "configuration": [], "risky": false }, + "nullable_type_declaration": { + "class": "PhpCsFixer\\Fixer\\LanguageConstruct\\NullableTypeDeclarationFixer", + "configuration": [], + "risky": false + }, + "operator_linebreak": { + "class": "PhpCsFixer\\Fixer\\Operator\\OperatorLinebreakFixer", + "configuration": [], + "risky": false + }, + "ordered_class_elements": { + "class": "PhpCsFixer\\Fixer\\ClassNotation\\OrderedClassElementsFixer", + "configuration": { + "order": [ + "use_trait" + ] + }, + "risky": false + }, "ordered_imports": { "class": "PhpCsFixer\\Fixer\\Import\\OrderedImportsFixer", "configuration": { @@ -181,7 +248,16 @@ "class", "function", "const" - ] + ], + "sort_algorithm": "none" + }, + "risky": false + }, + "ordered_types": { + "class": "PhpCsFixer\\Fixer\\ClassNotation\\OrderedTypesFixer", + "configuration": { + "null_adjustment": "always_last", + "sort_algorithm": "none" }, "risky": false }, @@ -209,10 +285,13 @@ }, "single_class_element_per_statement": { "class": "PhpCsFixer\\Fixer\\ClassNotation\\SingleClassElementPerStatementFixer", + "configuration": [], + "risky": false + }, + "single_import_per_statement": { + "class": "PhpCsFixer\\Fixer\\Import\\SingleImportPerStatementFixer", "configuration": { - "elements": [ - "property" - ] + "group_to_single_imports": false }, "risky": false }, @@ -221,8 +300,63 @@ "configuration": [], "risky": false }, + "single_line_empty_body": { + "class": "PhpCsFixer\\Fixer\\Basic\\SingleLineEmptyBodyFixer", + "configuration": [], + "risky": false + }, "single_space_around_construct": { "class": "PhpCsFixer\\Fixer\\LanguageConstruct\\SingleSpaceAroundConstructFixer", + "configuration": { + "constructs_followed_by_a_single_space": [ + "abstract", + "as", + "case", + "catch", + "class", + "const", + "const_import", + "do", + "else", + "elseif", + "enum", + "final", + "finally", + "for", + "foreach", + "function", + "function_import", + "if", + "insteadof", + "interface", + "match", + "named_argument", + "namespace", + "new", + "private", + "protected", + "public", + "readonly", + "static", + "switch", + "trait", + "try", + "type_colon", + "use", + "use_lambda", + "while" + ], + "constructs_preceded_by_a_single_space": [ + "as", + "else", + "elseif", + "use_lambda" + ] + }, + "risky": false + }, + "single_trait_insert_per_statement": { + "class": "PhpCsFixer\\Fixer\\ClassNotation\\SingleTraitInsertPerStatementFixer", "configuration": [], "risky": false }, @@ -251,26 +385,31 @@ "configuration": [], "risky": false }, - "unary_operator_spaces": { - "class": "PhpCsFixer\\Fixer\\Operator\\UnaryOperatorSpacesFixer", - "configuration": [], - "risky": false - }, - "visibility_required": { - "class": "PhpCsFixer\\Fixer\\ClassNotation\\VisibilityRequiredFixer", + "trailing_comma_in_multiline": { + "class": "PhpCsFixer\\Fixer\\ControlStructure\\TrailingCommaInMultilineFixer", "configuration": { + "after_heredoc": true, "elements": [ - "const", - "method", - "property" + "arguments", + "array_destructuring", + "arrays", + "match", + "parameters" ] }, "risky": false }, - "whitespace_after_comma_in_array": { - "class": "PhpCsFixer\\Fixer\\ArrayNotation\\WhitespaceAfterCommaInArrayFixer", + "types_spaces": { + "class": "PhpCsFixer\\Fixer\\Whitespace\\TypesSpacesFixer", "configuration": [], "risky": false + }, + "unary_operator_spaces": { + "class": "PhpCsFixer\\Fixer\\Operator\\UnaryOperatorSpacesFixer", + "configuration": { + "only_dec_inc": true + }, + "risky": false } } } diff --git a/tests/Fixtures/style.after.php.inc b/tests/Fixtures/style.after.php.inc index 8ce1475..5bed5f6 100644 --- a/tests/Fixtures/style.after.php.inc +++ b/tests/Fixtures/style.after.php.inc @@ -1,6 +1,7 @@ put($name . '.php', $original[$name]); } diff --git a/tests/ReleaseChannel/ReleaseChannelFixture.php b/tests/ReleaseChannel/ReleaseChannelFixture.php index 85f9ee8..d33bc5c 100644 --- a/tests/ReleaseChannel/ReleaseChannelFixture.php +++ b/tests/ReleaseChannel/ReleaseChannelFixture.php @@ -91,7 +91,7 @@ public function git(array $arguments, bool $optional = false): ?string $pipes, $this->directory, $environment, - ['bypass_shell' => true] + ['bypass_shell' => true], ); if (!is_resource($process)) { throw new RuntimeException('Could not start disposable Git.'); diff --git a/tests/ReleaseChannel/ReleaseChannelUpdaterTest.php b/tests/ReleaseChannel/ReleaseChannelUpdaterTest.php index 284fa57..7300d28 100644 --- a/tests/ReleaseChannel/ReleaseChannelUpdaterTest.php +++ b/tests/ReleaseChannel/ReleaseChannelUpdaterTest.php @@ -228,7 +228,7 @@ public static function invalidPublicationTimes(): iterable public function testRejectsANewerReleaseDiscoveredBeforeMutation(): void { $f = $this->fixture; - $f->catalog = static fn (int $count): array => 1 === $count ? array_values($f->releases) + $f->catalog = static fn(int $count): array => 1 === $count ? array_values($f->releases) : [...array_values($f->releases), [...$f->releases['v0.2.0'], 'id' => 3, 'tag_name' => 'v0.3.0']]; $this->expectExceptionMessage('changed before'); $f->run(); @@ -239,7 +239,7 @@ public function testRejectsWithdrawnPreviousChannelEvidence(): void { $f = $this->fixture; $f->git(['branch', 'v0', $f->commits['v0.1.0']]); - $f->catalog = static fn (int $count): array => 1 === $count ? array_values($f->releases) : [ + $f->catalog = static fn(int $count): array => 1 === $count ? array_values($f->releases) : [ $f->releases['v0.2.0'], ]; $this->expectExceptionMessage('previous channel release evidence'); @@ -250,7 +250,7 @@ public function testRejectsWithdrawnPreviousChannelEvidence(): void public function testRejectsAnUnboundedCatalog(): void { $f = $this->fixture; - $f->catalog = static fn (): array => array_fill(0, 100, $f->releases['v0.2.0']); + $f->catalog = static fn(): array => array_fill(0, 100, $f->releases['v0.2.0']); $this->expectExceptionMessage('verification limit'); $f->run(); } @@ -276,7 +276,7 @@ public function testRejectsRepositoryIdentityMismatch(): void public function testRejectsMalformedExactRefs(): void { $f = $this->fixture; - $f->referenceOverride = static fn (): array => ['ref' => 'refs/tags/foreign', + $f->referenceOverride = static fn(): array => ['ref' => 'refs/tags/foreign', 'object' => ['type' => 'commit', 'sha' => $f->commits['v0.2.0']]]; $this->expectExceptionMessage('Malformed exact'); $f->run(); diff --git a/tests/ServiceProvider/CompositionTest.php b/tests/ServiceProvider/CompositionTest.php index 93ffd8a..1e58fef 100644 --- a/tests/ServiceProvider/CompositionTest.php +++ b/tests/ServiceProvider/CompositionTest.php @@ -64,7 +64,7 @@ public function testOutputOverridePreservesRawProcessBytes(): void $replacement = new BufferedOutput(); $container = ContainerFactory::create(new AggregateServiceProvider( new StandardsServiceProvider(dirname(__DIR__, 2) . '/vendor', $default), - new ArrayServiceProvider([OutputInterface::class => static fn (): OutputInterface => $replacement]), + new ArrayServiceProvider([OutputInterface::class => static fn(): OutputInterface => $replacement]), )); $status = $container->get(ProcessRunner::class)->run([PHP_BINARY, '-r', 'echo "literal";'], sys_get_temp_dir()); @@ -79,7 +79,7 @@ public function testFailedBindingPreservesItsCause(): void $cause = new \RuntimeException('Synthetic transport construction failure.'); $container = ContainerFactory::create(new AggregateServiceProvider( new StandardsServiceProvider(dirname(__DIR__, 2) . '/vendor', new BufferedOutput()), - new ArrayServiceProvider([ProcessRunner::class => static fn (): ProcessRunner => throw $cause]), + new ArrayServiceProvider([ProcessRunner::class => static fn(): ProcessRunner => throw $cause]), )); try { diff --git a/tests/Standards/StandardsRunnerTest.php b/tests/Standards/StandardsRunnerTest.php index 4b44a73..c664492 100644 --- a/tests/Standards/StandardsRunnerTest.php +++ b/tests/Standards/StandardsRunnerTest.php @@ -28,7 +28,7 @@ public function testOnlyExplicitFixAddsWriteFlagAndFilesRemainIndividualArgument public function testBatchesAreBoundedAndEcsFailureIsNotSwallowed(): void { $process = new RecordingProcess(23); - $files = array_map(static fn (int $i): string => '/repo/' . $i . '.php', range(1, 101)); + $files = array_map(static fn(int $i): string => '/repo/' . $i . '.php', range(1, 101)); self::assertSame(23, (new StandardsRunner($process, '/tools/ecs'))->run('/repo', $files, '/config', false)); self::assertCount(102, $process->commands); self::assertCount(57, $process->commands[101]); @@ -68,8 +68,7 @@ public function __construct( private readonly int $ecsStatus = 0, private readonly int $syntaxStatus = 0, private readonly ?int $verificationStatus = null, - ) { - } + ) {} /** Fails if standards orchestration unexpectedly performs a capture operation. */ public function capture(array $arguments, string $directory): string