Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/per-documentation-policy.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 2 additions & 3 deletions .github/ReleaseChannelUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
*/
public function __construct(
private HttpClientInterface $http,
) {
}
) {}

/**
* Advances only the selected major branch after proving published source and its old commit.
Expand Down Expand Up @@ -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'],
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions docs/policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
83 changes: 63 additions & 20 deletions docs/resolved-preset.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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.
8 changes: 2 additions & 6 deletions src/Config/ECSConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/**
Expand All @@ -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'])
Expand Down
4 changes: 1 addition & 3 deletions src/Console/Command/CheckCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 1 addition & 3 deletions src/Console/Command/FixCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 1 addition & 2 deletions src/Console/StandardsOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 2 additions & 4 deletions src/Git/FileSelection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -75,6 +73,6 @@ private function assertCheckoutRoot(string $directory): void
/** @return list<string> 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));
}
}
4 changes: 1 addition & 3 deletions src/Process/NativeProcessRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/Project/ProjectScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,5 @@ public function __construct(
public string $config,
public ?string $base,
public ?string $head,
) {
}
) {}
}
4 changes: 1 addition & 3 deletions src/Project/ProjectScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions src/Standards/StandardsRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tests/Config/PresetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading
Loading