Browse Source

feature: introduce single_space_around_construct, deprecate single_space_after_construct (#6857)

Dariusz Rumiński 1 year ago
parent
commit
e04eef3d9e

+ 24 - 1
doc/list.rst

@@ -2856,6 +2856,8 @@ List of Available Rules
 
    Ensures a single space after language constructs.
 
+   *warning deprecated*   Use ``single_space_around_construct`` instead.
+
    Configuration options:
 
    - | ``constructs``
@@ -2864,9 +2866,30 @@ List of Available Rules
      | Default value: ``['abstract', 'as', 'attribute', 'break', 'case', 'catch', 'class', 'clone', 'comment', 'const', 'const_import', 'continue', 'do', 'echo', 'else', 'elseif', 'enum', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'function_import', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'match', 'named_argument', 'namespace', 'new', 'open_tag_with_echo', 'php_doc', 'php_open', 'print', 'private', 'protected', 'public', 'readonly', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'use', 'use_lambda', 'use_trait', 'var', 'while', 'yield', 'yield_from']``
 
 
+   `Source PhpCsFixer\\Fixer\\LanguageConstruct\\SingleSpaceAfterConstructFixer <./../src/Fixer/LanguageConstruct/SingleSpaceAfterConstructFixer.php>`_
+-  `single_space_around_construct <./rules/language_construct/single_space_around_construct.rst>`_
+
+   Ensures a single space after language constructs.
+
+   Configuration options:
+
+   - | ``constructs_contain_a_single_space``
+     | List of constructs which must contain a single space.
+     | Allowed values: a subset of ``['yield_from']``
+     | Default value: ``['yield_from']``
+   - | ``constructs_preceded_by_a_single_space``
+     | List of constructs which must be preceded by a single space.
+     | Allowed values: a subset of ``['use_lambda']``
+     | Default value: ``['use_lambda']``
+   - | ``constructs_followed_by_a_single_space``
+     | List of constructs which must be followed by a single space.
+     | Allowed values: a subset of ``['abstract', 'as', 'attribute', 'break', 'case', 'catch', 'class', 'clone', 'comment', 'const', 'const_import', 'continue', 'do', 'echo', 'else', 'elseif', 'enum', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'function_import', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'match', 'named_argument', 'namespace', 'new', 'open_tag_with_echo', 'php_doc', 'php_open', 'print', 'private', 'protected', 'public', 'readonly', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'type_colon', 'use', 'use_lambda', 'use_trait', 'var', 'while', 'yield', 'yield_from']``
+     | Default value: ``['abstract', 'as', 'attribute', 'break', 'case', 'catch', 'class', 'clone', 'comment', 'const', 'const_import', 'continue', 'do', 'echo', 'else', 'elseif', 'enum', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'function_import', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'match', 'named_argument', 'namespace', 'new', 'open_tag_with_echo', 'php_doc', 'php_open', 'print', 'private', 'protected', 'public', 'readonly', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'type_colon', 'use', 'use_lambda', 'use_trait', 'var', 'while', 'yield', 'yield_from']``
+
+
    Part of rule sets `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_
 
-   `Source PhpCsFixer\\Fixer\\LanguageConstruct\\SingleSpaceAfterConstructFixer <./../src/Fixer/LanguageConstruct/SingleSpaceAfterConstructFixer.php>`_
+   `Source PhpCsFixer\\Fixer\\LanguageConstruct\\SingleSpaceAroundConstructFixer <./../src/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixer.php>`_
 -  `single_trait_insert_per_statement <./rules/class_notation/single_trait_insert_per_statement.rst>`_
 
    Each trait ``use`` must be done as single statement.

+ 1 - 3
doc/ruleSets/Symfony.rst

@@ -129,9 +129,7 @@ Rules
   ``['comment_types' => ['hash']]``
 - `single_line_throw <./../rules/function_notation/single_line_throw.rst>`_
 - `single_quote <./../rules/string_notation/single_quote.rst>`_
-- `single_space_after_construct <./../rules/language_construct/single_space_after_construct.rst>`_
-  config:
-  ``['constructs' => ['abstract', 'as', 'attribute', 'break', 'case', 'catch', 'class', 'clone', 'comment', 'const', 'const_import', 'continue', 'do', 'echo', 'else', 'elseif', 'enum', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'function_import', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'match', 'named_argument', 'namespace', 'new', 'open_tag_with_echo', 'php_doc', 'php_open', 'print', 'private', 'protected', 'public', 'readonly', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'type_colon', 'use', 'use_lambda', 'use_trait', 'var', 'while', 'yield', 'yield_from']]``
+- `single_space_around_construct <./../rules/language_construct/single_space_around_construct.rst>`_
 - `space_after_semicolon <./../rules/semicolon/space_after_semicolon.rst>`_
   config:
   ``['remove_in_empty_for_expressions' => true]``

+ 4 - 1
doc/rules/index.rst

@@ -465,7 +465,10 @@ Language Construct
 - `no_unset_on_property <./language_construct/no_unset_on_property.rst>`_ *(risky)*
 
   Properties should be set to ``null`` instead of using ``unset``.
-- `single_space_after_construct <./language_construct/single_space_after_construct.rst>`_
+- `single_space_after_construct <./language_construct/single_space_after_construct.rst>`_ *(deprecated)*
+
+  Ensures a single space after language constructs.
+- `single_space_around_construct <./language_construct/single_space_around_construct.rst>`_
 
   Ensures a single space after language constructs.
 

+ 8 - 15
doc/rules/language_construct/single_space_after_construct.rst

@@ -4,6 +4,14 @@ Rule ``single_space_after_construct``
 
 Ensures a single space after language constructs.
 
+Warning
+-------
+
+This rule is deprecated and will be removed on next major version
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You should use ``single_space_around_construct`` instead.
+
 Configuration
 -------------
 
@@ -60,18 +68,3 @@ With configuration: ``['constructs' => ['yield_from']]``.
 
    -yield  from  baz();
    +yield from baz();
-
-Rule sets
----------
-
-The rule is part of the following rule sets:
-
-@PhpCsFixer
-  Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ rule set will enable the ``single_space_after_construct`` rule with the config below:
-
-  ``['constructs' => ['abstract', 'as', 'attribute', 'break', 'case', 'catch', 'class', 'clone', 'comment', 'const', 'const_import', 'continue', 'do', 'echo', 'else', 'elseif', 'enum', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'function_import', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'match', 'named_argument', 'namespace', 'new', 'open_tag_with_echo', 'php_doc', 'php_open', 'print', 'private', 'protected', 'public', 'readonly', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'type_colon', 'use', 'use_lambda', 'use_trait', 'var', 'while', 'yield', 'yield_from']]``
-
-@Symfony
-  Using the `@Symfony <./../../ruleSets/Symfony.rst>`_ rule set will enable the ``single_space_after_construct`` rule with the config below:
-
-  ``['constructs' => ['abstract', 'as', 'attribute', 'break', 'case', 'catch', 'class', 'clone', 'comment', 'const', 'const_import', 'continue', 'do', 'echo', 'else', 'elseif', 'enum', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'function_import', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'match', 'named_argument', 'namespace', 'new', 'open_tag_with_echo', 'php_doc', 'php_open', 'print', 'private', 'protected', 'public', 'readonly', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'type_colon', 'use', 'use_lambda', 'use_trait', 'var', 'while', 'yield', 'yield_from']]``

+ 120 - 0
doc/rules/language_construct/single_space_around_construct.rst

@@ -0,0 +1,120 @@
+======================================
+Rule ``single_space_around_construct``
+======================================
+
+Ensures a single space after language constructs.
+
+Configuration
+-------------
+
+``constructs_contain_a_single_space``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+List of constructs which must contain a single space.
+
+Allowed values: a subset of ``['yield_from']``
+
+Default value: ``['yield_from']``
+
+``constructs_preceded_by_a_single_space``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+List of constructs which must be preceded by a single space.
+
+Allowed values: a subset of ``['use_lambda']``
+
+Default value: ``['use_lambda']``
+
+``constructs_followed_by_a_single_space``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+List of constructs which must be followed by a single space.
+
+Allowed values: a subset of ``['abstract', 'as', 'attribute', 'break', 'case', 'catch', 'class', 'clone', 'comment', 'const', 'const_import', 'continue', 'do', 'echo', 'else', 'elseif', 'enum', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'function_import', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'match', 'named_argument', 'namespace', 'new', 'open_tag_with_echo', 'php_doc', 'php_open', 'print', 'private', 'protected', 'public', 'readonly', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'type_colon', 'use', 'use_lambda', 'use_trait', 'var', 'while', 'yield', 'yield_from']``
+
+Default value: ``['abstract', 'as', 'attribute', 'break', 'case', 'catch', 'class', 'clone', 'comment', 'const', 'const_import', 'continue', 'do', 'echo', 'else', 'elseif', 'enum', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'function_import', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'match', 'named_argument', 'namespace', 'new', 'open_tag_with_echo', 'php_doc', 'php_open', 'print', 'private', 'protected', 'public', 'readonly', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'type_colon', 'use', 'use_lambda', 'use_trait', 'var', 'while', 'yield', 'yield_from']``
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+*Default* configuration.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+   -throw  new  \Exception();
+   +throw new \Exception();
+
+Example #2
+~~~~~~~~~~
+
+With configuration: ``['constructs_contain_a_single_space' => ['yield_from'], 'constructs_followed_by_a_single_space' => ['yield_from']]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+   -function foo() { yield  from  baz(); }
+   +function foo() { yield from baz(); }
+
+Example #3
+~~~~~~~~~~
+
+With configuration: ``['constructs_preceded_by_a_single_space' => ['use_lambda'], 'constructs_followed_by_a_single_space' => ['use_lambda']]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+   -$foo = function& ()use($bar) {
+   +$foo = function& () use ($bar) {
+    };
+
+Example #4
+~~~~~~~~~~
+
+With configuration: ``['constructs_followed_by_a_single_space' => ['echo']]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+   -echo  "Hello!";
+   +echo "Hello!";
+
+Example #5
+~~~~~~~~~~
+
+With configuration: ``['constructs_followed_by_a_single_space' => ['yield_from']]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+   -yield  from  baz();
+   +yield from baz();
+
+Rule sets
+---------
+
+The rule is part of the following rule sets:
+
+@PhpCsFixer
+  Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ rule set will enable the ``single_space_around_construct`` rule with the default config.
+
+@Symfony
+  Using the `@Symfony <./../../ruleSets/Symfony.rst>`_ rule set will enable the ``single_space_around_construct`` rule with the default config.

+ 1 - 1
src/Fixer/Alias/ModernizeStrposFixer.php

@@ -77,7 +77,7 @@ if (strpos($haystack, $needle) === false) {}
     /**
      * {@inheritdoc}
      *
-     * Must run before BinaryOperatorSpacesFixer, NoExtraBlankLinesFixer, NoSpacesInsideParenthesisFixer, NoTrailingWhitespaceFixer, NotOperatorWithSpaceFixer, NotOperatorWithSuccessorSpaceFixer, PhpUnitDedicateAssertFixer, SingleSpaceAfterConstructFixer.
+     * Must run before BinaryOperatorSpacesFixer, NoExtraBlankLinesFixer, NoSpacesInsideParenthesisFixer, NoTrailingWhitespaceFixer, NotOperatorWithSpaceFixer, NotOperatorWithSuccessorSpaceFixer, PhpUnitDedicateAssertFixer, SingleSpaceAfterConstructFixer, SingleSpaceAroundConstructFixer.
      * Must run after StrictComparisonFixer.
      */
     public function getPriority(): int

+ 1 - 1
src/Fixer/Basic/BracesFixer.php

@@ -142,7 +142,7 @@ class Foo
      * {@inheritdoc}
      *
      * Must run before HeredocIndentationFixer.
-     * Must run after ClassAttributesSeparationFixer, ClassDefinitionFixer, EmptyLoopBodyFixer, NoAlternativeSyntaxFixer, NoEmptyStatementFixer, NoUselessElseFixer, SingleLineThrowFixer, SingleSpaceAfterConstructFixer, SingleTraitInsertPerStatementFixer.
+     * Must run after ClassAttributesSeparationFixer, ClassDefinitionFixer, EmptyLoopBodyFixer, NoAlternativeSyntaxFixer, NoEmptyStatementFixer, NoUselessElseFixer, SingleLineThrowFixer, SingleSpaceAfterConstructFixer, SingleSpaceAroundConstructFixer, SingleTraitInsertPerStatementFixer.
      */
     public function getPriority(): int
     {

+ 1 - 1
src/Fixer/FunctionNotation/FunctionDeclarationFixer.php

@@ -103,7 +103,7 @@ $f = fn () => null;
      * {@inheritdoc}
      *
      * Must run before MethodArgumentSpaceFixer.
-     * Must run after SingleSpaceAfterConstructFixer.
+     * Must run after SingleSpaceAfterConstructFixer, SingleSpaceAroundConstructFixer.
      */
     public function getPriority(): int
     {

+ 32 - 177
src/Fixer/LanguageConstruct/SingleSpaceAfterConstructFixer.php

@@ -14,8 +14,9 @@ declare(strict_types=1);
 
 namespace PhpCsFixer\Fixer\LanguageConstruct;
 
-use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\AbstractProxyFixer;
 use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\Fixer\DeprecatedFixerInterface;
 use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
 use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
 use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
@@ -23,15 +24,14 @@ use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
 use PhpCsFixer\FixerDefinition\CodeSample;
 use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
-use PhpCsFixer\Preg;
 use PhpCsFixer\Tokenizer\CT;
-use PhpCsFixer\Tokenizer\Token;
-use PhpCsFixer\Tokenizer\Tokens;
 
 /**
  * @author Andreas Möller <am@localheinz.com>
+ *
+ * @deprecated
  */
-final class SingleSpaceAfterConstructFixer extends AbstractFixer implements ConfigurableFixerInterface
+final class SingleSpaceAfterConstructFixer extends AbstractProxyFixer implements ConfigurableFixerInterface, DeprecatedFixerInterface
 {
     /**
      * @var array<string, null|int>
@@ -100,10 +100,25 @@ final class SingleSpaceAfterConstructFixer extends AbstractFixer implements Conf
         'yield_from' => T_YIELD_FROM,
     ];
 
+    private SingleSpaceAroundConstructFixer $singleSpaceAroundConstructFixer;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct()
+    {
+        $this->singleSpaceAroundConstructFixer = new SingleSpaceAroundConstructFixer();
+
+        parent::__construct();
+    }
+
     /**
-     * @var array<string, int>
+     * {@inheritdoc}
      */
-    private array $fixTokenMap = [];
+    public function getSuccessorsNames(): array
+    {
+        return array_keys($this->proxyFixers);
+    }
 
     /**
      * {@inheritdoc}
@@ -112,37 +127,13 @@ final class SingleSpaceAfterConstructFixer extends AbstractFixer implements Conf
     {
         parent::configure($configuration);
 
-        if (\defined('T_MATCH')) { // @TODO: drop condition when PHP 8.0+ is required
-            self::$tokenMap['match'] = T_MATCH;
-        }
-
-        if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
-            self::$tokenMap['readonly'] = T_READONLY;
-        }
-
-        if (\defined('T_ENUM')) { // @TODO: drop condition when PHP 8.1+ is required
-            self::$tokenMap['enum'] = T_ENUM;
-        }
-
-        $this->fixTokenMap = [];
-
-        foreach ($this->configuration['constructs'] as $key) {
-            if (null !== self::$tokenMap[$key]) {
-                $this->fixTokenMap[$key] = self::$tokenMap[$key];
-            }
-        }
-
-        if (isset($this->fixTokenMap['public'])) {
-            $this->fixTokenMap['constructor_public'] = CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC;
-        }
-
-        if (isset($this->fixTokenMap['protected'])) {
-            $this->fixTokenMap['constructor_protected'] = CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED;
-        }
-
-        if (isset($this->fixTokenMap['private'])) {
-            $this->fixTokenMap['constructor_private'] = CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE;
-        }
+        $this->singleSpaceAroundConstructFixer->configure([
+            'constructs_contain_a_single_space' => [
+                'yield_from',
+            ],
+            'constructs_preceded_by_a_single_space' => [],
+            'constructs_followed_by_a_single_space' => $this->configuration['constructs'],
+        ]);
     }
 
     /**
@@ -193,87 +184,15 @@ yield  from  baz();
      */
     public function getPriority(): int
     {
-        return 36;
+        return parent::getPriority();
     }
 
     /**
      * {@inheritdoc}
      */
-    public function isCandidate(Tokens $tokens): bool
+    protected function createProxyFixers(): array
     {
-        return $tokens->isAnyTokenKindsFound(array_values($this->fixTokenMap)) && !$tokens->hasAlternativeSyntax();
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
-    {
-        $tokenKinds = array_values($this->fixTokenMap);
-
-        for ($index = $tokens->count() - 2; $index >= 0; --$index) {
-            $token = $tokens[$index];
-
-            if (!$token->isGivenKind($tokenKinds)) {
-                continue;
-            }
-
-            $whitespaceTokenIndex = $index + 1;
-
-            if ($tokens[$whitespaceTokenIndex]->equalsAny([',', ';', ')', [CT::T_ARRAY_SQUARE_BRACE_CLOSE], [CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE]])) {
-                continue;
-            }
-
-            if (
-                $token->isGivenKind(T_STATIC)
-                && !$tokens[$tokens->getNextMeaningfulToken($index)]->isGivenKind([T_FUNCTION, T_VARIABLE])
-            ) {
-                continue;
-            }
-
-            if ($token->isGivenKind(T_OPEN_TAG)) {
-                if ($tokens[$whitespaceTokenIndex]->equals([T_WHITESPACE]) && !str_contains($tokens[$whitespaceTokenIndex]->getContent(), "\n") && !str_contains($token->getContent(), "\n")) {
-                    $tokens->clearAt($whitespaceTokenIndex);
-                }
-
-                continue;
-            }
-
-            if ($token->isGivenKind(T_CLASS) && $tokens[$tokens->getNextMeaningfulToken($index)]->equals('(')) {
-                continue;
-            }
-
-            if ($token->isGivenKind([T_EXTENDS, T_IMPLEMENTS]) && $this->isMultilineExtendsOrImplementsWithMoreThanOneAncestor($tokens, $index)) {
-                continue;
-            }
-
-            if ($token->isGivenKind(T_RETURN) && $this->isMultiLineReturn($tokens, $index)) {
-                continue;
-            }
-
-            if ($token->isGivenKind(T_CONST) && $this->isMultilineConstant($tokens, $index)) {
-                continue;
-            }
-
-            if ($token->isComment() || $token->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
-                if ($tokens[$whitespaceTokenIndex]->equals([T_WHITESPACE]) && str_contains($tokens[$whitespaceTokenIndex]->getContent(), "\n")) {
-                    continue;
-                }
-            }
-
-            $tokens->ensureWhitespaceAtIndex($whitespaceTokenIndex, 0, ' ');
-
-            if (
-                $token->isGivenKind(T_YIELD_FROM)
-                && 'yield from' !== strtolower($token->getContent())
-            ) {
-                $tokens[$index] = new Token([T_YIELD_FROM, Preg::replace(
-                    '/\s+/',
-                    ' ',
-                    $token->getContent()
-                )]);
-            }
-        }
+        return [$this->singleSpaceAroundConstructFixer];
     }
 
     protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
@@ -291,68 +210,4 @@ yield  from  baz();
                 ->getOption(),
         ]);
     }
-
-    private function isMultiLineReturn(Tokens $tokens, int $index): bool
-    {
-        ++$index;
-        $tokenFollowingReturn = $tokens[$index];
-
-        if (
-            !$tokenFollowingReturn->isGivenKind(T_WHITESPACE)
-            || !str_contains($tokenFollowingReturn->getContent(), "\n")
-        ) {
-            return false;
-        }
-
-        $nestedCount = 0;
-
-        for ($indexEnd = \count($tokens) - 1, ++$index; $index < $indexEnd; ++$index) {
-            if (str_contains($tokens[$index]->getContent(), "\n")) {
-                return true;
-            }
-
-            if ($tokens[$index]->equals('{')) {
-                ++$nestedCount;
-            } elseif ($tokens[$index]->equals('}')) {
-                --$nestedCount;
-            } elseif (0 === $nestedCount && $tokens[$index]->equalsAny([';', [T_CLOSE_TAG]])) {
-                break;
-            }
-        }
-
-        return false;
-    }
-
-    private function isMultilineExtendsOrImplementsWithMoreThanOneAncestor(Tokens $tokens, int $index): bool
-    {
-        $hasMoreThanOneAncestor = false;
-
-        while (++$index) {
-            $token = $tokens[$index];
-
-            if ($token->equals(',')) {
-                $hasMoreThanOneAncestor = true;
-
-                continue;
-            }
-
-            if ($token->equals('{')) {
-                return false;
-            }
-
-            if ($hasMoreThanOneAncestor && str_contains($token->getContent(), "\n")) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    private function isMultilineConstant(Tokens $tokens, int $index): bool
-    {
-        $scopeEnd = $tokens->getNextTokenOfKind($index, [';', [T_CLOSE_TAG]]) - 1;
-        $hasMoreThanOneConstant = null !== $tokens->findSequence([new Token(',')], $index + 1, $scopeEnd);
-
-        return $hasMoreThanOneConstant && $tokens->isPartialCodeMultiline($index, $scopeEnd);
-    }
 }

+ 462 - 0
src/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixer.php

@@ -0,0 +1,462 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Fixer\LanguageConstruct;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\Preg;
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Andreas Möller <am@localheinz.com>
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ */
+final class SingleSpaceAroundConstructFixer extends AbstractFixer implements ConfigurableFixerInterface
+{
+    /**
+     * @var array<string, null|int>
+     */
+    private static array $tokenMapContainASingleSpace = [
+        // for now, only one case - but we are ready to extend it, when we learn about new cases to cover
+        'yield_from' => T_YIELD_FROM,
+    ];
+
+    /**
+     * @var array<string, null|int>
+     */
+    private static array $tokenMapPrecededByASingleSpace = [
+        // for now, only one case - but we are ready to extend it, when we learn about new cases to cover
+        'use_lambda' => CT::T_USE_LAMBDA,
+    ];
+
+    /**
+     * @var array<string, null|int>
+     */
+    private static array $tokenMapFollowedByASingleSpace = [
+        'abstract' => T_ABSTRACT,
+        'as' => T_AS,
+        'attribute' => CT::T_ATTRIBUTE_CLOSE,
+        'break' => T_BREAK,
+        'case' => T_CASE,
+        'catch' => T_CATCH,
+        'class' => T_CLASS,
+        'clone' => T_CLONE,
+        'comment' => T_COMMENT,
+        'const' => T_CONST,
+        'const_import' => CT::T_CONST_IMPORT,
+        'continue' => T_CONTINUE,
+        'do' => T_DO,
+        'echo' => T_ECHO,
+        'else' => T_ELSE,
+        'elseif' => T_ELSEIF,
+        'enum' => null,
+        'extends' => T_EXTENDS,
+        'final' => T_FINAL,
+        'finally' => T_FINALLY,
+        'for' => T_FOR,
+        'foreach' => T_FOREACH,
+        'function' => T_FUNCTION,
+        'function_import' => CT::T_FUNCTION_IMPORT,
+        'global' => T_GLOBAL,
+        'goto' => T_GOTO,
+        'if' => T_IF,
+        'implements' => T_IMPLEMENTS,
+        'include' => T_INCLUDE,
+        'include_once' => T_INCLUDE_ONCE,
+        'instanceof' => T_INSTANCEOF,
+        'insteadof' => T_INSTEADOF,
+        'interface' => T_INTERFACE,
+        'match' => null,
+        'named_argument' => CT::T_NAMED_ARGUMENT_COLON,
+        'namespace' => T_NAMESPACE,
+        'new' => T_NEW,
+        'open_tag_with_echo' => T_OPEN_TAG_WITH_ECHO,
+        'php_doc' => T_DOC_COMMENT,
+        'php_open' => T_OPEN_TAG,
+        'print' => T_PRINT,
+        'private' => T_PRIVATE,
+        'protected' => T_PROTECTED,
+        'public' => T_PUBLIC,
+        'readonly' => null,
+        'require' => T_REQUIRE,
+        'require_once' => T_REQUIRE_ONCE,
+        'return' => T_RETURN,
+        'static' => T_STATIC,
+        'switch' => T_SWITCH,
+        'throw' => T_THROW,
+        'trait' => T_TRAIT,
+        'try' => T_TRY,
+        'type_colon' => CT::T_TYPE_COLON,
+        'use' => T_USE,
+        'use_lambda' => CT::T_USE_LAMBDA,
+        'use_trait' => CT::T_USE_TRAIT,
+        'var' => T_VAR,
+        'while' => T_WHILE,
+        'yield' => T_YIELD,
+        'yield_from' => T_YIELD_FROM,
+    ];
+
+    /**
+     * @var array<string, int>
+     */
+    private array $fixTokenMapFollowedByASingleSpace = [];
+
+    /**
+     * @var array<string, int>
+     */
+    private array $fixTokenMapContainASingleSpace = [];
+
+    /**
+     * @var array<string, int>
+     */
+    private array $fixTokenMapPrecededByASingleSpace = [];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function configure(array $configuration): void
+    {
+        parent::configure($configuration);
+
+        if (\defined('T_MATCH')) { // @TODO: drop condition when PHP 8.0+ is required
+            self::$tokenMapFollowedByASingleSpace['match'] = T_MATCH;
+        }
+
+        if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
+            self::$tokenMapFollowedByASingleSpace['readonly'] = T_READONLY;
+        }
+
+        if (\defined('T_ENUM')) { // @TODO: drop condition when PHP 8.1+ is required
+            self::$tokenMapFollowedByASingleSpace['enum'] = T_ENUM;
+        }
+
+        $this->fixTokenMapContainASingleSpace = [];
+
+        foreach ($this->configuration['constructs_contain_a_single_space'] as $key) {
+            if (null !== self::$tokenMapContainASingleSpace[$key]) {
+                $this->fixTokenMapContainASingleSpace[$key] = self::$tokenMapContainASingleSpace[$key];
+            }
+        }
+
+        $this->fixTokenMapPrecededByASingleSpace = [];
+
+        foreach ($this->configuration['constructs_preceded_by_a_single_space'] as $key) {
+            if (null !== self::$tokenMapPrecededByASingleSpace[$key]) {
+                $this->fixTokenMapPrecededByASingleSpace[$key] = self::$tokenMapPrecededByASingleSpace[$key];
+            }
+        }
+
+        $this->fixTokenMapFollowedByASingleSpace = [];
+
+        foreach ($this->configuration['constructs_followed_by_a_single_space'] as $key) {
+            if (null !== self::$tokenMapFollowedByASingleSpace[$key]) {
+                $this->fixTokenMapFollowedByASingleSpace[$key] = self::$tokenMapFollowedByASingleSpace[$key];
+            }
+        }
+
+        if (isset($this->fixTokenMapFollowedByASingleSpace['public'])) {
+            $this->fixTokenMapFollowedByASingleSpace['constructor_public'] = CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC;
+        }
+
+        if (isset($this->fixTokenMapFollowedByASingleSpace['protected'])) {
+            $this->fixTokenMapFollowedByASingleSpace['constructor_protected'] = CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED;
+        }
+
+        if (isset($this->fixTokenMapFollowedByASingleSpace['private'])) {
+            $this->fixTokenMapFollowedByASingleSpace['constructor_private'] = CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Ensures a single space after language constructs.',
+            [
+                new CodeSample(
+                    '<?php
+
+throw  new  \Exception();
+'
+                ),
+                new CodeSample(
+                    '<?php
+
+function foo() { yield  from  baz(); }
+',
+                    [
+                        'constructs_contain_a_single_space' => [
+                            'yield_from',
+                        ],
+                        'constructs_followed_by_a_single_space' => [
+                            'yield_from',
+                        ],
+                    ]
+                ),
+
+                new CodeSample(
+                    '<?php
+
+$foo = function& ()use($bar) {
+};
+',
+                    [
+                        'constructs_preceded_by_a_single_space' => [
+                            'use_lambda',
+                        ],
+                        'constructs_followed_by_a_single_space' => [
+                            'use_lambda',
+                        ],
+                    ]
+                ),
+                new CodeSample(
+                    '<?php
+
+echo  "Hello!";
+',
+                    [
+                        'constructs_followed_by_a_single_space' => [
+                            'echo',
+                        ],
+                    ]
+                ),
+                new CodeSample(
+                    '<?php
+
+yield  from  baz();
+',
+                    [
+                        'constructs_followed_by_a_single_space' => [
+                            'yield_from',
+                        ],
+                    ]
+                ),
+            ]
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * Must run before BracesFixer, FunctionDeclarationFixer.
+     * Must run after ModernizeStrposFixer.
+     */
+    public function getPriority(): int
+    {
+        return 36;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens): bool
+    {
+        $tokenKinds = array_merge(
+            array_values($this->fixTokenMapContainASingleSpace),
+            array_values($this->fixTokenMapPrecededByASingleSpace),
+            array_values($this->fixTokenMapFollowedByASingleSpace),
+        );
+
+        return $tokens->isAnyTokenKindsFound($tokenKinds) && !$tokens->hasAlternativeSyntax();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    {
+        $tokenKindsContainASingleSpace = array_values($this->fixTokenMapContainASingleSpace);
+
+        for ($index = $tokens->count() - 1; $index > 0; --$index) {
+            if ($tokens[$index]->isGivenKind($tokenKindsContainASingleSpace)) {
+                $token = $tokens[$index];
+
+                if (
+                    $token->isGivenKind(T_YIELD_FROM)
+                    && 'yield from' !== strtolower($token->getContent())
+                ) {
+                    $tokens[$index] = new Token([T_YIELD_FROM, Preg::replace(
+                        '/\s+/',
+                        ' ',
+                        $token->getContent()
+                    )]);
+                }
+            }
+        }
+
+        $tokenKindsPrecededByASingleSpace = array_values($this->fixTokenMapPrecededByASingleSpace);
+
+        for ($index = $tokens->count() - 1; $index > 0; --$index) {
+            if ($tokens[$index]->isGivenKind($tokenKindsPrecededByASingleSpace)) {
+                $tokens->ensureWhitespaceAtIndex($index - 1, 1, ' ');
+            }
+        }
+
+        $tokenKindsFollowedByASingleSpace = array_values($this->fixTokenMapFollowedByASingleSpace);
+
+        for ($index = $tokens->count() - 2; $index >= 0; --$index) {
+            $token = $tokens[$index];
+
+            if (!$token->isGivenKind($tokenKindsFollowedByASingleSpace)) {
+                continue;
+            }
+
+            $whitespaceTokenIndex = $index + 1;
+
+            if ($tokens[$whitespaceTokenIndex]->equalsAny([',', ';', ')', [CT::T_ARRAY_SQUARE_BRACE_CLOSE], [CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE]])) {
+                continue;
+            }
+
+            if (
+                $token->isGivenKind(T_STATIC)
+                && !$tokens[$tokens->getNextMeaningfulToken($index)]->isGivenKind([T_FUNCTION, T_VARIABLE])
+            ) {
+                continue;
+            }
+
+            if ($token->isGivenKind(T_OPEN_TAG)) {
+                if ($tokens[$whitespaceTokenIndex]->equals([T_WHITESPACE]) && !str_contains($tokens[$whitespaceTokenIndex]->getContent(), "\n") && !str_contains($token->getContent(), "\n")) {
+                    $tokens->clearAt($whitespaceTokenIndex);
+                }
+
+                continue;
+            }
+
+            if ($token->isGivenKind(T_CLASS) && $tokens[$tokens->getNextMeaningfulToken($index)]->equals('(')) {
+                continue;
+            }
+
+            if ($token->isGivenKind([T_EXTENDS, T_IMPLEMENTS]) && $this->isMultilineExtendsOrImplementsWithMoreThanOneAncestor($tokens, $index)) {
+                continue;
+            }
+
+            if ($token->isGivenKind(T_RETURN) && $this->isMultiLineReturn($tokens, $index)) {
+                continue;
+            }
+
+            if ($token->isGivenKind(T_CONST) && $this->isMultilineConstant($tokens, $index)) {
+                continue;
+            }
+
+            if ($token->isComment() || $token->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
+                if ($tokens[$whitespaceTokenIndex]->equals([T_WHITESPACE]) && str_contains($tokens[$whitespaceTokenIndex]->getContent(), "\n")) {
+                    continue;
+                }
+            }
+
+            $tokens->ensureWhitespaceAtIndex($whitespaceTokenIndex, 0, ' ');
+        }
+    }
+
+    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
+    {
+        $tokenMapContainASingleSpaceKeys = array_keys(self::$tokenMapContainASingleSpace);
+        $tokenMapPrecededByASingleSpaceKeys = array_keys(self::$tokenMapPrecededByASingleSpace);
+        $tokenMapFollowedByASingleSpaceKeys = array_keys(self::$tokenMapFollowedByASingleSpace);
+
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('constructs_contain_a_single_space', 'List of constructs which must contain a single space.'))
+                ->setAllowedTypes(['array'])
+                ->setAllowedValues([new AllowedValueSubset($tokenMapContainASingleSpaceKeys)])
+                ->setDefault($tokenMapContainASingleSpaceKeys)
+                ->getOption(),
+            (new FixerOptionBuilder('constructs_preceded_by_a_single_space', 'List of constructs which must be preceded by a single space.'))
+                ->setAllowedTypes(['array'])
+                ->setAllowedValues([new AllowedValueSubset($tokenMapPrecededByASingleSpaceKeys)])
+                ->setDefault($tokenMapPrecededByASingleSpaceKeys)
+                ->getOption(),
+            (new FixerOptionBuilder('constructs_followed_by_a_single_space', 'List of constructs which must be followed by a single space.'))
+                ->setAllowedTypes(['array'])
+                ->setAllowedValues([new AllowedValueSubset($tokenMapFollowedByASingleSpaceKeys)])
+                ->setDefault($tokenMapFollowedByASingleSpaceKeys)
+                ->getOption(),
+        ]);
+    }
+
+    private function isMultiLineReturn(Tokens $tokens, int $index): bool
+    {
+        ++$index;
+        $tokenFollowingReturn = $tokens[$index];
+
+        if (
+            !$tokenFollowingReturn->isGivenKind(T_WHITESPACE)
+            || !str_contains($tokenFollowingReturn->getContent(), "\n")
+        ) {
+            return false;
+        }
+
+        $nestedCount = 0;
+
+        for ($indexEnd = \count($tokens) - 1, ++$index; $index < $indexEnd; ++$index) {
+            if (str_contains($tokens[$index]->getContent(), "\n")) {
+                return true;
+            }
+
+            if ($tokens[$index]->equals('{')) {
+                ++$nestedCount;
+            } elseif ($tokens[$index]->equals('}')) {
+                --$nestedCount;
+            } elseif (0 === $nestedCount && $tokens[$index]->equalsAny([';', [T_CLOSE_TAG]])) {
+                break;
+            }
+        }
+
+        return false;
+    }
+
+    private function isMultilineExtendsOrImplementsWithMoreThanOneAncestor(Tokens $tokens, int $index): bool
+    {
+        $hasMoreThanOneAncestor = false;
+
+        while (++$index) {
+            $token = $tokens[$index];
+
+            if ($token->equals(',')) {
+                $hasMoreThanOneAncestor = true;
+
+                continue;
+            }
+
+            if ($token->equals('{')) {
+                return false;
+            }
+
+            if ($hasMoreThanOneAncestor && str_contains($token->getContent(), "\n")) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private function isMultilineConstant(Tokens $tokens, int $index): bool
+    {
+        $scopeEnd = $tokens->getNextTokenOfKind($index, [';', [T_CLOSE_TAG]]) - 1;
+        $hasMoreThanOneConstant = null !== $tokens->findSequence([new Token(',')], $index + 1, $scopeEnd);
+
+        return $hasMoreThanOneConstant && $tokens->isPartialCodeMultiline($index, $scopeEnd);
+    }
+}

Some files were not shown because too many files changed in this diff