Browse Source

Merge branch 'master' into 3.0

Dariusz Ruminski 6 years ago
parent
commit
e4898f0d55

+ 5 - 1
README.rst

@@ -1192,10 +1192,12 @@ Choose from the list of available rules:
 
 * **phpdoc_align** [@Symfony]
 
-  All items of the given PHPDoc tags must be aligned vertically.
+  All items of the given phpdoc tags must be either left-aligned or (by
+  default) aligned vertically.
 
   Configuration options:
 
+  - ``align`` (``'left'``, ``'vertical'``): align comments; defaults to ``'vertical'``
   - ``tags`` (a subset of ``['param', 'property', 'return', 'throws', 'type',
     'var', 'method']``): the tags that should be aligned; defaults to
     ``['method', 'param', 'property', 'return', 'throws', 'type', 'var']``
@@ -1547,6 +1549,8 @@ Choose from the list of available rules:
 
   Configuration options:
 
+  - ``always_move_variable`` (``bool``): whether variables should always be on non
+    assignable side when applying Yoda style; defaults to ``false``
   - ``equal`` (``bool``, ``null``): style for equal (``==``, ``!=``) statements; defaults to
     ``true``
   - ``identical`` (``bool``, ``null``): style for identical (``===``, ``!==``) statements;

+ 115 - 24
src/Fixer/ControlStructure/YodaStyleFixer.php

@@ -21,6 +21,7 @@ use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\Tokenizer\CT;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\Tokenizer\TokensAnalyzer;
 
 /**
  * @author Bram Gotink <bram@gotink.me>
@@ -34,6 +35,11 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
      */
     private $candidatesMap;
 
+    /**
+     * @var array<string, null|bool>
+     */
+    private $candidateTypesConfiguration;
+
     /**
      * @var array<int|string>
      */
@@ -76,6 +82,14 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
                         'less_and_greater' => null,
                     ]
                 ),
+                new CodeSample(
+                    '<?php
+return $foo === count($bar);
+',
+                    [
+                        'always_move_variable' => true,
+                    ]
+                ),
             ]
         );
     }
@@ -114,6 +128,10 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
                 ->setAllowedTypes(['bool', 'null'])
                 ->setDefault(null)
                 ->getOption(),
+            (new FixerOptionBuilder('always_move_variable', 'Whether variables should always be on non assignable side when applying Yoda style.'))
+                ->setAllowedTypes(['bool'])
+                ->setDefault(false)
+                ->getOption(),
         ]);
     }
 
@@ -225,12 +243,12 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
     {
         for ($i = count($tokens) - 1; $i > 1; --$i) {
             if ($tokens[$i]->isGivenKind($this->candidateTypes)) {
-                $yoda = $this->configuration[$tokens[$i]->getId()];
+                $yoda = $this->candidateTypesConfiguration[$tokens[$i]->getId()];
             } elseif (
                 ($tokens[$i]->equals('<') && in_array('<', $this->candidateTypes, true))
                 || ($tokens[$i]->equals('>') && in_array('>', $this->candidateTypes, true))
             ) {
-                $yoda = $this->configuration[$tokens[$i]->getContent()];
+                $yoda = $this->candidateTypesConfiguration[$tokens[$i]->getContent()];
             } else {
                 continue;
             }
@@ -333,31 +351,36 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
      */
     private function getCompareFixableInfo(Tokens $tokens, $index, $yoda)
     {
-        if ($yoda) {
-            $right = $this->getRightSideCompareFixableInfo($tokens, $index);
-            if ($this->isVariable($tokens, $right['start'], $right['end']) || $this->isListStatement($tokens, $right['start'], $right['end'])) {
-                return null;
-            }
+        $left = $this->getLeftSideCompareFixableInfo($tokens, $index);
+        $right = $this->getRightSideCompareFixableInfo($tokens, $index);
 
-            $left = $this->getLeftSideCompareFixableInfo($tokens, $index);
-            $otherIsVar = $this->isVariable($tokens, $left['start'], $left['end']);
+        if ($yoda) {
+            $expectedAssignableSide = $right;
+            $expectedValueSide = $left;
         } else {
-            $left = $this->getLeftSideCompareFixableInfo($tokens, $index);
-            if ($this->isVariable($tokens, $left['start'], $left['end']) || $this->isListStatement($tokens, $left['start'], $left['end'])) {
-                return null;
-            }
-
-            $right = $this->getRightSideCompareFixableInfo($tokens, $index);
-
             if ($tokens[$tokens->getNextMeaningfulToken($right['end'])]->equals('=')) {
                 return null;
             }
 
-            $otherIsVar = $this->isVariable($tokens, $right['start'], $right['end']);
+            $expectedAssignableSide = $left;
+            $expectedValueSide = $right;
         }
 
-        // edge case handling, for example `$a === 1 === 2;`
-        if (!$otherIsVar) {
+        if (
+            // variable cannot be moved to expected side
+            !(
+                !$this->isVariable($tokens, $expectedAssignableSide['start'], $expectedAssignableSide['end'], false)
+                && !$this->isListStatement($tokens, $expectedAssignableSide['start'], $expectedAssignableSide['end'])
+                && $this->isVariable($tokens, $expectedValueSide['start'], $expectedValueSide['end'], false)
+            )
+            // variable cannot be moved to expected side (strict mode)
+            && !(
+                $this->configuration['always_move_variable']
+                && !$this->isVariable($tokens, $expectedAssignableSide['start'], $expectedAssignableSide['end'], true)
+                && !$this->isListStatement($tokens, $expectedAssignableSide['start'], $expectedAssignableSide['end'])
+                && $this->isVariable($tokens, $expectedValueSide['start'], $expectedValueSide['end'], true)
+            )
+        ) {
             return null;
         }
 
@@ -481,17 +504,39 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
      * variable.
      *
      * @param Tokens $tokens The token list
-     * @param int    $index  The first index of the possible variable
+     * @param int    $start  The first index of the possible variable
      * @param int    $end    The last index of the possible variable
+     * @param bool   $strict Enable strict variable detection
      *
      * @return bool Whether the tokens describe a variable
      */
-    private function isVariable(Tokens $tokens, $index, $end)
+    private function isVariable(Tokens $tokens, $start, $end, $strict)
     {
-        if ($end === $index) {
-            return $tokens[$index]->isGivenKind(T_VARIABLE);
+        $tokenAnalyzer = new TokensAnalyzer($tokens);
+
+        if ($start === $end) {
+            return $tokens[$start]->isGivenKind(T_VARIABLE);
         }
 
+        if ($strict) {
+            if ($tokens[$start]->equals('(')) {
+                return false;
+            }
+
+            for ($index = $start; $index <= $end; ++$index) {
+                if (
+                    $tokens[$index]->isCast()
+                    || $tokens[$index]->isGivenKind(T_INSTANCEOF)
+                    || $tokens[$index]->equalsAny(['.', '!'])
+                    || $tokenAnalyzer->isBinaryOperator($index)
+                ) {
+                    return false;
+                }
+            }
+        }
+
+        $index = $start;
+
         // handle multiple braces around statement ((($a === 1)))
         while (
             $tokens[$index]->equals('(')
@@ -577,6 +622,11 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
                 continue;
             }
 
+            // $a(...) or $a->b(...)
+            if ($strict && $current->isGivenKind([T_STRING, T_VARIABLE]) && $next->equals('(')) {
+                return false;
+            }
+
             // {...} (as in $a->{$b})
             if ($expectString && $current->isGivenKind(CT::T_DYNAMIC_PROP_BRACE_OPEN)) {
                 $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, $index);
@@ -599,6 +649,47 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
             break;
         }
 
+        return !$this->isConstant($tokens, $start, $end);
+    }
+
+    private function isConstant(Tokens $tokens, $index, $end)
+    {
+        $expectNumberOnly = false;
+        $expectNothing = false;
+
+        for (; $index <= $end; ++$index) {
+            $token = $tokens[$index];
+
+            if ($token->isComment() || $token->isWhitespace()) {
+                if ($expectNothing) {
+                    return false;
+                }
+
+                continue;
+            }
+
+            if ($expectNumberOnly && !$token->isGivenKind([T_LNUMBER, T_DNUMBER])) {
+                return false;
+            }
+
+            if ($token->equals('-')) {
+                $expectNumberOnly = true;
+
+                continue;
+            }
+
+            if (
+                $token->isGivenKind([T_LNUMBER, T_DNUMBER, T_CONSTANT_ENCAPSED_STRING])
+                || $token->equalsAny([[T_STRING, 'true'], [T_STRING, 'false'], [T_STRING, 'null']])
+            ) {
+                $expectNothing = true;
+
+                continue;
+            }
+
+            return false;
+        }
+
         return true;
     }
 
@@ -634,7 +725,7 @@ final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInt
             $this->candidatesMap['>'] = new Token('<');
         }
 
-        $this->configuration = $candidateTypes;
+        $this->candidateTypesConfiguration = $candidateTypes;
         $this->candidateTypes = array_keys($candidateTypes);
     }
 }

+ 125 - 10
src/Fixer/Phpdoc/PhpdocAlignFixer.php

@@ -34,9 +34,31 @@ use PhpCsFixer\Utils;
  */
 final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
 {
+    /**
+     * @internal
+     */
+    const ALIGN_LEFT = 'left';
+
+    /**
+     * @internal
+     */
+    const ALIGN_VERTICAL = 'vertical';
+
+    /**
+     * @var string
+     */
     private $regex;
+
+    /**
+     * @var string
+     */
     private $regexCommentLine;
 
+    /**
+     * @var string
+     */
+    private $align;
+
     private static $alignableTags = [
         'param',
         'property',
@@ -73,19 +95,23 @@ final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerI
         if (!empty($tagsWithNameToAlign)) {
             $types[] = '(?P<tag>'.implode('|', $tagsWithNameToAlign).')\s+(?P<hint>[^$]+?)\s+(?P<var>(?:&|\.{3})?\$[^\s]+)';
         }
+
         // e.g. @return <hint>
         if (!empty($tagsWithoutNameToAlign)) {
             $types[] = '(?P<tag2>'.implode('|', $tagsWithoutNameToAlign).')\s+(?P<hint2>[^\s]+?)';
         }
+
         // e.g. @method <hint> <signature>
         if (!empty($tagsWithMethodSignatureToAlign)) {
             $types[] = '(?P<tag3>'.implode('|', $tagsWithMethodSignatureToAlign).')(\s+(?P<hint3>[^\s(]+)|)\s+(?P<signature>.+\))';
         }
+
         // optional <desc>
         $desc = '(?:\s+(?P<desc>\V*))';
 
         $this->regex = '/^'.$indent.' \* @(?:'.implode('|', $types).')'.$desc.'\s*$/u';
         $this->regexCommentLine = '/^'.$indent.' \*(?! @)(?:\s+(?P<desc>\V+))(?<!\*\/)\r?$/u';
+        $this->align = $this->configuration['align'];
     }
 
     /**
@@ -93,9 +119,8 @@ final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerI
      */
     public function getDefinition()
     {
-        return new FixerDefinition(
-            'All items of the given PHPDoc tags must be aligned vertically.',
-            [new CodeSample('<?php
+        $code = <<<'EOF'
+<?php
 /**
  * @param  EngineInterface $templating
  * @param string      $format
@@ -103,7 +128,16 @@ final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerI
  * @param    bool         $debug
  * @param  mixed    &$reference     a parameter passed by reference
  */
-')]
+
+EOF;
+
+        return new FixerDefinition(
+            'All items of the given phpdoc tags must be either left-aligned or (by default) aligned vertically.',
+            [
+                new CodeSample($code),
+                new CodeSample($code, ['align' => self::ALIGN_VERTICAL]),
+                new CodeSample($code, ['align' => self::ALIGN_LEFT]),
+            ]
         );
     }
 
@@ -168,7 +202,14 @@ final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerI
             ])
         ;
 
-        return new FixerConfigurationResolver([$tags->getOption()]);
+        $align = new FixerOptionBuilder('align', 'Align comments');
+        $align
+            ->setAllowedTypes(['string'])
+            ->setAllowedValues([self::ALIGN_LEFT, self::ALIGN_VERTICAL])
+            ->setDefault(self::ALIGN_VERTICAL)
+        ;
+
+        return new FixerConfigurationResolver([$tags->getOption(), $align->getOption()]);
     }
 
     /**
@@ -240,7 +281,10 @@ final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerI
                     $line =
                         $item['indent']
                         .' *  '
-                        .str_repeat(' ', $tagMax + $hintMax + $varMax + $extraIndent)
+                        .$this->getIndent(
+                            $tagMax + $hintMax + $varMax + $extraIndent,
+                            $this->getLeftAlignedDescriptionIndent($items, $j)
+                        )
                         .$item['desc']
                         .$lineEnding;
 
@@ -255,22 +299,25 @@ final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerI
                     $item['indent']
                     .' * @'
                     .$item['tag']
-                    .str_repeat(' ', $tagMax - strlen($item['tag']) + 1)
+                    .$this->getIndent(
+                        $tagMax - strlen($item['tag']) + 1,
+                        $item['hint'] ? 1 : 0
+                    )
                     .$item['hint']
                 ;
 
                 if (!empty($item['var'])) {
                     $line .=
-                        str_repeat(' ', ($hintMax ?: -1) - strlen($item['hint']) + 1)
+                        $this->getIndent(($hintMax ?: -1) - strlen($item['hint']) + 1)
                         .$item['var']
                         .(
                             !empty($item['desc'])
-                            ? str_repeat(' ', $varMax - strlen($item['var']) + 1).$item['desc'].$lineEnding
+                            ? $this->getIndent($varMax - strlen($item['var']) + 1).$item['desc'].$lineEnding
                             : $lineEnding
                         )
                     ;
                 } elseif (!empty($item['desc'])) {
-                    $line .= str_repeat(' ', $hintMax - strlen($item['hint']) + 1).$item['desc'].$lineEnding;
+                    $line .= $this->getIndent($hintMax - strlen($item['hint']) + 1).$item['desc'].$lineEnding;
                 } else {
                     $line .= $lineEnding;
                 }
@@ -303,6 +350,10 @@ final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerI
                 $matches['var'] = $matches['signature'];
             }
 
+            if (isset($matches['hint'])) {
+                $matches['hint'] = trim($matches['hint']);
+            }
+
             return $matches;
         }
 
@@ -314,4 +365,68 @@ final class PhpdocAlignFixer extends AbstractFixer implements ConfigurableFixerI
             return $matches;
         }
     }
+
+    /**
+     * @param int $verticalAlignIndent
+     * @param int $leftAlignIndent
+     *
+     * @return string
+     */
+    private function getIndent($verticalAlignIndent, $leftAlignIndent = 1)
+    {
+        $indent = self::ALIGN_VERTICAL === $this->align ? $verticalAlignIndent : $leftAlignIndent;
+
+        return \str_repeat(' ', $indent);
+    }
+
+    /**
+     * @param array[] $items
+     * @param int     $index
+     *
+     * @return int
+     */
+    private function getLeftAlignedDescriptionIndent(array $items, $index)
+    {
+        if (self::ALIGN_LEFT !== $this->align) {
+            return 0;
+        }
+
+        // Find last tagged line:
+        $item = null;
+        for (; $index >= 0; --$index) {
+            $item = $items[$index];
+            if (null !== $item['tag']) {
+                break;
+            }
+        }
+
+        // No last tag found — no indent:
+        if (null === $item) {
+            return 0;
+        }
+
+        // Indent according to existing values:
+        return
+            $this->getSentenceIndent($item['tag']) +
+            $this->getSentenceIndent($item['hint']) +
+            $this->getSentenceIndent($item['var']);
+    }
+
+    /**
+     * Get indent for sentence.
+     *
+     * @param null|string $sentence
+     *
+     * @return int
+     */
+    private function getSentenceIndent($sentence)
+    {
+        if (null === $sentence) {
+            return 0;
+        }
+
+        $length = strlen($sentence);
+
+        return 0 === $length ? 0 : $length + 1;
+    }
 }

+ 170 - 5
tests/Fixer/ControlStructure/YodaStyleFixerTest.php

@@ -30,9 +30,9 @@ final class YodaStyleFixerTest extends AbstractFixerTestCase
      *
      * @dataProvider provideFixCases
      */
-    public function testFix($expected, $input = null)
+    public function testFix($expected, $input = null, array $extraConfig = [])
     {
-        $this->fixer->configure(['equal' => true, 'identical' => true]);
+        $this->fixer->configure(['equal' => true, 'identical' => true] + $extraConfig);
         $this->doTest($expected, $input);
     }
 
@@ -44,9 +44,9 @@ final class YodaStyleFixerTest extends AbstractFixerTestCase
      *
      * @dataProvider provideFixCases
      */
-    public function testFixInverse($expected, $input = null)
+    public function testFixInverse($expected, $input = null, array $extraConfig = [])
     {
-        $this->fixer->configure(['equal' => false, 'identical' => false]);
+        $this->fixer->configure(['equal' => false, 'identical' => false] + $extraConfig);
 
         if (null === $input) {
             $this->doTest($expected);
@@ -420,6 +420,171 @@ $a#4
             [
                 '<?php false === $a = array();',
             ],
+            [
+                '<?php $e = count($this->array[$var]) === $a;',
+                '<?php $e = $a === count($this->array[$var]);',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php $i = $this->getStuff() === $myVariable;',
+                '<?php $i = $myVariable === $this->getStuff();',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php $e = count($this->array[$var]) === $a;',
+                '<?php $e = $a === count($this->array[$var]);',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php $g = ($a & self::MY_BITMASK) === $a;',
+                '<?php $g = $a === ($a & self::MY_BITMASK);',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar + 2 === $k;',
+                '<?php return $k === $myVar + 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar - 2 === $k;',
+                '<?php return $k === $myVar - 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar * 2 === $k;',
+                '<?php return $k === $myVar * 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar / 2 === $k;',
+                '<?php return $k === $myVar / 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar % 2 === $k;',
+                '<?php return $k === $myVar % 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar ** 2 === $k;',
+                '<?php return $k === $myVar ** 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar < 2 === $k;',
+                '<?php return $k === $myVar < 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar > 2 === $k;',
+                '<?php return $k === $myVar > 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar <= 2 === $k;',
+                '<?php return $k === $myVar <= 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar >= 2 === $k;',
+                '<?php return $k === $myVar >= 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar . 2 === $k;',
+                '<?php return $k === $myVar . 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar << 2 === $k;',
+                '<?php return $k === $myVar << 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar >> 2 === $k;',
+                '<?php return $k === $myVar >> 2;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return !$myVar === $k;',
+                '<?php return $k === !$myVar;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return $myVar instanceof Foo === $k;',
+                '<?php return $k === $myVar instanceof Foo;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return (bool) $myVar === $k;',
+                '<?php return $k === (bool) $myVar;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return (int) $myVar === $k;',
+                '<?php return $k === (int) $myVar;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return (float) $myVar === $k;',
+                '<?php return $k === (float) $myVar;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return (string) $myVar === $k;',
+                '<?php return $k === (string) $myVar;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return (array) $myVar === $k;',
+                '<?php return $k === (array) $myVar;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php return (object) $myVar === $k;',
+                '<?php return $k === (object) $myVar;',
+                ['always_move_variable' => true],
+            ],
+            [
+                '<?php $a = null === foo();',
+                '<?php $a = foo() === null;',
+            ],
+            [
+                '<?php $a = \'foo\' === foo();',
+                '<?php $a = foo() === \'foo\';',
+            ],
+            [
+                '<?php $a = "foo" === foo();',
+                '<?php $a = foo() === "foo";',
+            ],
+            [
+                '<?php $a = 1 === foo();',
+                '<?php $a = foo() === 1;',
+            ],
+            [
+                '<?php $a = 1.2 === foo();',
+                '<?php $a = foo() === 1.2;',
+            ],
+            [
+                '<?php $a = true === foo();',
+                '<?php $a = foo() === true;',
+            ],
+            [
+                '<?php $a = false === foo();',
+                '<?php $a = foo() === false;',
+            ],
+            [
+                '<?php $a = -1 === reset($foo);',
+                '<?php $a = reset($foo) === -1;',
+            ],
+            [
+                '<?php $a = - 1 === reset($foo);',
+                '<?php $a = reset($foo) === - 1;',
+            ],
+            [
+                '<?php $a = -/* bar */1 === reset($foo);',
+                '<?php $a = reset($foo) === -/* bar */1;',
+            ],
         ];
     }
 
@@ -517,7 +682,7 @@ $a#4
     {
         return [
             [['equal' => 2], 'Invalid configuration: The option "equal" with value 2 is expected to be of type "bool" or "null", but is of type "integer".'],
-            [['_invalid_' => true], 'Invalid configuration: The option "_invalid_" does not exist. Defined options are: "equal", "identical", "less_and_greater".'],
+            [['_invalid_' => true], 'Invalid configuration: The option "_invalid_" does not exist. Defined options are: "always_move_variable", "equal", "identical", "less_and_greater".'],
         ];
     }
 

+ 336 - 0
tests/Fixer/Phpdoc/PhpdocAlignFixerTest.php

@@ -48,6 +48,99 @@ EOF;
      * @param  mixed    &$reference     A parameter passed by reference
      */
 
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
+    public function testFixLeftAlign()
+    {
+        $this->fixer->configure(['tags' => ['param'], 'align' => 'left']);
+
+        $expected = <<<'EOF'
+<?php
+    /**
+     * @param EngineInterface $templating
+     * @param string $format
+     * @param int $code An HTTP response status code
+     * @param bool $debug
+     * @param mixed &$reference A parameter passed by reference
+     */
+
+EOF;
+
+        $input = <<<'EOF'
+<?php
+    /**
+     * @param  EngineInterface $templating
+     * @param string      $format
+     * @param  int  $code       An HTTP response status code
+     * @param    bool         $debug
+     * @param  mixed    &$reference     A parameter passed by reference
+     */
+
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
+    public function testFixPartiallyUntyped()
+    {
+        $this->fixer->configure(['tags' => ['param']]);
+
+        $expected = <<<'EOF'
+<?php
+    /**
+     * @param         $id
+     * @param         $parentId
+     * @param int     $websiteId
+     * @param         $position
+     * @param int[][] $siblings
+     */
+
+EOF;
+
+        $input = <<<'EOF'
+<?php
+    /**
+     * @param      $id
+     * @param    $parentId
+     * @param int $websiteId
+     * @param        $position
+     * @param int[][]  $siblings
+     */
+
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
+    public function testFixPartiallyUntypedLeftAlign()
+    {
+        $this->fixer->configure(['tags' => ['param'], 'align' => 'left']);
+
+        $expected = <<<'EOF'
+<?php
+    /**
+     * @param $id
+     * @param $parentId
+     * @param int $websiteId
+     * @param $position
+     * @param int[][] $siblings
+     */
+
+EOF;
+
+        $input = <<<'EOF'
+<?php
+    /**
+     * @param      $id
+     * @param    $parentId
+     * @param int $websiteId
+     * @param        $position
+     * @param int[][]  $siblings
+     */
+
 EOF;
 
         $this->doTest($expected, $input);
@@ -93,6 +186,51 @@ EOF;
      *                          It does it well
      */
 
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
+    public function testFixMultiLineDescLeftAlign()
+    {
+        $this->fixer->configure(['tags' => ['param', 'property', 'method'], 'align' => 'left']);
+
+        $expected = <<<'EOF'
+<?php
+    /**
+     * @param EngineInterface $templating
+     * @param string $format
+     * @param int $code An HTTP response status code
+     *                  See constants
+     * @param bool $debug
+     * @param bool $debug See constants
+     *                    See constants
+     * @param mixed &$reference A parameter passed by reference
+     * @property mixed $foo A foo
+     *                      See constants
+     * @method static baz($bop) A method that does a thing
+     *                          It does it well
+     */
+
+EOF;
+
+        $input = <<<'EOF'
+<?php
+    /**
+     * @param  EngineInterface $templating
+     * @param string      $format
+     * @param  int  $code       An HTTP response status code
+     *                              See constants
+     * @param    bool         $debug
+     * @param    bool         $debug See constants
+     * See constants
+     * @param  mixed    &$reference     A parameter passed by reference
+     * @property   mixed   $foo     A foo
+     *                               See constants
+     * @method static   baz($bop)   A method that does a thing
+     *                          It does it well
+     */
+
 EOF;
 
         $this->doTest($expected, $input);
@@ -140,6 +278,53 @@ EOF;
      * description foo
      */
 
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
+    public function testFixMultiLineDescWithThrowsLeftAlign()
+    {
+        $this->fixer->configure(['tags' => ['param', 'return', 'throws'], 'align' => 'left']);
+
+        $expected = <<<'EOF'
+<?php
+    /**
+     * @param EngineInterface $templating
+     * @param string $format
+     * @param int $code An HTTP response status code
+     *                  See constants
+     * @param bool $debug
+     * @param bool $debug See constants
+     *                    See constants
+     * @param mixed &$reference A parameter passed by reference
+     *
+     * @return Foo description foo
+     *
+     * @throws Foo description foo
+     *             description foo
+     */
+
+EOF;
+
+        $input = <<<'EOF'
+<?php
+    /**
+     * @param  EngineInterface $templating
+     * @param string      $format
+     * @param  int  $code       An HTTP response status code
+     *                              See constants
+     * @param    bool         $debug
+     * @param    bool         $debug See constants
+     * See constants
+     * @param  mixed    &$reference     A parameter passed by reference
+     *
+     * @return Foo description foo
+     *
+     * @throws Foo             description foo
+     * description foo
+     */
+
 EOF;
 
         $this->doTest($expected, $input);
@@ -304,6 +489,64 @@ EOF;
         $this->doTest($expected);
     }
 
+    public function testFixTestLeftAlign()
+    {
+        $this->fixer->configure(['tags' => ['param'], 'align' => 'left']);
+
+        $expected = <<<'EOF'
+<?php
+    /**
+     * @param int $a
+     * @param string $b
+     *
+     * @dataProvider     dataJobCreation
+     */
+
+EOF;
+
+        $input = <<<'EOF'
+<?php
+    /**
+     * @param     int       $a
+     * @param     string    $b
+     *
+     * @dataProvider     dataJobCreation
+     */
+
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
+    public function testFixTest()
+    {
+        $this->fixer->configure(['tags' => ['param']]);
+
+        $expected = <<<'EOF'
+<?php
+    /**
+     * @param int         $a
+     * @param string|null $b
+     *
+     * @dataProvider   dataJobCreation
+     */
+
+EOF;
+
+        $input = <<<'EOF'
+<?php
+    /**
+     * @param     int       $a
+     * @param     string|null    $b
+     *
+     * @dataProvider   dataJobCreation
+     */
+
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
     public function testFixWithVar()
     {
         $this->fixer->configure(['tags' => ['var']]);
@@ -487,6 +730,47 @@ EOF;
         $this->doTest($expected, $input);
     }
 
+    public function testDifferentIndentationLeftAlign()
+    {
+        $this->fixer->configure(['tags' => ['param', 'return'], 'align' => 'left']);
+
+        $expected = <<<'EOF'
+<?php
+/**
+ * @param int $limit
+ * @param string $more
+ *
+ * @return array
+ */
+
+        /**
+         * @param int $limit
+         * @param string $more
+         *
+         * @return array
+         */
+EOF;
+
+        $input = <<<'EOF'
+<?php
+/**
+ * @param   int       $limit
+ * @param   string       $more
+ *
+ * @return array
+ */
+
+        /**
+         * @param   int       $limit
+         * @param   string       $more
+         *
+         * @return array
+         */
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
     /**
      * @param array                  $config
      * @param string                 $expected
@@ -736,6 +1020,29 @@ EOF;
         $this->doTest($expected, $input);
     }
 
+    public function testAlignsMethodWithoutParametersLeftAlign()
+    {
+        $this->fixer->configure(['tags' => ['method', 'property'], 'align' => 'left']);
+
+        $expected = <<<'EOF'
+<?php
+    /**
+     * @property string $foo Desc
+     * @method int foo() Description
+     */
+EOF;
+
+        $input = <<<'EOF'
+<?php
+    /**
+     * @property    string   $foo     Desc
+     * @method int      foo()          Description
+     */
+EOF;
+
+        $this->doTest($expected, $input);
+    }
+
     public function testDoesNotFormatMethod()
     {
         $this->fixer->configure(['tags' => ['method']]);
@@ -892,6 +1199,35 @@ final class Sample
     {
     }
 }
+',
+            ],
+            [
+                ['tags' => ['param'], 'align' => 'left'],
+                '<?php
+final class Sample
+{
+    /**
+     * @param int $a
+     * @param int $b
+     * @param array[] ...$c
+     */
+    public function sample2($a, $b, ...$c)
+    {
+    }
+}
+',
+                '<?php
+final class Sample
+{
+    /**
+     * @param int       $a
+     * @param int    $b
+     * @param array[]      ...$c
+     */
+    public function sample2($a, $b, ...$c)
+    {
+    }
+}
 ',
             ],
         ];