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
8 changes: 8 additions & 0 deletions .changelog/multiline-boolean-conditions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
category: added
type: minor
---

Check and fix one symbolic boolean part per line in existing multiline if, elseif, while and do-while conditions through an owned native ECS Sniff. Preserve single-line conditions, function/arrow/class expressions and native spacing/skip overrides. Fingerprint owned policy source for cache invalidation and record the Sniff separately from native fixer risk classifications.

Enforce first-expression and closing-wrapper placement through the native PHPCS PSR-12 implementation, adapted to the ECS spacing service. Default to four spaces/LF; document nonstandard spacing overrides explicitly and preserve alternative syntax without claiming complete standards certification.
13 changes: 9 additions & 4 deletions .github/OptionalAnalysis/AnalysisResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ public function __construct(
public string $details = '',
public ?int $nativeExit = null,
) {
if (!in_array($tool, ['Magic Numbers', 'Rector'], true)
if (
!in_array($tool, ['Magic Numbers', 'Rector'], true)
|| !in_array($status, ['disabled', 'not-executed', 'clean', 'findings', 'operational-error'], true)
|| (null !== $selectedCount && $selectedCount < 0)
|| (null !== $findingCount && $findingCount < 0)
|| (null !== $nativeExit && $nativeExit < 0)) {
|| (null !== $selectedCount
&& $selectedCount < 0)
|| (null !== $findingCount
&& $findingCount < 0)
|| (null !== $nativeExit
&& $nativeExit < 0)
) {
throw new InvalidArgumentException('Invalid optional-analysis result.');
}
}
Expand Down
6 changes: 4 additions & 2 deletions .github/OptionalAnalysis/BoundedProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,13 @@ private function assertPrerequisites(float $timeout): void
if (!is_finite($timeout) || $timeout <= 0 || $timeout > self::TIMEOUT_SECONDS) {
throw new InvalidArgumentException('Optional analyzer timeout must be positive, finite and at most 300 seconds.');
}
if ('Linux' !== PHP_OS_FAMILY
if (
'Linux' !== PHP_OS_FAMILY
|| !is_executable('/usr/bin/setsid')
|| !function_exists('posix_getpgid')
|| !function_exists('posix_getsid')
|| !function_exists('posix_kill')) {
|| !function_exists('posix_kill')
) {
throw new RuntimeException('Optional workflow execution requires hosted Linux, setsid and PHP POSIX process-group support.');
}
}
Expand Down
18 changes: 13 additions & 5 deletions .github/OptionalAnalysis/NativeReportParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,11 @@ private function children(DOMElement $parent, string $name): array

continue;
}
if (XML_COMMENT_NODE === $child->nodeType
|| (XML_TEXT_NODE === $child->nodeType && '' === trim($child->textContent))) {
if (
XML_COMMENT_NODE === $child->nodeType
|| (XML_TEXT_NODE === $child->nodeType
&& '' === trim($child->textContent))
) {
continue;
}

Expand Down Expand Up @@ -187,10 +190,15 @@ private function rectorErrors(array $errors): array
{
$diagnostics = [];
foreach ($errors as $error) {
if (!is_array($error)
|| (isset($error['file']) && !is_string($error['file']))
if (
!is_array($error)
|| (isset($error['file'])
&& !is_string($error['file']))
|| !is_string($error['message'] ?? null)
|| (isset($error['line']) && (!is_int($error['line']) || $error['line'] < 1))) {
|| (isset($error['line'])
&& (!is_int($error['line'])
|| $error['line'] < 1))
) {
throw new InvalidArgumentException('Rector JSON error location is invalid.');
}
$diagnostics[] = ['file' => $error['file'] ?? '[unavailable]', 'line' => $error['line'] ?? null, 'message' => $error['message']];
Expand Down
6 changes: 4 additions & 2 deletions .github/OptionalAnalysis/WorkflowInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ public function __construct(
public string $rectorPolicy,
public string $rectorConfig,
) {
if (!str_starts_with($directory, '/')
if (
!str_starts_with($directory, '/')
|| 1 !== preg_match('/\A[a-fA-F0-9]{40}\z/D', $base)
|| 1 !== preg_match('/\A[a-fA-F0-9]{40}\z/D', $head)) {
|| 1 !== preg_match('/\A[a-fA-F0-9]{40}\z/D', $head)
) {
throw new InvalidArgumentException('Optional analyses require an absolute consumer directory and exact base/head SHAs.');
}
foreach ([$magicNumbersPolicy, $rectorPolicy] as $policy) {
Expand Down
72 changes: 49 additions & 23 deletions .github/ReleaseChannelUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ public function __construct(
public function update(string $repository, string $tag, string $sha): array
{
$selected = self::version($tag);
if (null === $selected || '0' !== $selected['major'] || 'php-fast-forward/code-standards' !== $repository || !self::sha($sha) || 1 !== preg_match(
'~^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$~D',
$repository,
)) {
if (
null === $selected
|| '0' !== $selected['major']
|| 'php-fast-forward/code-standards' !== $repository
|| !self::sha($sha)
|| 1 !== preg_match(
'~^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$~D',
$repository,
)
) {
throw new RuntimeException('Select an exact repository, complete stable release tag and full commit SHA.');
}

Expand Down Expand Up @@ -204,8 +210,11 @@ private function revalidate(
throw new RuntimeException('Release or conflicting-tag evidence changed before the channel update.');
}

if (null !== $previous && (!self::contains($finalCatalog, $previous)
|| $this->commit($repository, $previous['tag_name']) !== $current['sha'])) {
if (
null !== $previous
&& (!self::contains($finalCatalog, $previous)
|| $this->commit($repository, $previous['tag_name']) !== $current['sha'])
) {
throw new RuntimeException('The previous channel release evidence changed before the update.');
}
}
Expand Down Expand Up @@ -280,8 +289,11 @@ private function reference(string $repository, string $ref): ?array
if (null === $data) {
return null;
}
if ('refs/' . $ref !== ($data['ref'] ?? null) || !self::sha($data['object']['sha'] ?? null)
|| !is_string($data['object']['type'] ?? null)) {
if (
'refs/' . $ref !== ($data['ref'] ?? null)
|| !self::sha($data['object']['sha'] ?? null)
|| !is_string($data['object']['type'] ?? null)
) {
throw new RuntimeException('Malformed exact Git reference evidence.');
}

Expand Down Expand Up @@ -361,8 +373,12 @@ private function catalog(string $repository, string $major): array
throw new RuntimeException('Malformed release catalog.');
}
foreach ($data as $release) {
if (!is_array($release) || !is_bool($release['draft'] ?? null)
|| !is_bool($release['prerelease'] ?? null) || !is_string($release['tag_name'] ?? null)) {
if (
!is_array($release)
|| !is_bool($release['draft'] ?? null)
|| !is_bool($release['prerelease'] ?? null)
|| !is_string($release['tag_name'] ?? null)
) {
throw new RuntimeException('Partial release catalog metadata cannot establish the latest version.');
}
if ($release['draft'] || $release['prerelease']) {
Expand Down Expand Up @@ -393,10 +409,13 @@ private function catalog(string $repository, string $major): array
$left['tag_name'],
)),
);
if (count($releases) > 1 && 0 === self::compare(
self::version($releases[0]['tag_name']),
self::version($releases[1]['tag_name']),
)) {
if (
count($releases) > 1
&& 0 === self::compare(
self::version($releases[0]['tag_name']),
self::version($releases[1]['tag_name']),
)
) {
throw new RuntimeException('Build-only release ties cannot choose a unique channel destination.');
}

Expand Down Expand Up @@ -461,10 +480,13 @@ private static function published(array $release): bool
*/
private static function publicationTime(mixed $value): bool
{
if (!is_string($value) || 1 !== preg_match(
'/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/D',
$value,
)) {
if (
!is_string($value)
|| 1 !== preg_match(
'/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/D',
$value,
)
) {
return false;
}
$date = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i:s\Z', $value, new DateTimeZone('UTC'));
Expand All @@ -481,11 +503,15 @@ private static function publicationTime(mixed $value): bool
*/
private static function version(mixed $tag): ?array
{
if (!is_string($tag) || strlen($tag) > 128 || 1 !== preg_match(
'/^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/D',
$tag,
$matches,
)) {
if (
!is_string($tag)
|| strlen($tag) > 128
|| 1 !== preg_match(
'/^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/D',
$tag,
$matches,
)
) {
return null;
}

Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ including native ECS findings, failure classification and remaining-work counts.
See the [structured check contract](docs/structured-check.rst). Text is the
default; `fix` uses text output.
The default preset stores its ECS cache beneath the system temporary directory,
in `fast-forward-code-standards/<SHA-256 of the executing checkout path>`.
Different checkouts cannot invalidate each other's cache. Native
in `fast-forward-code-standards/<SHA-256 of the executing checkout path>/<owned-policy fingerprint>`.
The fingerprint includes the installed preset and owned checker paths and source
bytes. Different checkouts cannot invalidate each other's cache, and a changed
owned rule cannot reuse an earlier passing result. Native
`withCache()` overrides remain supported and apply last; share cache directories
only when their invalidation and concurrent use are intentionally coordinated.
`fix` explicitly applies ECS fixes. It does not commit, push or update dependencies.
Expand Down Expand Up @@ -128,6 +130,26 @@ both ECS and a competing PHP-CS-Fixer pass. Standalone ECS users may import the
same library preset and set their own paths. CLI selection uses the scope below;
ECS's configured paths do not expand the CLI's explicit selected file list.

For braced multiline `if`, `elseif`, `while` and `do-while` conditions, the
native PSR-12 wrapper places the first expression on the next line and the
closing parenthesis on its own line (`) {`, or `);` for do-while). The native
leading-operator policy follows PER 3.0. An owned whitespace policy additionally
puts each symbolic `&&` and `||` on its own line in those multiline conditions.
The policy includes grouped expressions, call arguments
and array indexes within that condition; function, arrow and anonymous-class
declarations remain opaque. Single-line conditions and other expression contexts
retain their native rules. Splitting every operator, including nested expression
parts, is a project extension rather than an additional PER requirement.
The default is four spaces and LF. Native ECS `withSpacing()` overrides for two
spaces, tabs or CRLF remain supported as explicit deviations from PSR-12/PER,
not standards-compliant alternatives.
Consumers can explicitly skip `FastForward\CodeStandards\Sniff\MultilineBooleanConditionSniff`
through `withSkip()`; that native method replaces the skip list, so preserve any
other intended preservation skips. See [the resolved preset](docs/resolved-preset.rst).
The separate `MultilineConditionSpacingSniff` adapts the native wrapper to the
same spacing service. Alternative colon syntax is preserved; the wrapper tests
do not certify that syntax, or the complete preset, against every PSR/PER rule.

The formatter worker uses ECS's public factory and command APIs with this tool's
own Composer installation. An isolated tool does not implicitly load the target
checkout's `vendor/autoload.php`. Explicit PHP configuration retains the checkout
Expand Down
3 changes: 2 additions & 1 deletion bin/ecs-worker
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ try {
$argc < $separator + 2
|| '--' !== $argv[$separator]
|| !in_array($argv[4], ['--check', '--fix'], true)
|| ($json && '--check' !== $argv[4])
|| ($json
&& '--check' !== $argv[4])
) {
throw new InvalidArgumentException('ECS worker requires installation paths, an explicit mode and selected files.');
}
Expand Down
2 changes: 1 addition & 1 deletion docs/policy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,4 @@ The `PHPDoc comparison <phpdoc-comparison.rst>`__ records actual custom Rector a
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.
The current preset replaces prepared PSR-12 with ECS 13.3.9'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 native fixers and two allowlisted PHPCS Sniffs: owned boolean whitespace and a native PSR-12 condition-wrapper adapter. The native PER transition added 20 and removed five fixers 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.
Loading
Loading