Browse Source

PHP8.1 - "final const" support

SpacePossum 3 years ago
parent
commit
ef7be2f845

+ 10 - 10
php-cs-fixer

@@ -21,22 +21,22 @@ if (defined('HHVM_VERSION_ID')) {
     } else {
         exit(1);
     }
+} elseif (!defined('PHP_VERSION_ID')) { // PHP_VERSION_ID is available as of PHP 5.2.7
+    fwrite(STDERR, 'PHP version no supported, please update. Current PHP version: '.PHP_VERSION.".\n");
+
+    exit(1);
+} elseif (\PHP_VERSION_ID === 80000) {
+    fwrite(STDERR, "PHP CS Fixer is not able run on PHP 8.0.0 due to bug in PHP tokenizer (https://bugs.php.net/bug.php?id=80462).\n");
+    fwrite(STDERR, "Update PHP version to unblock execution.\n");
+
+    exit(1);
 } elseif (
-    !defined('PHP_VERSION_ID')
-    || \PHP_VERSION_ID === 80000
-    || \PHP_VERSION_ID < 70103
+    \PHP_VERSION_ID < 70103
     || \PHP_VERSION_ID >= 80100
 ) {
     fwrite(STDERR, "PHP needs to be a minimum version of PHP 7.1.3 and maximum version of PHP 8.0.*.\n");
     fwrite(STDERR, 'Current PHP version: '.PHP_VERSION.".\n");
 
-    if (defined('PHP_VERSION_ID') && \PHP_VERSION_ID === 80000) {
-        fwrite(STDERR, "PHP CS Fixer is not able run on PHP 8.0.0 due to bug in PHP tokenizer (https://bugs.php.net/bug.php?id=80462).\n");
-        fwrite(STDERR, "Update PHP version to unblock execution.\n");
-
-        exit(1);
-    }
-
     if (getenv('PHP_CS_FIXER_IGNORE_ENV')) {
         fwrite(STDERR, "Ignoring environment requirements because `PHP_CS_FIXER_IGNORE_ENV` is set. Execution may be unstable.\n");
     } else {

+ 1 - 4
src/Console/Report/ListSetsReport/ReporterFactory.php

@@ -54,10 +54,7 @@ final class ReporterFactory
         return $this;
     }
 
-    /**
-     * @return $this
-     */
-    public function registerReporter(ReporterInterface $reporter)
+    public function registerReporter(ReporterInterface $reporter): self
     {
         $format = $reporter->getFormat();
 

+ 1 - 3
src/DocBlock/Annotation.php

@@ -163,11 +163,9 @@ final class Annotation
     }
 
     /**
-     * @return TypeExpression
-     *
      * @internal
      */
-    public function getTypeExpression()
+    public function getTypeExpression(): TypeExpression
     {
         return new TypeExpression($this->getTypesContent(), $this->namespace, $this->namespaceUses);
     }

+ 54 - 37
src/Fixer/ClassNotation/NoUnneededFinalMethodFixer.php

@@ -23,6 +23,7 @@ use PhpCsFixer\FixerDefinition\CodeSample;
 use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
 use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\Tokenizer\TokensAnalyzer;
 
 /**
  * @author Filippo Tessarotto <zoeslam@gmail.com>
@@ -90,17 +91,20 @@ class Bar
      */
     protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
     {
-        $tokensCount = \count($tokens);
-        for ($index = 0; $index < $tokensCount; ++$index) {
-            if (!$tokens[$index]->isGivenKind(T_CLASS)) {
+        foreach ($this->getClassMethods($tokens) as $element) {
+            $index = $element['method_final_index'];
+
+            if ($element['class_is_final']) {
+                $this->clearFinal($tokens, $index);
+
                 continue;
             }
 
-            $classOpen = $tokens->getNextTokenOfKind($index, ['{']);
-            $prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
-            $classIsFinal = $prevToken->isGivenKind(T_FINAL);
+            if (!$element['method_is_private'] || false === $this->configuration['private_methods'] || $element['method_is_constructor']) {
+                continue;
+            }
 
-            $this->fixClass($tokens, $classOpen, $classIsFinal);
+            $this->clearFinal($tokens, $index);
         }
     }
 
@@ -117,54 +121,67 @@ class Bar
         ]);
     }
 
-    private function fixClass(Tokens $tokens, int $classOpenIndex, bool $classIsFinal): void
+    private function getClassMethods(Tokens $tokens): \Generator
     {
-        $tokensCount = \count($tokens);
+        $tokensAnalyzer = new TokensAnalyzer($tokens);
+        $modifierKinds = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_STATIC];
 
-        for ($index = $classOpenIndex + 1; $index < $tokensCount; ++$index) {
-            // Class end
-            if ($tokens[$index]->equals('}')) {
-                return;
-            }
+        $classesAreFinal = [];
+        $elements = $tokensAnalyzer->getClassyElements();
 
-            // Skip method content
-            if ($tokens[$index]->equals('{')) {
-                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
+        for (end($elements);; prev($elements)) {
+            $index = key($elements);
 
-                continue;
+            if (null === $index) {
+                break;
             }
 
-            if (!$tokens[$index]->isGivenKind(T_FINAL)) {
-                continue;
+            $element = current($elements);
+
+            if ('method' !== $element['type']) {
+                continue; // not a method
             }
 
-            if (!$classIsFinal && (!$this->isPrivateMethodOtherThanConstructor($tokens, $index, $classOpenIndex) || !$this->configuration['private_methods'])) {
-                continue;
+            $classIndex = $element['classIndex'];
+
+            if (!\array_key_exists($classIndex, $classesAreFinal)) {
+                $prevToken = $tokens[$tokens->getPrevMeaningfulToken($classIndex)];
+                $classesAreFinal[$classIndex] = $prevToken->isGivenKind(T_FINAL);
             }
 
-            $tokens->clearAt($index);
+            $element['class_is_final'] = $classesAreFinal[$classIndex];
+            $element['method_is_constructor'] = '__construct' === strtolower($tokens[$tokens->getNextMeaningfulToken($index)]->getContent());
+            $element['method_final_index'] = null;
+            $element['method_is_private'] = false;
 
-            ++$index;
+            $previous = $index;
 
-            if ($tokens[$index]->isWhitespace()) {
-                $tokens->clearAt($index);
-            }
+            do {
+                $previous = $tokens->getPrevMeaningfulToken($previous);
+
+                if ($tokens[$previous]->isGivenKind(T_PRIVATE)) {
+                    $element['method_is_private'] = true;
+                } elseif ($tokens[$previous]->isGivenKind(T_FINAL)) {
+                    $element['method_final_index'] = $previous;
+                }
+            } while ($tokens[$previous]->isGivenKind($modifierKinds));
+
+            yield $element;
         }
     }
 
-    private function isPrivateMethodOtherThanConstructor(Tokens $tokens, int $index, int $classOpenIndex): bool
+    private function clearFinal(Tokens $tokens, ?int $index): void
     {
-        $index = max($classOpenIndex + 1, $tokens->getPrevTokenOfKind($index, [';', '{', '}']));
-        $private = false;
+        if (null === $index) {
+            return;
+        }
 
-        while (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
-            if ($tokens[$index]->isGivenKind(T_PRIVATE)) {
-                $private = true;
-            }
+        $tokens->clearAt($index);
 
-            $index = $tokens->getNextMeaningfulToken($index);
-        }
+        ++$index;
 
-        return $private && '__construct' !== strtolower($tokens[$tokens->getNextMeaningfulToken($index)]->getContent());
+        if ($tokens[$index]->isWhitespace()) {
+            $tokens->clearAt($index);
+        }
     }
 }

+ 50 - 31
src/Fixer/ClassNotation/ProtectedToPrivateFixer.php

@@ -21,6 +21,7 @@ use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
 use PhpCsFixer\Tokenizer\CT;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\Tokenizer\TokensAnalyzer;
 
 /**
  * @author Filippo Tessarotto <zoeslam@gmail.com>
@@ -75,58 +76,76 @@ final class Sample
      */
     protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
     {
-        $end = \count($tokens) - 3; // min. number of tokens to form a class candidate to fix
-        for ($index = 0; $index < $end; ++$index) {
-            if (!$tokens[$index]->isGivenKind(T_CLASS)) {
-                continue;
-            }
+        $tokensAnalyzer = new TokensAnalyzer($tokens);
+        $modifierKinds = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_NS_SEPARATOR, T_STRING, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, T_STATIC, CT::T_TYPE_ALTERNATION];
+
+        if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
+            $modifierKinds[] = T_READONLY;
+        }
+
+        $classesCandidate = [];
 
-            $classOpen = $tokens->getNextTokenOfKind($index, ['{']);
-            $classClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
+        foreach ($tokensAnalyzer->getClassyElements() as $index => $element) {
+            $classIndex = $element['classIndex'];
 
-            if (!$this->skipClass($tokens, $index, $classOpen, $classClose)) {
-                $this->fixClass($tokens, $classOpen, $classClose);
+            if (!\array_key_exists($classIndex, $classesCandidate)) {
+                $classesCandidate[$classIndex] = $this->isClassCandidate($tokens, $classIndex);
             }
 
-            $index = $classClose;
-        }
-    }
+            if (false === $classesCandidate[$classIndex]) {
+                continue; // not "final" class, "extends", is "anonymous" or uses trait
+            }
 
-    private function fixClass(Tokens $tokens, int $classOpenIndex, int $classCloseIndex): void
-    {
-        for ($index = $classOpenIndex + 1; $index < $classCloseIndex; ++$index) {
-            if ($tokens[$index]->equals('{')) {
-                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
+            $previous = $index;
+            $isProtected = false;
+            $isFinal = false;
+
+            do {
+                $previous = $tokens->getPrevMeaningfulToken($previous);
 
+                if ($tokens[$previous]->isGivenKind(T_PROTECTED)) {
+                    $isProtected = $previous;
+                } elseif ($tokens[$previous]->isGivenKind(T_FINAL)) {
+                    $isFinal = $previous;
+                }
+            } while ($tokens[$previous]->isGivenKind($modifierKinds));
+
+            if (false === $isProtected) {
                 continue;
             }
 
-            if (!$tokens[$index]->isGivenKind(T_PROTECTED)) {
-                continue;
+            if ($isFinal && 'const' === $element['type']) {
+                continue; // Final constants cannot be private
             }
 
-            $tokens[$index] = new Token([T_PRIVATE, 'private']);
+            $element['protected_index'] = $isProtected;
+            $tokens[$element['protected_index']] = new Token([T_PRIVATE, 'private']);
         }
     }
 
-    /**
-     * Decide whether to skip the fix for given class.
-     */
-    private function skipClass(Tokens $tokens, int $classIndex, int $classOpenIndex, int $classCloseIndex): bool
+    private function isClassCandidate(Tokens $tokens, int $classIndex): bool
     {
         $prevToken = $tokens[$tokens->getPrevMeaningfulToken($classIndex)];
+
         if (!$prevToken->isGivenKind(T_FINAL)) {
-            return true;
+            return false;
         }
 
-        for ($index = $classIndex; $index < $classOpenIndex; ++$index) {
-            if ($tokens[$index]->isGivenKind(T_EXTENDS)) {
-                return true;
-            }
+        $classNameIndex = $tokens->getNextMeaningfulToken($classIndex); // move to class name as anonymous class is never "final"
+        $classExtendsIndex = $tokens->getNextMeaningfulToken($classNameIndex); // move to possible "extends"
+
+        if ($tokens[$classExtendsIndex]->isGivenKind(T_EXTENDS)) {
+            return false;
+        }
+
+        if (!$tokens->isTokenKindFound(CT::T_USE_TRAIT)) {
+            return true; // cheap test
         }
 
-        $useIndex = $tokens->getNextTokenOfKind($classIndex, [[CT::T_USE_TRAIT]]);
+        $classOpenIndex = $tokens->getNextTokenOfKind($classNameIndex, ['{']);
+        $classCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpenIndex);
+        $useIndex = $tokens->getNextTokenOfKind($classOpenIndex, [[CT::T_USE_TRAIT]]);
 
-        return $useIndex && $useIndex < $classCloseIndex;
+        return null === $useIndex || $useIndex > $classCloseIndex;
     }
 }

+ 1 - 2
src/Fixer/LanguageConstruct/SingleSpaceAfterConstructFixer.php

@@ -108,8 +108,7 @@ final class SingleSpaceAfterConstructFixer extends AbstractFixer implements Conf
     {
         parent::configure($configuration);
 
-        // @TODO: drop condition when PHP 8.0+ is required
-        if (\defined('T_MATCH')) {
+        if (\defined('T_MATCH')) { // @TODO: drop condition when PHP 8.0+ is required
             self::$tokenMap['match'] = T_MATCH;
         }
 

+ 2 - 2
tests/Console/Command/SelfUpdateCommandTest.php

@@ -238,7 +238,7 @@ OUTPUT;
         $versionChecker = $this->prophesize(\PhpCsFixer\Console\SelfUpdate\NewVersionCheckerInterface::class);
 
         $newMajorVersion = $this->getNewMajorReleaseVersion();
-        $versionChecker->getLatestVersion()->will(function () use ($latestVersionSuccess, $newMajorVersion) {
+        $versionChecker->getLatestVersion()->will(function () use ($latestVersionSuccess, $newMajorVersion): string {
             if ($latestVersionSuccess) {
                 return $newMajorVersion;
             }
@@ -249,7 +249,7 @@ OUTPUT;
         $newMinorVersion = $this->getNewMinorReleaseVersion();
         $versionChecker
             ->getLatestVersionOfMajor($this->getCurrentMajorVersion())
-            ->will(function () use ($latestMinorVersionSuccess, $newMinorVersion) {
+            ->will(function () use ($latestMinorVersionSuccess, $newMinorVersion): string {
                 if ($latestMinorVersionSuccess) {
                     return $newMinorVersion;
                 }

+ 31 - 0
tests/Fixer/Casing/ConstantCaseFixerTest.php

@@ -203,4 +203,35 @@ final class ConstantCaseFixerTest extends AbstractFixerTestCase
             ['<?php class Foo { public function Bar() { return $this?->False; } }'],
         ];
     }
+
+    /**
+     * @dataProvider provideFix81Cases
+     * @requires PHP 8.1
+     */
+    public function testFix81(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public static function provideFix81Cases(): \Generator
+    {
+        yield [
+            '<?php
+                class Foo
+                {
+                    final const TRUE = 1;
+                    public final const FALSE = true;
+                    final public const NULL = null;
+                }
+            ',
+            '<?php
+                class Foo
+                {
+                    final const TRUE = 1;
+                    public final const FALSE = TRUE;
+                    final public const NULL = NULL;
+                }
+            ',
+        ];
+    }
 }

+ 16 - 0
tests/Fixer/Casing/LowercaseStaticReferenceFixerTest.php

@@ -271,4 +271,20 @@ class Foo
 ',
         ];
     }
+
+    /**
+     * @dataProvider provideFix81Cases
+     * @requires PHP 8.1
+     */
+    public function testFix81(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public static function provideFix81Cases(): \Generator
+    {
+        yield [
+            '<?php class A { final const PARENT = 42; }',
+        ];
+    }
 }

+ 25 - 0
tests/Fixer/ClassNotation/ClassAttributesSeparationFixerTest.php

@@ -2206,5 +2206,30 @@ class Foo
     public int $a4;
 }',
         ];
+
+        yield [
+            '<?php
+class Foo
+{
+    final public const B1 = "1";
+
+    public final const B2 = "2";
+
+    final const B3 = "3";
+}
+',
+            '<?php
+class Foo
+{
+    final public const B1 = "1";
+    public final const B2 = "2";
+
+
+    final const B3 = "3";
+
+
+}
+',
+        ];
     }
 }

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