diff --git a/packages/coding-standard/src/Fixer/Spacing/StandaloneLineRequiredParamFixer.php b/packages/coding-standard/src/Fixer/Spacing/StandaloneLineRequiredParamFixer.php new file mode 100644 index 0000000000..9a970adde4 --- /dev/null +++ b/packages/coding-standard/src/Fixer/Spacing/StandaloneLineRequiredParamFixer.php @@ -0,0 +1,207 @@ + $tokens + */ + public function isCandidate(Tokens $tokens): bool + { + return $tokens->isTokenKindFound(T_FUNCTION); + } + + /** + * @param Tokens $tokens + */ + public function fix(SplFileInfo $fileInfo, Tokens $tokens): void + { + // from the bottom up, as adding tokens shifts every position after them + for ($position = count($tokens) - 1; $position >= 0; --$position) { + /** @var Token $token */ + $token = $tokens[$position]; + + if (! $token->isGivenKind(T_FUNCTION)) { + continue; + } + + if (! $this->isPublicRequiredMethod($tokens, $position)) { + continue; + } + + if (! $this->hasParams($tokens, $position)) { + continue; + } + + $this->paramNewliner->processFunction($tokens, $position); + } + } + + /** + * @param Tokens $tokens + */ + private function isPublicRequiredMethod(Tokens $tokens, int $position): bool + { + $headStart = $this->resolveMethodHeadStart($tokens, $position); + + $isPublic = false; + $isRequired = false; + + for ($index = $headStart; $index < $position; ++$index) { + /** @var Token $token */ + $token = $tokens[$index]; + + if ($token->isGivenKind(T_PUBLIC)) { + $isPublic = true; + continue; + } + + // within the method head, any "Required" bareword can only come from a #[Required] attribute + if ($token->isGivenKind(T_STRING) && $token->getContent() === 'Required') { + $isRequired = true; + continue; + } + + if ($token->isGivenKind(T_DOC_COMMENT) && $this->hasRequiredAnnotation($token->getContent())) { + $isRequired = true; + } + } + + return $isPublic && $isRequired; + } + + /** + * Walk back over modifiers, doc blocks and attributes to the first token of the method head. + * + * @param Tokens $tokens + */ + private function resolveMethodHeadStart(Tokens $tokens, int $position): int + { + $headStart = $position; + + for ($index = $position - 1; $index >= 0; --$index) { + /** @var Token $token */ + $token = $tokens[$index]; + + if ($token->isGivenKind(self::METHOD_HEAD_KINDS)) { + $headStart = $index; + continue; + } + + if ($token->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) { + $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index); + $headStart = $index; + continue; + } + + break; + } + + return $headStart; + } + + private function hasRequiredAnnotation(string $docComment): bool + { + return (bool) preg_match('#@required\b#i', $docComment); + } + + /** + * An empty parameter list would be broken into an empty line between the brackets. + * + * @param Tokens $tokens + */ + private function hasParams(Tokens $tokens, int $position): bool + { + $namePosition = $tokens->getNextMeaningfulToken($position); + if ($namePosition === null) { + return false; + } + + $openBracketPosition = $tokens->getNextMeaningfulToken($namePosition); + if ($openBracketPosition === null) { + return false; + } + + $firstParamPosition = $tokens->getNextMeaningfulToken($openBracketPosition); + if ($firstParamPosition === null) { + return false; + } + + /** @var Token $firstParamToken */ + $firstParamToken = $tokens[$firstParamPosition]; + + return $firstParamToken->getContent() !== ')'; + } +} diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/attribute_required.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/attribute_required.php.inc new file mode 100644 index 0000000000..94f7f87e0d --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/attribute_required.php.inc @@ -0,0 +1,30 @@ + +----- + diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/docblock_required.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/docblock_required.php.inc new file mode 100644 index 0000000000..7a8de08399 --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/docblock_required.php.inc @@ -0,0 +1,34 @@ + +----- + diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_empty_params.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_empty_params.php.inc new file mode 100644 index 0000000000..f7f7ff36d1 --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_empty_params.php.inc @@ -0,0 +1,13 @@ + diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_no_required.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_no_required.php.inc new file mode 100644 index 0000000000..9afe502806 --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_no_required.php.inc @@ -0,0 +1,12 @@ + diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_private_required.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_private_required.php.inc new file mode 100644 index 0000000000..b10babb146 --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/Fixture/skip_private_required.php.inc @@ -0,0 +1,13 @@ + diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/StandaloneLineRequiredParamFixerTest.php b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/StandaloneLineRequiredParamFixerTest.php new file mode 100644 index 0000000000..1ea7beeef1 --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/StandaloneLineRequiredParamFixerTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFiles(__DIR__ . '/Fixture'); + } + + public function provideConfig(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/config/configured_rule.php b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/config/configured_rule.php new file mode 100644 index 0000000000..e37b1722a5 --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineRequiredParamFixer/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(StandaloneLineRequiredParamFixer::class); +}; diff --git a/src/Config/Level/SpacesLevel.php b/src/Config/Level/SpacesLevel.php index aaa7ecc96c..6fa48e8b62 100644 --- a/src/Config/Level/SpacesLevel.php +++ b/src/Config/Level/SpacesLevel.php @@ -32,6 +32,7 @@ use Symplify\CodingStandard\Fixer\Spacing\MethodChainingNewlineFixer; use Symplify\CodingStandard\Fixer\Spacing\SpaceAfterCommaHereNowDocFixer; use Symplify\CodingStandard\Fixer\Spacing\StandaloneLinePromotedPropertyFixer; +use Symplify\CodingStandard\Fixer\Spacing\StandaloneLineRequiredParamFixer; use Symplify\CodingStandard\Fixer\Strict\BlankLineAfterStrictTypesFixer; /** @@ -74,6 +75,7 @@ final class SpacesLevel // most invasive structural changes MethodChainingIndentationFixer::class, StandaloneLinePromotedPropertyFixer::class, + StandaloneLineRequiredParamFixer::class, MethodArgumentSpaceFixer::class, // newline spacing (from deprecated "symplify" set)