Repository: FriendsOfPHP/PHP-CS-Fixer (php)

Issue:
Extraneous spaces around double colon are not removed
There does not appear to be a setting that handles spacing around double colon, which I would like to have automatically removed.

----

#### The PHP version you are using (`$ php -v`):
```
PHP 7.2.10 (cli) (built: Nov 16 2018 15:24:33) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
```

#### PHP CS Fixer version you are using (`$ php-cs-fixer -V`):
```
PHP CS Fixer 2.14.1 Sunrise by Fabien Potencier and Dariusz Ruminski (2e82abd)
```

#### The command you use to run PHP CS Fixer:
`php-cs-fixer fix --verbose --diff --dry-run`

#### The configuration file you are using, if any:
```php
<?php

declare(strict_types=1);

$finder = PhpCsFixer\Finder::create()
    ->exclude('var')
    ->ignoreDotFiles(false)
    ->name(basename(__FILE__))
    ->in(__DIR__)
;

return PhpCsFixer\Config::create()
    ->setRules([
        '@PHPUnit60Migration:risky' => true,
        '@DoctrineAnnotation' => true,
        '@Symfony' => true,
        '@Symfony:risky' => true,
        '@PHP71Migration' => true,
        '@PHP71Migration:risky' => true,
        '@PhpCsFixer' => true,
        '@PhpCsFixer:risky' => true,
        'array_syntax' => [
            'syntax' => 'short',
        ],
        'align_multiline_comment' => [
            'comment_type' => 'phpdocs_like',
        ],
        'concat_space' => [
            'spacing' => 'one',
        ],
        'explicit_indirect_variable' => true,
        'explicit_string_variable' => true,
        'linebreak_after_opening_tag' => true,
        'list_syntax' => [
            'syntax' => 'short',
        ],
        'method_argument_space' => [
            'on_multiline' => 'ensure_fully_multiline',
        ],
        'method_chaining_indentation' => true,
        'multiline_comment_opening_closing' => true,
        'no_alternative_syntax' => true,
        'no_binary_string' => true,
        'no_php4_constructor' => true,
        'no_superfluous_elseif' => true,
        'no_useless_else' => true,
        'ordered_imports' => true,
        'return_assignment' => true,
        'simplified_null_return' => true,
        'trim_array_spaces' => false,
    ])
    ->setIndent(str_repeat(' ', 4))
    ->setRiskyAllowed(true)
    ->setFinder($finder)
;
```

#### If applicable, please provide minimum samples of PHP code (as plain text, not screenshots):
 * before running PHP CS Fixer (no changes):
```php
$x = self:: FOOBAR;
```

 * with unexpected changes applied when running PHP CS Fixer:
```php
$x = self:: FOOBAR;
```

 * with the changes you expected instead:
```php
$x = self::FOOBAR;
```


Pull request description:
NoSpaceAroundDoubleColonFixer - introduction
closes #4315
fixes following the leading examples in https://www.php-fig.org/psr/psr-2/

Patch:
diff --git a/doc/ruleSets/PSR2.rst b/doc/ruleSets/PSR2.rst
index 09bd5aa01..95992cda7 100644
--- a/doc/ruleSets/PSR2.rst
+++ b/doc/ruleSets/PSR2.rst
@@ -22,6 +22,7 @@ Rules
   ``['on_multiline' => 'ensure_fully_multiline']``
 - `no_break_comment <./../rules/control_structure/no_break_comment.rst>`_
 - `no_closing_tag <./../rules/php_tag/no_closing_tag.rst>`_
+- `no_space_around_double_colon <./../rules/operator/no_space_around_double_colon.rst>`_
 - `no_spaces_after_function_name <./../rules/function_notation/no_spaces_after_function_name.rst>`_
 - `no_spaces_inside_parenthesis <./../rules/whitespace/no_spaces_inside_parenthesis.rst>`_
 - `no_trailing_whitespace <./../rules/whitespace/no_trailing_whitespace.rst>`_
diff --git a/doc/rules/index.rst b/doc/rules/index.rst
index 64a4ce974..e5aad3a26 100644
--- a/doc/rules/index.rst
+++ b/doc/rules/index.rst
@@ -351,6 +351,8 @@ Operator
     Use ``&&`` and ``||`` logical operators instead of ``and`` and ``or``.
 - `new_with_braces <./operator/new_with_braces.rst>`_
     All instances created with new keyword must be followed by braces.
+- `no_space_around_double_colon <./operator/no_space_around_double_colon.rst>`_
+    There must be no space around double colons (also called Scope Resolution Operator or Paamayim Nekudotayim).
 - `not_operator_with_space <./operator/not_operator_with_space.rst>`_
     Logical NOT operators (``!``) should have leading and trailing whitespaces.
 - `not_operator_with_successor_space <./operator/not_operator_with_successor_space.rst>`_
diff --git a/doc/rules/operator/no_space_around_double_colon.rst b/doc/rules/operator/no_space_around_double_colon.rst
new file mode 100644
index 000000000..8e47f9623
--- /dev/null
+++ b/doc/rules/operator/no_space_around_double_colon.rst
@@ -0,0 +1,37 @@
+=====================================
+Rule ``no_space_around_double_colon``
+=====================================
+
+There must be no space around double colons (also called Scope Resolution
+Operator or Paamayim Nekudotayim).
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+
+   -<?php echo Foo\Bar :: class;
+   +<?php echo Foo\Bar::class;
+
+Rule sets
+---------
+
+The rule is part of the following rule sets:
+
+@PSR12
+  Using the `@PSR12 <./../../ruleSets/PSR12.rst>`_ rule set will enable the ``no_space_around_double_colon`` rule.
+
+@PSR2
+  Using the `@PSR2 <./../../ruleSets/PSR2.rst>`_ rule set will enable the ``no_space_around_double_colon`` rule.
+
+@PhpCsFixer
+  Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ rule set will enable the ``no_space_around_double_colon`` rule.
+
+@Symfony
+  Using the `@Symfony <./../../ruleSets/Symfony.rst>`_ rule set will enable the ``no_space_around_double_colon`` rule.
diff --git a/src/Fixer/Operator/NoSpaceAroundDoubleColonFixer.php b/src/Fixer/Operator/NoSpaceAroundDoubleColonFixer.php
new file mode 100644
index 000000000..8d0bec3b8
--- /dev/null
+++ b/src/Fixer/Operator/NoSpaceAroundDoubleColonFixer.php
@@ -0,0 +1,69 @@
+<?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\Operator;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\Tokenizer\Tokens;
+
+final class NoSpaceAroundDoubleColonFixer extends AbstractFixer
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'There must be no space around double colons (also called Scope Resolution Operator or Paamayim Nekudotayim).',
+            [new CodeSample("\n<?php echo Foo\\Bar :: class;\n")]
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens): bool
+    {
+        return $tokens->isTokenKindFound(T_DOUBLE_COLON);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    {
+        for ($index = \count($tokens) - 2; $index > 1; --$index) {
+            if ($tokens[$index]->isGivenKind(T_DOUBLE_COLON)) {
+                $this->removeSpace($tokens, $index, 1);
+                $this->removeSpace($tokens, $index, -1);
+            }
+        }
+    }
+
+    private function removeSpace(Tokens $tokens, int $index, int $direction): void
+    {
+        if (!$tokens[$index + $direction]->isWhitespace()) {
+            return;
+        }
+
+        if ($tokens[$tokens->getNonWhitespaceSibling($index, $direction)]->isComment()) {
+            return;
+        }
+
+        $tokens->clearAt($index + $direction);
+    }
+}
diff --git a/src/RuleSet/Sets/PSR2Set.php b/src/RuleSet/Sets/PSR2Set.php
index a196ed8f5..b9489eeaf 100644
--- a/src/RuleSet/Sets/PSR2Set.php
+++ b/src/RuleSet/Sets/PSR2Set.php
@@ -39,6 +39,7 @@ public function getRules(): array
             ],
             'no_break_comment' => true,
             'no_closing_tag' => true,
+            'no_space_around_double_colon' => true,
             'no_spaces_after_function_name' => true,
             'no_spaces_inside_parenthesis' => true,
             'no_trailing_whitespace' => true,


Test patch:
diff --git a/tests/Fixer/Operator/NoSpaceAroundDoubleColonFixerTest.php b/tests/Fixer/Operator/NoSpaceAroundDoubleColonFixerTest.php
new file mode 100644
index 000000000..473d2f7cf
--- /dev/null
+++ b/tests/Fixer/Operator/NoSpaceAroundDoubleColonFixerTest.php
@@ -0,0 +1,122 @@
+<?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\Tests\Fixer\Operator;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @covers \PhpCsFixer\Fixer\Operator\NoSpaceAroundDoubleColonFixer
+ *
+ * @internal
+ */
+final class NoSpaceAroundDoubleColonFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @dataProvider provideFixCases
+     */
+    public function testFix(string $expected, string $input): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public static function provideFixCases(): \Generator
+    {
+        yield [
+            '<?php echo self::$a;',
+            '<?php echo self :: $a;',
+        ];
+
+        yield [
+            '<?php echo static::$a;',
+            '<?php echo static ::$a;',
+        ];
+
+        yield [
+            '<?php
+                echo F\B::class;
+                echo A\B::     /**/ c;
+                echo C\B/**/::c;
+            ',
+            '<?php
+                echo F\B::    class;
+                echo A\B   ::     /**/ c;
+                echo C\B/**/::   c;
+            ',
+        ];
+
+        yield [
+            '<?php
+namespace {
+    class Foo { public const a = 1; }
+
+    echo Foo::a; // Fix
+    echo "\n".Place\Bar::$a."\n"; // Fix
+}
+
+namespace Somewhere\Over\The\Rainbow {
+    class Bar {
+        public static $a = "BAR-A:: ";
+
+        public function v(?string $z = "zzz"): void
+        {
+            echo "\n".self::$a.$z; // Fix
+            echo "\n".static::class; // Fix
+            echo "\n".static # do ...
+              :: # ... not ...
+            $a.$z; // ... fix
+        }
+    }
+
+    $bar = new Bar();
+    $bar->v();
+}
+
+ # ; echo A :: B;
+// ; echo A :: B;
+/* ; echo A :: B; */
+',
+            '<?php
+namespace {
+    class Foo { public const a = 1; }
+
+    echo Foo:: a; // Fix
+    echo "\n".Place\Bar  ::   $a."\n"; // Fix
+}
+
+namespace Somewhere\Over\The\Rainbow {
+    class Bar {
+        public static $a = "BAR-A:: ";
+
+        public function v(?string $z = "zzz"): void
+        {
+            echo "\n".self  ::  $a.$z; // Fix
+            echo "\n".static  ::  class; // Fix
+            echo "\n".static # do ...
+              :: # ... not ...
+            $a.$z; // ... fix
+        }
+    }
+
+    $bar = new Bar();
+    $bar->v();
+}
+
+ # ; echo A :: B;
+// ; echo A :: B;
+/* ; echo A :: B; */
+',
+        ];
+    }
+}
diff --git a/tests/RuleSet/RuleSetTest.php b/tests/RuleSet/RuleSetTest.php
index 675ee23f0..3c593d457 100644
--- a/tests/RuleSet/RuleSetTest.php
+++ b/tests/RuleSet/RuleSetTest.php
@@ -198,6 +198,7 @@ public function testResolveRulesWithNestedSet(): void
                 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
                 'no_break_comment' => true,
                 'no_closing_tag' => true,
+                'no_space_around_double_colon' => true,
                 'no_spaces_after_function_name' => true,
                 'no_spaces_inside_parenthesis' => true,
                 'no_trailing_whitespace' => true,
@@ -239,6 +240,7 @@ public function testResolveRulesWithDisabledSet(): void
                 'no_break_comment' => true,
                 'no_closing_tag' => true,
                 'no_spaces_after_function_name' => true,
+                'no_space_around_double_colon' => true,
                 'no_spaces_inside_parenthesis' => true,
                 'no_trailing_whitespace' => true,
                 'no_trailing_whitespace_in_comment' => true,