Browse Source

Add PhpdocAddMissingParamAnnotationFixer

Dariusz Ruminski 8 years ago
parent
commit
24c774fdf9

+ 1 - 0
.php_cs.dist

@@ -24,6 +24,7 @@ return PhpCsFixer\Config::create()
         'ordered_class_elements' => true,
         'ordered_imports' => true,
         'php_unit_strict' => true,
+        'phpdoc_add_missing_param_annotation' => true,
         'psr4' => true,
         'strict_comparison' => true,
         'strict_param' => true,

+ 4 - 0
README.rst

@@ -447,6 +447,10 @@ Choose from the list of available rules:
    | "assertEquals".
    | *Rule is: configurable, risky.*
 
+* **phpdoc_add_missing_param_annotation**
+   | Phpdoc should contain @param for all params.
+   | *Rule is: configurable.*
+
 * **phpdoc_align** [@Symfony]
    | All items of the @param, @throws, @return, @var, and @type phpdoc tags
    | must be aligned vertically.

+ 248 - 0
src/Fixer/Phpdoc/PhpdocAddMissingParamAnnotationFixer.php

@@ -0,0 +1,248 @@
+<?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\Phpdoc;
+
+use PhpCsFixer\AbstractFunctionReferenceFixer;
+use PhpCsFixer\DocBlock\DocBlock;
+use PhpCsFixer\DocBlock\Line;
+use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\WhitespacesFixerConfigAwareInterface;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ */
+final class PhpdocAddMissingParamAnnotationFixer extends AbstractFunctionReferenceFixer implements WhitespacesFixerConfigAwareInterface
+{
+    /**
+     * @var array<string, bool>
+     */
+    private $configuration;
+
+    private static $defaultConfiguration = array(
+        'only_untyped' => true,
+    );
+
+    /**
+     * @param null|array<string, bool> $configuration
+     */
+    public function configure(array $configuration = null)
+    {
+        if (null === $configuration) {
+            $this->configuration = self::$defaultConfiguration;
+
+            return;
+        }
+
+        foreach ($configuration as $key => $value) {
+            if (!array_key_exists($key, self::$defaultConfiguration)) {
+                throw new InvalidFixerConfigurationException($this->getName(), sprintf('"%s" is not handled by the fixer.', $key));
+            }
+
+            if (!is_bool($value)) {
+                throw new InvalidFixerConfigurationException($this->getName(), sprintf('Expected boolean got "%s".', is_object($value) ? get_class($value) : gettype($value)));
+            }
+
+            $configuration[$key] = $value;
+        }
+
+        $this->configuration = $configuration;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getPriority()
+    {
+        // must be run after PhpdocNoAliasTagFixer and before PhpdocAlignFixer
+        return -5;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isTokenKindFound(T_DOC_COMMENT);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isRisky()
+    {
+        return false;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function fix(\SplFileInfo $file, Tokens $tokens)
+    {
+        for ($index = 0, $limit = $tokens->count(); $index < $limit; ++$index) {
+            $token = $tokens[$index];
+
+            if (!$token->isGivenKind(T_DOC_COMMENT)) {
+                continue;
+            }
+
+            if (1 === preg_match('/inheritdoc/i', $token->getContent())) {
+                continue;
+            }
+
+            $index = $tokens->getNextMeaningfulToken($index);
+
+            if (null === $index) {
+                return;
+            }
+
+            while ($tokens[$index]->isGivenKind(array(
+                T_ABSTRACT,
+                T_FINAL,
+                T_PRIVATE,
+                T_PROTECTED,
+                T_PUBLIC,
+                T_STATIC,
+                T_VAR,
+            ))) {
+                $index = $tokens->getNextMeaningfulToken($index);
+            }
+
+            if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
+                continue;
+            }
+
+            $openIndex = $tokens->getNextTokenOfKind($index, array('('));
+            $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
+
+            $arguments = array();
+
+            foreach ($this->getArguments($tokens, $openIndex, $index) as $start => $end) {
+                $argumentInfo = $this->prepareArgumentInformation($tokens, $start, $end);
+
+                if (!$this->configuration['only_untyped'] || '' === $argumentInfo['type']) {
+                    $arguments[$argumentInfo['name']] = $argumentInfo;
+                }
+            }
+
+            if (!count($arguments)) {
+                continue;
+            }
+
+            $doc = new DocBlock($token->getContent());
+            $lastParamLine = null;
+
+            foreach ($doc->getAnnotationsOfType('param') as $annotation) {
+                $pregMatched = preg_match('/^[^$]+(\$\w+).*$/s', $annotation->getContent(), $matches);
+
+                if (1 === $pregMatched) {
+                    unset($arguments[$matches[1]]);
+                }
+
+                $lastParamLine = max($lastParamLine, $annotation->getEnd());
+            }
+
+            if (!count($arguments)) {
+                continue;
+            }
+
+            $lines = $doc->getLines();
+            $linesCount = count($lines);
+
+            preg_match('/^(\s*).*$/', $lines[$linesCount - 1]->getContent(), $matches);
+            $indent = $matches[1];
+
+            $newLines = array();
+
+            foreach ($arguments as $argument) {
+                $type = $argument['type'] ?: 'mixed';
+
+                if ('?' !== $type[0] && 'null' === strtolower($argument['default'])) {
+                    $type = 'null|'.$type;
+                }
+
+                $newLines[] = new Line(sprintf(
+                    '%s* @param %s %s%s',
+                    $indent,
+                    $type,
+                    $argument['name'],
+                    $this->whitespacesConfig->getLineEnding()
+                ));
+            }
+
+            array_splice(
+                $lines,
+                $lastParamLine ? $lastParamLine + 1 : $linesCount - 1,
+                0,
+                $newLines
+            );
+
+            $token->setContent(implode('', $lines));
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDescription()
+    {
+        return 'Phpdoc should contain @param for all params.';
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $start
+     * @param int    $end
+     *
+     * @return array
+     */
+    private function prepareArgumentInformation(Tokens $tokens, $start, $end)
+    {
+        $info = array(
+            'default' => '',
+            'name' => '',
+            'type' => '',
+        );
+
+        $sawName = false;
+        $sawEq = false;
+
+        for ($index = $start; $index <= $end; ++$index) {
+            $token = $tokens[$index];
+
+            if ($token->isComment() || $token->isWhitespace()) {
+                continue;
+            }
+
+            if ($token->isGivenKind(T_VARIABLE)) {
+                $sawName = true;
+                $info['name'] = $token->getContent();
+
+                continue;
+            }
+
+            if ($token->equals('=')) {
+                $sawEq = true;
+
+                continue;
+            }
+
+            if ($sawName) {
+                $info['default'] .= $token->getContent();
+            } else {
+                $info['type'] .= $token->getContent();
+            }
+        }
+
+        return $info;
+    }
+}

+ 2 - 0
src/Test/AbstractIntegrationTestCase.php

@@ -100,6 +100,8 @@ abstract class AbstractIntegrationTestCase extends \PHPUnit_Framework_TestCase
      * @dataProvider getTests
      *
      * @see doTest()
+     *
+     * @param IntegrationCase $case
      */
     public function testIntegration(IntegrationCase $case)
     {

+ 301 - 0
tests/Fixer/Phpdoc/PhpdocAddMissingParamAnnotationFixerTest.php

@@ -0,0 +1,301 @@
+<?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\Tests\Fixer\Phpdoc;
+
+use PhpCsFixer\Test\AbstractFixerTestCase;
+use PhpCsFixer\WhitespacesFixerConfig;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ */
+final class PhpdocAddMissingParamAnnotationFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @param string      $expected
+     * @param null|string $input
+     * @param null|array  $config
+     *
+     * @dataProvider provideFixCases
+     */
+    public function testFix($expected, $input = null, array $config = null)
+    {
+        $this->fixer->configure($config ? $config : array('only_untyped' => false));
+
+        $this->doTest($expected, $input);
+    }
+
+    public function provideFixCases()
+    {
+        return array(
+            array(
+                '<?php
+    /**
+     *
+     */',
+            ),
+            array(
+                '<?php
+    /**
+     * @param int $foo
+     * @param mixed $bar
+     */
+    function f1($foo, $bar) {}',
+                '<?php
+    /**
+     * @param int $foo
+     */
+    function f1($foo, $bar) {}',
+            ),
+            array(
+                '<?php
+    /**
+     * @param int $bar
+     * @param mixed $foo
+     */
+    function f2($foo, $bar) {}',
+                '<?php
+    /**
+     * @param int $bar
+     */
+    function f2($foo, $bar) {}',
+            ),
+            array(
+                '<?php
+    /**
+     * @return void
+     * @param mixed $foo
+     * @param mixed $bar
+     */
+    function f3($foo, $bar) {}',
+                '<?php
+    /**
+     * @return void
+     */
+    function f3($foo, $bar) {}',
+            ),
+            array(
+                '<?php
+    abstract class Foo {
+        /**
+         * @param int $bar
+         * @param mixed $foo
+         */
+        abstract public function f4a($foo, $bar);
+    }',
+                '<?php
+    abstract class Foo {
+        /**
+         * @param int $bar
+         */
+        abstract public function f4a($foo, $bar);
+    }',
+            ),
+            array(
+                '<?php
+    class Foo {
+        /**
+         * @param int $bar
+         * @param mixed $foo
+         */
+        static final public function f4b($foo, $bar) {}
+    }',
+                '<?php
+    class Foo {
+        /**
+         * @param int $bar
+         */
+        static final public function f4b($foo, $bar) {}
+    }',
+            ),
+            array(
+                '<?php
+    class Foo {
+        /**
+         * @var int
+         */
+        private $foo;
+    }',
+            ),
+            array(
+                '<?php
+    /**
+     * @param $bar No type !!
+     * @param mixed $foo
+     */
+    function f5($foo, $bar) {}',
+                '<?php
+    /**
+     * @param $bar No type !!
+     */
+    function f5($foo, $bar) {}',
+            ),
+            array(
+                '<?php
+    /**
+     * @param int
+     * @param int $bar
+     * @param Foo\Bar $foo
+     */
+    function f6(Foo\Bar $foo, $bar) {}',
+                '<?php
+    /**
+     * @param int
+     * @param int $bar
+     */
+    function f6(Foo\Bar $foo, $bar) {}',
+            ),
+            array(
+                '<?php
+    /**
+     * @param int $bar
+     * @param null|string $foo
+     */
+    function f7(string $foo = nuLl, $bar) {}',
+                '<?php
+    /**
+     * @param int $bar
+     */
+    function f7(string $foo = nuLl, $bar) {}',
+            ),
+            array(
+                '<?php
+    /**
+     * @param int $bar
+     * @param mixed $baz
+     *
+     * @return void
+     */
+    function f9(string $foo, $bar, $baz) {}',
+                '<?php
+    /**
+     * @param int $bar
+     *
+     * @return void
+     */
+    function f9(string $foo, $bar, $baz) {}',
+                array('only_untyped' => true),
+            ),
+            array(
+                '<?php
+    /**
+     * @param bool|bool[] $caseSensitive Line 1
+     *                                   Line 2
+     */
+     function f11($caseSensitive) {}
+',
+            ),
+        );
+    }
+
+    /**
+     * @param string      $expected
+     * @param null|string $input
+     * @param null|array  $config
+     *
+     * @dataProvider provideCases70
+     * @requires PHP 7.0
+     */
+    public function testFix70($expected, $input = null, array $config = null)
+    {
+        $this->fixer->configure($config ? $config : array('only_untyped' => false));
+
+        $this->doTest($expected, $input);
+    }
+
+    public function provideCases70()
+    {
+        return array(
+            array(
+                '<?php
+    /**
+     * @param int $bar
+     * @param string $foo
+     */
+    function f8(string $foo = "null", $bar) {}',
+                '<?php
+    /**
+     * @param int $bar
+     */
+    function f8(string $foo = "null", $bar) {}',
+            ),
+            array(
+                '<?php
+    /**
+     * @{inheritdoc}
+     */
+    function f10(string $foo = "null", $bar) {}',
+            ),
+        );
+    }
+
+    /**
+     * @param string      $expected
+     * @param null|string $input
+     * @param null|array  $config
+     *
+     * @dataProvider provideCases71
+     * @requires PHP 7.1
+     */
+    public function testFix71($expected, $input = null, array $config = null)
+    {
+        $this->fixer->configure($config ? $config : array('only_untyped' => false));
+
+        $this->doTest($expected, $input);
+    }
+
+    public function provideCases71()
+    {
+        return array(
+            array(
+                '<?php
+    /**
+     * @param int $bar
+     * @param ?array $foo
+     */
+    function p1(?array $foo = null, $bar) {}',
+                '<?php
+    /**
+     * @param int $bar
+     */
+    function p1(?array $foo = null, $bar) {}',
+            ),
+        );
+    }
+
+    /**
+     * @param string      $expected
+     * @param null|string $input
+     * @param null|array  $config
+     *
+     * @dataProvider provideMessyWhitespacesCases
+     */
+    public function testMessyWhitespaces($expected, $input = null, array $config = null)
+    {
+        $this->fixer->setWhitespacesConfig(new WhitespacesFixerConfig("\t", "\r\n"));
+        $this->fixer->configure($config ? $config : array('only_untyped' => false));
+
+        $this->doTest($expected, $input);
+    }
+
+    public function provideMessyWhitespacesCases()
+    {
+        return array(
+            array(
+                "<?php\r\n\t/**\r\n\t * @param int \$bar\r\n\t * @param null|string \$foo\r\n\t */\r\n\tfunction f7(string \$foo = nuLl, \$bar) {}",
+                "<?php\r\n\t/**\r\n\t * @param int \$bar\r\n\t */\r\n\tfunction f7(string \$foo = nuLl, \$bar) {}",
+            ),
+        );
+    }
+}

+ 2 - 0
tests/FixerFactoryTest.php

@@ -289,6 +289,8 @@ final class FixerFactoryTest extends \PHPUnit_Framework_TestCase
             array($fixers['no_blank_lines_after_phpdoc'], $fixers['single_blank_line_before_namespace']), // tested also in: no_blank_lines_after_phpdoc,single_blank_line_before_namespace.test
             array($fixers['php_unit_fqcn_annotation'], $fixers['no_unused_imports']), // tested also in: php_unit_fqcn_annotation,unused_use.test
             array($fixers['protected_to_private'], $fixers['ordered_class_elements']), // tested also in: protected_to_private,ordered_class_elements.test
+            array($fixers['phpdoc_add_missing_param_annotation'], $fixers['phpdoc_align']), // tested also in: phpdoc_add_missing_param_annotation,phpdoc_align.test
+            array($fixers['phpdoc_no_alias_tag'], $fixers['phpdoc_add_missing_param_annotation']), // tested also in: phpdoc_no_alias_tag,phpdoc_add_missing_param_annotation.test
         );
 
         // prepare bulk tests for phpdoc fixers to test that:

+ 23 - 0
tests/Fixtures/Integration/misc/phpdocs.test

@@ -0,0 +1,23 @@
+--TEST--
+Integration of phpdocs fixers.
+--RULESET--
+{
+    "phpdoc_add_missing_param_annotation": true,
+    "phpdoc_align": true,
+    "phpdoc_annotation_without_dot": true,
+    "phpdoc_indent": true,
+    "phpdoc_no_access": true,
+    "phpdoc_no_alias_tag": true,
+    "phpdoc_no_empty_return": true,
+    "phpdoc_order": true,
+    "phpdoc_scalar": true,
+    "phpdoc_separation": true,
+    "phpdoc_summary": true,
+    "phpdoc_trim": true,
+    "phpdoc_types": true,
+    "phpdoc_var_without_name": true,
+    "no_blank_lines_after_phpdoc": true,
+    "no_empty_phpdoc": true
+}
+--SETTINGS--
+{"checkPriority": false}

+ 30 - 0
tests/Fixtures/Integration/misc/phpdocs.test-in.php

@@ -0,0 +1,30 @@
+<?php
+
+class Foo {
+    /**
+     * @access public
+     */
+    public $bar;
+
+    /**
+     * @type array $baz
+     */
+
+
+    public $baz;
+
+/**
+ *
+ * Foo
+ *
+ * @throws Exception
+ * @param inTeGer $fo This is int.
+ *
+ * @param float $bar This is float.
+ * @return void
+ *
+ *
+ * @custom
+ */
+    public function foo ($fo, $bar, array $baz, $qux) {}
+}

+ 25 - 0
tests/Fixtures/Integration/misc/phpdocs.test-out.php

@@ -0,0 +1,25 @@
+<?php
+
+class Foo {
+    
+    public $bar;
+
+    /**
+     * @var array
+     */
+    public $baz;
+
+    /**
+     * Foo.
+     *
+     * @param int   $fo  this is int
+     * @param float $bar this is float
+     * @param mixed $qux
+     *
+     * @throws Exception
+     *
+     *
+     * @custom
+     */
+    public function foo ($fo, $bar, array $baz, $qux) {}
+}

+ 20 - 0
tests/Fixtures/Integration/priority/phpdoc_add_missing_param_annotation,phpdoc_align.test

@@ -0,0 +1,20 @@
+--TEST--
+Integration of fixers: phpdoc_add_missing_param_annotation,phpdoc_align.
+--RULESET--
+{"phpdoc_add_missing_param_annotation": true, "phpdoc_align": true}
+--EXPECT--
+<?php
+/**
+ * @param inTeGer $fo  fo descr
+ * @param float   $bar fo descr
+ * @param mixed   $qux
+ */
+function foo ($fo, $bar, array $baz, $qux) {}
+
+--INPUT--
+<?php
+/**
+ * @param inTeGer $fo fo descr
+ * @param float $bar fo descr
+ */
+function foo ($fo, $bar, array $baz, $qux) {}

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