Browse Source

Merge branch 'master' into 3.0

# Conflicts:
#	tests/Fixer/ClassNotation/MethodSeparationFixerTest.php
Dariusz Ruminski 6 years ago
parent
commit
e789099d32

+ 1 - 1
.php_cs.dist

@@ -66,7 +66,7 @@ $config = PhpCsFixer\Config::create()
         'php_unit_test_class_requires_covers' => true,
         'phpdoc_add_missing_param_annotation' => true,
         'phpdoc_order' => true,
-        'phpdoc_trim_after_description' => true,
+        'phpdoc_trim_consecutive_blank_line_separation' => true,
         'phpdoc_types_order' => true,
         'return_assignment' => true,
         'semicolon_after_instruction' => true,

+ 13 - 1
README.rst

@@ -1343,12 +1343,24 @@ Choose from the list of available rules:
 
   Docblocks should only be used on structural elements.
 
+* **phpdoc_to_return_type**
+
+  EXPERIMENTAL: Takes ``@return`` annotation of non-mixed types and adjusts
+  accordingly the function signature. Requires PHP >= 7.0.
+
+  *Risky rule: [1] This rule is EXPERIMENTAL and is not covered with backward compatibility promise. [2] ``@return`` annotation is mandatory for the fixer to make changes, signatures of methods without it (no docblock, inheritdocs) will not be fixed. [3] Manual actions are required if inherited signatures are not properly documented. [4] ``@inheritdocs`` support is under construction.*
+
+  Configuration options:
+
+  - ``scalar_types`` (``bool``): fix also scalar types; may have unexpected
+    behaviour due to PHP bad type coercion system; defaults to ``true``
+
 * **phpdoc_trim** [@Symfony]
 
   PHPDoc should start and end with content, excluding the very first and
   last line of the docblocks.
 
-* **phpdoc_trim_after_description**
+* **phpdoc_trim_consecutive_blank_line_separation**
 
   Removes extra blank lines after summary and after description in PHPDoc.
 

+ 1 - 1
src/DocBlock/DocBlock.php

@@ -173,7 +173,7 @@ class DocBlock
             }
 
             if (!$line->containsUsefulContent()) {
-                // if we next line is also non-useful, or contains a tag, then we're done here
+                // if next line is also non-useful, or contains a tag, then we're done here
                 $next = $this->getLine($index + 1);
                 if (null === $next || !$next->containsUsefulContent() || $next->containsATag()) {
                     break;

+ 326 - 0
src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php

@@ -0,0 +1,326 @@
+<?php
+
+/*
+ * 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\FunctionNotation;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\DocBlock\Annotation;
+use PhpCsFixer\DocBlock\DocBlock;
+use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\VersionSpecification;
+use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
+use PhpCsFixer\Preg;
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Filippo Tessarotto <zoeslam@gmail.com>
+ */
+final class PhpdocToReturnTypeFixer extends AbstractFixer implements ConfigurableFixerInterface
+{
+    /**
+     * @var array
+     */
+    private $blacklistFuncNames = [
+        [T_STRING, '__construct'],
+        [T_STRING, '__destruct'],
+        [T_STRING, '__clone'],
+    ];
+
+    /**
+     * @var array
+     */
+    private $versionSpecificTypes = [
+        'void' => 70100,
+        'iterable' => 70100,
+        'object' => 70200,
+    ];
+
+    /**
+     * @var array
+     */
+    private $scalarTypes = [
+        'bool' => true,
+        'float' => true,
+        'int' => true,
+        'string' => true,
+    ];
+
+    /**
+     * @var array
+     */
+    private $skippedTypes = [
+        'mixed' => true,
+        'resource' => true,
+        'null' => true,
+    ];
+
+    /**
+     * @var string
+     */
+    private $classRegex = '/^\\\\?[a-zA-Z_\\x7f-\\xff](?:\\\\?[a-zA-Z0-9_\\x7f-\\xff]+)*(?<array>\[\])*$/';
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'EXPERIMENTAL: Takes `@return` annotation of non-mixed types and adjusts accordingly the function signature. Requires PHP >= 7.0.',
+            [
+                new VersionSpecificCodeSample(
+                    '<?php
+
+/** @return \My\Bar */
+function my_foo()
+{}
+',
+                    new VersionSpecification(70000)
+                ),
+                new VersionSpecificCodeSample(
+                    '<?php
+
+/** @return void */
+function my_foo()
+{}
+',
+                    new VersionSpecification(70100)
+                ),
+                new VersionSpecificCodeSample(
+                    '<?php
+
+/** @return object */
+function my_foo()
+{}
+',
+                    new VersionSpecification(70200)
+                ),
+            ],
+            null,
+            '[1] This rule is EXPERIMENTAL and is not covered with backward compatibility promise. [2] `@return` annotation is mandatory for the fixer to make changes, signatures of methods without it (no docblock, inheritdocs) will not be fixed. [3] Manual actions are required if inherited signatures are not properly documented. [4] `@inheritdocs` support is under construction.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return PHP_VERSION_ID >= 70000 && $tokens->isTokenKindFound(T_FUNCTION);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getPriority()
+    {
+        // should be run after PhpdocScalarFixer.
+        // should be run before ReturnTypeDeclarationFixer, FullyQualifiedStrictTypesFixer.
+        return 1;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isRisky()
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function createConfigurationDefinition()
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('scalar_types', 'Fix also scalar types; may have unexpected behaviour due to PHP bad type coercion system.'))
+                ->setAllowedTypes(['bool'])
+                ->setDefault(true)
+                ->getOption(),
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        for ($index = $tokens->count() - 1; 0 < $index; --$index) {
+            if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
+                continue;
+            }
+
+            $funcName = $tokens->getNextMeaningfulToken($index);
+            if ($tokens[$funcName]->equalsAny($this->blacklistFuncNames, false)) {
+                continue;
+            }
+
+            $returnTypeAnnotation = $this->findReturnAnnotations($tokens, $index);
+            if (1 !== count($returnTypeAnnotation)) {
+                continue;
+            }
+            $returnTypeAnnotation = current($returnTypeAnnotation);
+            $types = array_values($returnTypeAnnotation->getTypes());
+            $typesCount = count($types);
+            if (1 > $typesCount || 2 < $typesCount) {
+                continue;
+            }
+
+            $isNullable = false;
+            $returnType = current($types);
+            if (2 === $typesCount) {
+                $null = $types[0];
+                $returnType = $types[1];
+                if ('null' !== $null) {
+                    $null = $types[1];
+                    $returnType = $types[0];
+                }
+
+                if ('null' !== $null) {
+                    continue;
+                }
+
+                $isNullable = true;
+
+                if (PHP_VERSION_ID < 70100) {
+                    continue;
+                }
+
+                if ('void' === $returnType) {
+                    continue;
+                }
+            }
+
+            if ('static' === $returnType) {
+                $returnType = 'self';
+            }
+
+            if (isset($this->skippedTypes[$returnType])) {
+                continue;
+            }
+
+            if (isset($this->versionSpecificTypes[$returnType]) && PHP_VERSION_ID < $this->versionSpecificTypes[$returnType]) {
+                continue;
+            }
+
+            if (isset($this->scalarTypes[$returnType]) && false === $this->configuration['scalar_types']) {
+                continue;
+            }
+
+            if (1 !== Preg::match($this->classRegex, $returnType, $matches)) {
+                continue;
+            }
+
+            if (isset($matches['array'])) {
+                $returnType = 'array';
+            }
+
+            $startIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
+
+            if ($this->hasReturnTypeHint($tokens, $startIndex)) {
+                continue;
+            }
+
+            $this->fixFunctionDefinition($tokens, $startIndex, $isNullable, $returnType);
+        }
+    }
+
+    /**
+     * Determine whether the function already has a return type hint.
+     *
+     * @param Tokens $tokens
+     * @param int    $index  The index of the end of the function definition line, EG at { or ;
+     *
+     * @return bool
+     */
+    private function hasReturnTypeHint(Tokens $tokens, $index)
+    {
+        $endFuncIndex = $tokens->getPrevTokenOfKind($index, [')']);
+        $nextIndex = $tokens->getNextMeaningfulToken($endFuncIndex);
+
+        return $tokens[$nextIndex]->isGivenKind(CT::T_TYPE_COLON);
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $index      The index of the end of the function definition line, EG at { or ;
+     * @param bool   $isNullable
+     * @param string $returnType
+     */
+    private function fixFunctionDefinition(Tokens $tokens, $index, $isNullable, $returnType)
+    {
+        static $specialTypes = [
+            'array' => [CT::T_ARRAY_TYPEHINT, 'array'],
+            'callable' => [T_CALLABLE, 'callable'],
+        ];
+        $newTokens = [
+            new Token([CT::T_TYPE_COLON, ':']),
+            new Token([T_WHITESPACE, ' ']),
+        ];
+        if (true === $isNullable) {
+            $newTokens[] = new Token([CT::T_NULLABLE_TYPE, '?']);
+        }
+
+        if (isset($specialTypes[$returnType])) {
+            $newTokens[] = new Token($specialTypes[$returnType]);
+        } else {
+            foreach (explode('\\', $returnType) as $nsIndex => $value) {
+                if (0 === $nsIndex && '' === $value) {
+                    continue;
+                }
+
+                if (0 < $nsIndex) {
+                    $newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
+                }
+                $newTokens[] = new Token([T_STRING, $value]);
+            }
+        }
+
+        $endFuncIndex = $tokens->getPrevTokenOfKind($index, [')']);
+        $tokens->insertAt($endFuncIndex + 1, $newTokens);
+    }
+
+    /**
+     * Find all the return annotations in the function's PHPDoc comment.
+     *
+     * @param Tokens $tokens
+     * @param int    $index  The index of the function token
+     *
+     * @return Annotation[]
+     */
+    private function findReturnAnnotations(Tokens $tokens, $index)
+    {
+        do {
+            $index = $tokens->getPrevNonWhitespace($index);
+        } while ($tokens[$index]->isGivenKind([
+            T_COMMENT,
+            T_ABSTRACT,
+            T_FINAL,
+            T_PRIVATE,
+            T_PROTECTED,
+            T_PUBLIC,
+            T_STATIC,
+        ]));
+
+        if (!$tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
+            return [];
+        }
+
+        $doc = new DocBlock($tokens[$index]->getContent());
+
+        return $doc->getAnnotationsOfType('return');
+    }
+}

+ 31 - 9
src/Fixer/Phpdoc/PhpdocTrimAfterDescriptionFixer.php → src/Fixer/Phpdoc/PhpdocTrimConsecutiveBlankLineSeparationFixer.php

@@ -23,8 +23,9 @@ use PhpCsFixer\Tokenizer\Tokens;
 
 /**
  * @author Nobu Funaki <nobu.funaki@gmail.com>
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  */
-final class PhpdocTrimAfterDescriptionFixer extends AbstractFixer
+final class PhpdocTrimConsecutiveBlankLineSeparationFixer extends AbstractFixer
 {
     /**
      * {@inheritdoc}
@@ -40,10 +41,16 @@ final class PhpdocTrimAfterDescriptionFixer extends AbstractFixer
  * Summary.
  *
  *
- * Description.
+ * Description that contain 4 lines,
+ *
+ *
+ * while 2 of them are blank!
  *
  *
  * @param string $foo
+ *
+ *
+ * @dataProvider provideFixCases
  */
 function fnc($foo) {}
 '
@@ -73,12 +80,12 @@ function fnc($foo) {}
             $doc = new DocBlock($token->getContent());
             $summaryEnd = (new ShortDescription($doc))->getEnd();
 
-            if (null === $summaryEnd) {
-                continue;
+            if (null !== $summaryEnd) {
+                $this->fixSummary($doc, $summaryEnd);
+                $this->fixDescription($doc, $summaryEnd);
             }
 
-            $this->fixSummary($doc, $summaryEnd);
-            $this->fixDescription($doc, $summaryEnd);
+            $this->fixAllTheRest($doc);
 
             $tokens[$index] = new Token([T_DOC_COMMENT, $doc->getContent()]);
         }
@@ -101,7 +108,8 @@ function fnc($foo) {}
      */
     private function fixDescription(DocBlock $doc, $summaryEnd)
     {
-        $annotationStart = $this->findFirstAnnotation($doc);
+        $annotationStart = $this->findFirstAnnotationOrEnd($doc);
+
         // assuming the end of the Description appears before the first Annotation
         $descriptionEnd = $this->reverseFindLastUsefulContent($doc, $annotationStart);
 
@@ -109,9 +117,23 @@ function fnc($foo) {}
             return; // no Description
         }
 
+        if ($annotationStart === count($doc->getLines()) - 1) {
+            return; // no content after Description
+        }
+
         $this->removeExtraBlankLinesBetween($doc, $descriptionEnd, $annotationStart);
     }
 
+    private function fixAllTheRest(DocBlock $doc)
+    {
+        $annotationStart = $this->findFirstAnnotationOrEnd($doc);
+        $lastLine = $this->reverseFindLastUsefulContent($doc, count($doc->getLines()) - 1);
+
+        if (null !== $lastLine && $annotationStart !== $lastLine) {
+            $this->removeExtraBlankLinesBetween($doc, $annotationStart, $lastLine);
+        }
+    }
+
     /**
      * @param DocBlock $doc
      * @param int      $from
@@ -160,9 +182,9 @@ function fnc($foo) {}
     /**
      * @param DocBlock $doc
      *
-     * @return null|int
+     * @return int
      */
-    private function findFirstAnnotation(DocBlock $doc)
+    private function findFirstAnnotationOrEnd(DocBlock $doc)
     {
         $index = null;
         foreach ($doc->getLines() as $index => $line) {

+ 3 - 0
tests/AutoReview/FixerFactoryTest.php

@@ -173,10 +173,13 @@ final class FixerFactoryTest extends TestCase
             [$fixers['phpdoc_order'], $fixers['phpdoc_separation']],
             [$fixers['phpdoc_order'], $fixers['phpdoc_trim']],
             [$fixers['phpdoc_return_self_reference'], $fixers['no_superfluous_phpdoc_tags']],
+            [$fixers['phpdoc_scalar'], $fixers['phpdoc_to_return_type']],
             [$fixers['phpdoc_separation'], $fixers['phpdoc_trim']],
             [$fixers['phpdoc_summary'], $fixers['phpdoc_trim']],
             [$fixers['phpdoc_to_comment'], $fixers['no_empty_comment']],
             [$fixers['phpdoc_to_comment'], $fixers['phpdoc_no_useless_inheritdoc']],
+            [$fixers['phpdoc_to_return_type'], $fixers['fully_qualified_strict_types']],
+            [$fixers['phpdoc_to_return_type'], $fixers['return_type_declaration']],
             [$fixers['phpdoc_var_without_name'], $fixers['phpdoc_trim']],
             [$fixers['pow_to_exponentiation'], $fixers['binary_operator_spaces']],
             [$fixers['pow_to_exponentiation'], $fixers['method_argument_space']],

+ 2 - 0
tests/DocBlock/ShortDescriptionTest.php

@@ -65,6 +65,8 @@ final class ShortDescriptionTest extends TestCase
                   *
                   * There might be extra blank lines.
                   *
+                  *
+                  * And here is description...
                   */'],
         ];
     }

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

@@ -684,7 +684,6 @@ public function B(); // allowed comment
      * @param string      $expected
      * @param null|string $input
      *
-     *
      * @dataProvider provideFixTraitsCases
      */
     public function testFixTraits($expected, $input = null)

+ 0 - 1
tests/Fixer/ClassNotation/NoBlankLinesAfterClassOpeningFixerTest.php

@@ -39,7 +39,6 @@ final class NoBlankLinesAfterClassOpeningFixerTest extends AbstractFixerTestCase
      * @param string      $expected
      * @param null|string $input
      *
-     *
      * @dataProvider provideTraitsCases
      */
     public function testFixTraits($expected, $input = null)

+ 0 - 1
tests/Fixer/FunctionNotation/MethodArgumentSpaceFixerTest.php

@@ -686,7 +686,6 @@ INPUT
      * @param string $expected
      * @param string $input
      *
-     *
      * @dataProvider provideFix56Cases
      */
     public function testFix56($expected, $input)

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