Kuba Werłos 5 лет назад
Родитель
Сommit
8418a3fd51

+ 12 - 0
README.rst

@@ -1367,6 +1367,18 @@ Choose from the list of available rules:
   There should not be space before or after object ``T_OBJECT_OPERATOR``
   There should not be space before or after object ``T_OBJECT_OPERATOR``
   ``->``.
   ``->``.
 
 
+* **operator_linebreak**
+
+  Operators - when multiline - must always be at the beginning or at the
+  end of the line.
+
+  Configuration options:
+
+  - ``only_booleans`` (``bool``): whether to limit operators to only boolean ones;
+    defaults to ``false``
+  - ``position`` (``'beginning'``, ``'end'``): whether to place operators at the
+    beginning or at the end of the line; defaults to ``'beginning'``
+
 * **ordered_class_elements** [@PhpCsFixer]
 * **ordered_class_elements** [@PhpCsFixer]
 
 
   Orders the elements of classes/interfaces/traits.
   Orders the elements of classes/interfaces/traits.

+ 314 - 0
src/Fixer/Operator/OperatorLinebreakFixer.php

@@ -0,0 +1,314 @@
+<?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\Operator;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\Preg;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\CaseAnalysis;
+use PhpCsFixer\Tokenizer\Analyzer\ReferenceAnalyzer;
+use PhpCsFixer\Tokenizer\Analyzer\SwitchAnalyzer;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Kuba Werłos <werlos@gmail.com>
+ */
+final class OperatorLinebreakFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
+{
+    /**
+     * @internal
+     */
+    const BOOLEAN_OPERATORS = [[T_BOOLEAN_AND], [T_BOOLEAN_OR], [T_LOGICAL_AND], [T_LOGICAL_OR], [T_LOGICAL_XOR]];
+
+    /**
+     * @internal
+     */
+    const NON_BOOLEAN_OPERATORS = ['%', '&', '*', '+', '-', '.', '/', ':', '<', '=', '>', '?', '^', '|', [T_AND_EQUAL], [T_CONCAT_EQUAL], [T_DIV_EQUAL], [T_DOUBLE_ARROW], [T_IS_EQUAL], [T_IS_GREATER_OR_EQUAL], [T_IS_IDENTICAL], [T_IS_NOT_EQUAL], [T_IS_NOT_IDENTICAL], [T_IS_SMALLER_OR_EQUAL], [T_MINUS_EQUAL], [T_MOD_EQUAL], [T_MUL_EQUAL], [T_OBJECT_OPERATOR], [T_OR_EQUAL], [T_PAAMAYIM_NEKUDOTAYIM], [T_PLUS_EQUAL], [T_POW], [T_POW_EQUAL], [T_SL], [T_SL_EQUAL], [T_SR], [T_SR_EQUAL], [T_XOR_EQUAL]];
+
+    /**
+     * @var string
+     */
+    private $position = 'beginning';
+
+    /**
+     * @var array<array<int|string>|string>
+     */
+    private $operators = [];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'Operators - when multiline - must always be at the beginning or at the end of the line.',
+            [
+                new CodeSample('<?php
+function foo() {
+    return $bar ||
+        $baz;
+}
+'),
+                new CodeSample(
+                    '<?php
+function foo() {
+    return $bar
+        || $baz;
+}
+',
+                    ['position' => 'end']
+                ),
+            ]
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getConfigurationDefinition()
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('only_booleans', 'whether to limit operators to only boolean ones'))
+                ->setAllowedTypes(['bool'])
+                ->setDefault(false)
+                ->getOption(),
+            (new FixerOptionBuilder('position', 'whether to place operators at the beginning or at the end of the line'))
+                ->setAllowedValues(['beginning', 'end'])
+                ->setDefault($this->position)
+                ->getOption(),
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function configure(array $configuration = null)
+    {
+        parent::configure($configuration);
+
+        $this->operators = self::BOOLEAN_OPERATORS;
+        if (!$this->configuration['only_booleans']) {
+            $this->operators = array_merge($this->operators, self::NON_BOOLEAN_OPERATORS);
+            if (\PHP_VERSION_ID >= 70000) {
+                $this->operators[] = [T_COALESCE];
+                $this->operators[] = [T_SPACESHIP];
+            }
+        }
+        $this->position = $this->configuration['position'];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        $referenceAnalyzer = new ReferenceAnalyzer();
+
+        $excludedIndices = $this->getExcludedIndices($tokens);
+
+        $index = $tokens->count();
+        while ($index > 1) {
+            --$index;
+
+            if (!$tokens[$index]->equalsAny($this->operators, false)) {
+                continue;
+            }
+
+            if ($referenceAnalyzer->isReference($tokens, $index)) {
+                continue;
+            }
+
+            if (\in_array($index, $excludedIndices, true)) {
+                continue;
+            }
+
+            $operatorIndices = [$index];
+            if ($tokens[$index]->equals(':')) {
+                /** @var int $prevIndex */
+                $prevIndex = $tokens->getPrevMeaningfulToken($index);
+                if ($tokens[$prevIndex]->equals('?')) {
+                    $operatorIndices = [$prevIndex, $index];
+                    $index = $prevIndex;
+                }
+            }
+
+            $this->fixOperatorLinebreak($tokens, $operatorIndices);
+        }
+    }
+
+    /**
+     * Currently only colons from "switch".
+     *
+     * @return int[]
+     */
+    private function getExcludedIndices(Tokens $tokens)
+    {
+        $indices = [];
+        for ($index = $tokens->count() - 1; $index > 0; --$index) {
+            if ($tokens[$index]->isGivenKind(T_SWITCH)) {
+                $indices = array_merge($indices, $this->getCasesColonsForSwitch($tokens, $index));
+            }
+        }
+
+        return $indices;
+    }
+
+    /**
+     * @param int $switchIndex
+     *
+     * @return int[]
+     */
+    private function getCasesColonsForSwitch(Tokens $tokens, $switchIndex)
+    {
+        return array_map(
+            static function (CaseAnalysis $caseAnalysis) {
+                return $caseAnalysis->getColonIndex();
+            },
+            (new SwitchAnalyzer())->getSwitchAnalysis($tokens, $switchIndex)->getCases()
+        );
+    }
+
+    /**
+     * @param int[] $operatorIndices
+     */
+    private function fixOperatorLinebreak(Tokens $tokens, array $operatorIndices)
+    {
+        /** @var int $prevIndex */
+        $prevIndex = $tokens->getPrevMeaningfulToken(min($operatorIndices));
+        $indexStart = $prevIndex + 1;
+
+        /** @var int $nextIndex */
+        $nextIndex = $tokens->getNextMeaningfulToken(max($operatorIndices));
+        $indexEnd = $nextIndex - 1;
+
+        if (!$this->isMultiline($tokens, $indexStart, $indexEnd)) {
+            return; // operator is not surrounded by multiline whitespaces, do not touch it
+        }
+
+        if ('beginning' === $this->position) {
+            if (!$this->isMultiline($tokens, max($operatorIndices), $indexEnd)) {
+                return; // operator already is placed correctly
+            }
+            $this->fixMoveToTheBeginning($tokens, $operatorIndices);
+
+            return;
+        }
+
+        if (!$this->isMultiline($tokens, $indexStart, min($operatorIndices))) {
+            return; // operator already is placed correctly
+        }
+        $this->fixMoveToTheEnd($tokens, $operatorIndices);
+    }
+
+    /**
+     * @param int[] $operatorIndices
+     */
+    private function fixMoveToTheBeginning(Tokens $tokens, array $operatorIndices)
+    {
+        /** @var int $prevIndex */
+        $prevIndex = $tokens->getNonEmptySibling(min($operatorIndices), -1);
+
+        /** @var int $nextIndex */
+        $nextIndex = $tokens->getNextMeaningfulToken(max($operatorIndices));
+
+        for ($i = $nextIndex - 1; $i > max($operatorIndices); --$i) {
+            if ($tokens[$i]->isWhitespace() && 1 === Preg::match('/\R/u', $tokens[$i]->getContent())) {
+                $isWhitespaceBefore = $tokens[$prevIndex]->isWhitespace();
+                $inserts = $this->getReplacementsAndClear($tokens, $operatorIndices, -1);
+                if ($isWhitespaceBefore) {
+                    $inserts[] = new Token([T_WHITESPACE, ' ']);
+                }
+                $tokens->insertAt($nextIndex, $inserts);
+
+                break;
+            }
+        }
+    }
+
+    /**
+     * @param int[] $operatorIndices
+     */
+    private function fixMoveToTheEnd(Tokens $tokens, array $operatorIndices)
+    {
+        /** @var int $prevIndex */
+        $prevIndex = $tokens->getPrevMeaningfulToken(min($operatorIndices));
+
+        /** @var int $nextIndex */
+        $nextIndex = $tokens->getNonEmptySibling(max($operatorIndices), 1);
+
+        for ($i = $prevIndex + 1; $i < max($operatorIndices); ++$i) {
+            if ($tokens[$i]->isWhitespace() && 1 === Preg::match('/\R/u', $tokens[$i]->getContent())) {
+                $isWhitespaceAfter = $tokens[$nextIndex]->isWhitespace();
+                $inserts = $this->getReplacementsAndClear($tokens, $operatorIndices, 1);
+                if ($isWhitespaceAfter) {
+                    array_unshift($inserts, new Token([T_WHITESPACE, ' ']));
+                }
+                $tokens->insertAt($prevIndex + 1, $inserts);
+
+                break;
+            }
+        }
+    }
+
+    /**
+     * @param int[] $indices
+     * @param int   $direction
+     *
+     * @return Token[]
+     */
+    private function getReplacementsAndClear(Tokens $tokens, array $indices, $direction)
+    {
+        return array_map(
+            static function ($index) use ($tokens, $direction) {
+                $clone = $tokens[$index];
+                if ($tokens[$index + $direction]->isWhitespace()) {
+                    $tokens->clearAt($index + $direction);
+                }
+                $tokens->clearAt($index);
+
+                return $clone;
+            },
+            $indices
+        );
+    }
+
+    /**
+     * @param int $indexStart
+     * @param int $indexEnd
+     *
+     * @return bool
+     */
+    private function isMultiline(Tokens $tokens, $indexStart, $indexEnd)
+    {
+        for ($index = $indexStart; $index <= $indexEnd; ++$index) {
+            if (false !== strpos($tokens[$index]->getContent(), "\n")) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}

+ 52 - 0
src/Tokenizer/Analyzer/ReferenceAnalyzer.php

@@ -0,0 +1,52 @@
+<?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\Tokenizer\Analyzer;
+
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Kuba Werłos <werlos@gmail.com>
+ *
+ * @internal
+ */
+final class ReferenceAnalyzer
+{
+    /**
+     * @param int $index
+     *
+     * @return bool
+     */
+    public function isReference(Tokens $tokens, $index)
+    {
+        if ($tokens[$index]->isGivenKind(CT::T_RETURN_REF)) {
+            return true;
+        }
+
+        if (!$tokens[$index]->equals('&')) {
+            return false;
+        }
+
+        /** @var int $index */
+        $index = $tokens->getPrevMeaningfulToken($index);
+        if ($tokens[$index]->equalsAny(['=', [T_AS], [T_CALLABLE], [T_DOUBLE_ARROW], [CT::T_ARRAY_TYPEHINT]])) {
+            return true;
+        }
+
+        if ($tokens[$index]->isGivenKind(T_STRING)) {
+            $index = $tokens->getPrevMeaningfulToken($index);
+        }
+
+        return $tokens[$index]->equalsAny(['(', ',', [T_NS_SEPARATOR], [CT::T_NULLABLE_TYPE]]);
+    }
+}

+ 517 - 0
tests/Fixer/Operator/OperatorLinebreakFixerTest.php

@@ -0,0 +1,517 @@
+<?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\Operator;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @author  Kuba Werłos <werlos@gmail.com>
+ *
+ * @covers  \PhpCsFixer\Fixer\Operator\OperatorLinebreakFixer
+ *
+ * @internal
+ */
+final class OperatorLinebreakFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @dataProvider provideFixCases
+     *
+     * @param string      $expected
+     * @param null|string $input
+     */
+    public function testFix($expected, $input = null, array $configuration = null)
+    {
+        if (null !== $configuration) {
+            $this->fixer->configure($configuration);
+        }
+
+        $this->doTest($expected, $input);
+    }
+
+    public static function provideFixCases()
+    {
+        foreach (static::pairs() as $key => $value) {
+            yield sprintf('%s when position is "beginning"', $key) => $value;
+
+            yield sprintf('%s when position is "end"', $key) => [
+                $value[1],
+                $value[0],
+                ['position' => 'end'],
+            ];
+        }
+
+        yield 'ignore add operator when only booleans enabled' => [
+            '<?php
+return $foo
+    +
+    $bar;
+',
+            null,
+            ['only_booleans' => true],
+        ];
+
+        yield 'handle operator when on separate line when position is "beginning"' => [
+            '<?php
+return $foo
+    || $bar;
+',
+            '<?php
+return $foo
+    ||
+    $bar;
+',
+        ];
+
+        yield 'handle operator when on separate line when position is "end"' => [
+            '<?php
+return $foo ||
+    $bar;
+',
+            '<?php
+return $foo
+    ||
+    $bar;
+',
+            ['position' => 'end'],
+        ];
+
+        yield 'handle Elvis operator with space inside' => [
+            '<?php
+return $foo
+    ?: $bar;
+',
+            '<?php
+return $foo ? :
+    $bar;
+',
+        ];
+
+        yield 'handle Elvis operator with space inside when position is "end"' => [
+            '<?php
+return $foo ?:
+    $bar;
+',
+            '<?php
+return $foo
+    ? : $bar;
+',
+            ['position' => 'end'],
+        ];
+
+        yield 'handle Elvis operator with comment inside' => [
+            '<?php
+return $foo/* Lorem ipsum */
+    ?: $bar;
+',
+            '<?php
+return $foo ?/* Lorem ipsum */:
+    $bar;
+',
+        ];
+
+        yield 'handle Elvis operators with comment inside when position is "end"' => [
+            '<?php
+return $foo ?:
+    /* Lorem ipsum */$bar;
+',
+            '<?php
+return $foo
+    ?/* Lorem ipsum */: $bar;
+',
+            ['position' => 'end'],
+        ];
+
+        yield 'assign by reference' => [
+            '<?php
+                $a
+                    = $b;
+                $c =&
+                     $d;
+            ',
+            '<?php
+                $a =
+                    $b;
+                $c =&
+                     $d;
+            ',
+        ];
+
+        yield 'passing by reference' => [
+            '<?php
+                function foo(
+                    &$a,
+                    &$b,
+                    int
+                        &$c,
+                    \Bar\Baz
+                        &$d
+                ) {};',
+            null,
+            ['position' => 'end'],
+        ];
+
+        yield 'multiple switches' => [
+            '<?php
+                switch ($foo) {
+                   case 1:
+                      break;
+                   case 2:
+                      break;
+                }
+                switch($bar) {
+                   case 1:
+                      break;
+                   case 2:
+                      break;
+                }',
+        ];
+
+        if (\PHP_VERSION_ID >= 70000) {
+            yield 'return type' => [
+                '<?php
+                function foo()
+                :
+                bool
+                {};',
+            ];
+        }
+    }
+
+    /**
+     * @dataProvider provideFix71Cases
+     *
+     * @param string      $expected
+     * @param null|string $input
+     *
+     * @requires     PHP 7.1
+     */
+    public function testFix71($expected, $input = null, array $configuration = null)
+    {
+        if (null !== $configuration) {
+            $this->fixer->configure($configuration);
+        }
+
+        $this->doTest($expected, $input);
+    }
+
+    public static function provideFix71Cases()
+    {
+        yield 'nullable type when position is "end"' => [
+            '<?php
+                function foo(
+                    ?int $x,
+                    ?int $y,
+                    ?int $z
+                ) {};',
+            null,
+            ['position' => 'end'],
+        ];
+    }
+
+    private static function pairs()
+    {
+        yield 'handle equal sign' => [
+            '<?php
+$foo
+    = $bar;
+',
+            '<?php
+$foo =
+    $bar;
+',
+        ];
+
+        yield 'handle add operator' => [
+            '<?php
+return $foo
+    + $bar;
+',
+            '<?php
+return $foo +
+    $bar;
+',
+            ['only_booleans' => false],
+        ];
+
+        yield 'handle uppercase operator' => [
+            '<?php
+return $foo
+    AND $bar;
+',
+            '<?php
+return $foo AND
+    $bar;
+',
+        ];
+
+        yield 'handle concatenation operator' => [
+            '<?php
+return $foo
+    .$bar;
+',
+            '<?php
+return $foo.
+    $bar;
+',
+        ];
+
+        yield 'handle ternary operator' => [
+            '<?php
+return $foo
+    ? $bar
+    : $baz;
+',
+            '<?php
+return $foo ?
+    $bar :
+    $baz;
+',
+        ];
+
+        yield 'handle multiple operators' => [
+            '<?php
+return $foo
+    || $bar
+    || $baz;
+',
+            '<?php
+return $foo ||
+    $bar ||
+    $baz;
+',
+        ];
+
+        yield 'handle operator when no whitespace is before' => [
+            '<?php
+function foo() {
+    return $a
+        ||$b;
+}
+',
+            '<?php
+function foo() {
+    return $a||
+        $b;
+}
+',
+        ];
+
+        yield 'handle operator with one-line comments' => [
+            '<?php
+function getNewCuyamaTotal() {
+    return 562 // Population
+        + 2150 // Ft. above sea level
+        + 1951; // Established
+}
+',
+            '<?php
+function getNewCuyamaTotal() {
+    return 562 + // Population
+        2150 + // Ft. above sea level
+        1951; // Established
+}
+',
+        ];
+
+        yield 'handle operator with PHPDoc comments' => [
+            '<?php
+function getNewCuyamaTotal() {
+    return 562 /** Population */
+        + 2150 /** Ft. above sea level */
+        + 1951; /** Established */
+}
+',
+            '<?php
+function getNewCuyamaTotal() {
+    return 562 + /** Population */
+        2150 + /** Ft. above sea level */
+        1951; /** Established */
+}
+',
+        ];
+
+        yield 'handle operator with multiple comments next to each other' => [
+            '<?php
+function foo() {
+    return isThisTheRealLife() // First comment
+        // Second comment
+        // Third comment
+        || isThisJustFantasy();
+}
+',
+            '<?php
+function foo() {
+    return isThisTheRealLife() || // First comment
+        // Second comment
+        // Third comment
+        isThisJustFantasy();
+}
+',
+        ];
+
+        yield 'handle nested operators' => [
+            '<?php
+function foo() {
+    return $a
+        && (
+            $b
+            || $c
+        )
+        && $d;
+}
+',
+            '<?php
+function foo() {
+    return $a &&
+        (
+            $b ||
+            $c
+        ) &&
+        $d;
+}
+',
+        ];
+
+        yield 'handle Elvis operator' => [
+            '<?php
+return $foo
+    ?: $bar;
+',
+            '<?php
+return $foo ?:
+    $bar;
+',
+        ];
+
+        yield 'handle ternary operator inside of switch' => [
+            '<?php
+switch ($foo) {
+    case 1:
+        return $isOK ? 1 : -1;
+    case (
+            $a
+            ? 2
+            : 3
+        ) :
+        return 23;
+    case $b[
+            $a
+            ? 4
+            : 5
+        ]
+        : return 45;
+}
+',
+            '<?php
+switch ($foo) {
+    case 1:
+        return $isOK ? 1 : -1;
+    case (
+            $a ?
+            2 :
+            3
+        ) :
+        return 23;
+    case $b[
+            $a ?
+            4 :
+            5
+        ]
+        : return 45;
+}
+',
+        ];
+
+        yield 'handle ternary operator with switch inside' => [
+            '<?php
+                $a
+                    ? array_map(
+                        function () {
+                            switch (true) {
+                                case 1:
+                                    return true;
+                            }
+                        },
+                        [1, 2, 3]
+                    )
+                    : false;
+            ',
+            '<?php
+                $a ?
+                    array_map(
+                        function () {
+                            switch (true) {
+                                case 1:
+                                    return true;
+                            }
+                        },
+                        [1, 2, 3]
+                    ) :
+                    false;
+            ',
+        ];
+
+        $operator = [
+            '+', '-', '*', '/', '%', '**', // Arithmetic
+            '+=', '-=', '*=', '/=', '%=', '**=', // Arithmetic assignment
+            '=', // Assignment
+            '&', '|', '^', '<<', '>>', // Bitwise
+            '&=', '|=', '^=', '<<=', '>>=', // Bitwise assignment
+            '==', '===', '!=', '<>', '!==', '<', '>', '<=', '>=',  // Comparison
+            'and', 'or', 'xor', '&&', '||', // Logical
+            '.', '.=', // String
+            '->', // Object
+            '::', // Scope Resolution
+        ];
+
+        if (\PHP_VERSION_ID >= 70000) {
+            $operator[] = '??';
+            $operator[] = '<=>';
+        }
+
+        foreach ($operator as $operator) {
+            yield sprintf('handle %s operator', $operator) => [
+                sprintf('<?php
+                    $foo
+                        %s $bar;
+                ', $operator),
+                sprintf('<?php
+                    $foo %s
+                        $bar;
+                ', $operator),
+            ];
+        }
+
+        yield 'handle => operator' => [
+            '<?php
+[$foo
+    => $bar];
+',
+            '<?php
+[$foo =>
+    $bar];
+',
+        ];
+
+        yield 'handle & operator with constant' => [
+            '<?php
+\Foo::bar
+    & $baz;
+',
+            '<?php
+\Foo::bar &
+    $baz;
+',
+        ];
+    }
+}

+ 135 - 0
tests/Tokenizer/Analyzer/ReferenceAnalyzerTest.php

@@ -0,0 +1,135 @@
+<?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\Tokenizer\Analyzer;
+
+use PhpCsFixer\Tests\TestCase;
+use PhpCsFixer\Tokenizer\Analyzer\ReferenceAnalyzer;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Kuba Werłos <werlos@gmail.com>
+ *
+ * @covers  \PhpCsFixer\Tokenizer\Analyzer\ReferenceAnalyzer
+ *
+ * @internal
+ */
+final class ReferenceAnalyzerTest extends TestCase
+{
+    public function testNonAmpersand()
+    {
+        $analyzer = new ReferenceAnalyzer();
+
+        static::assertFalse($analyzer->isReference(Tokens::fromCode('<?php $foo;$bar;$baz;'), 3));
+    }
+
+    public function testReferenceAndNonReferenceTogether()
+    {
+        $analyzer = new ReferenceAnalyzer();
+
+        $tokens = Tokens::fromCode('<?php function foo(&$bar = BAZ & QUX) {};');
+
+        static::assertTrue($analyzer->isReference($tokens, 5));
+        static::assertFalse($analyzer->isReference($tokens, 12));
+    }
+
+    /**
+     * @dataProvider provideReferenceCases
+     *
+     * @param mixed $code
+     */
+    public function testReference($code)
+    {
+        $this->doTestCode(true, $code);
+    }
+
+    public static function provideReferenceCases()
+    {
+        yield ['<?php $foo =& $bar;'];
+        yield ['<?php $foo =& find_var($bar);'];
+        yield ['<?php $foo["bar"] =& $baz;'];
+        yield ['<?php function foo(&$bar) {};'];
+        yield ['<?php function foo($bar, &$baz) {};'];
+        yield ['<?php function &() {};'];
+        yield ['<?php
+class Foo {
+    public $value = 42;
+    public function &getValue() {
+        return $this->value;
+    }
+}'];
+        yield ['<?php function foo(\Bar\Baz &$qux) {};'];
+        yield ['<?php function foo(array &$bar) {};'];
+        yield ['<?php function foo(callable &$bar) {};'];
+        yield ['<?php function foo(int &$bar) {};'];
+        yield ['<?php function foo(string &$bar) {};'];
+        yield ['<?php foreach($foos as &$foo) {}'];
+        yield ['<?php foreach($foos as $key => &$foo) {}'];
+
+        if (PHP_VERSION >= 70100) {
+            yield ['<?php function foo(?int &$bar) {};'];
+        }
+    }
+
+    /**
+     * @dataProvider provideNonReferenceCases
+     *
+     * @param mixed $code
+     */
+    public function testNonReference($code)
+    {
+        $this->doTestCode(false, $code);
+    }
+
+    public static function provideNonReferenceCases()
+    {
+        yield ['<?php $foo & $bar;'];
+        yield ['<?php FOO & $bar;'];
+        yield ['<?php Foo::BAR & $baz;'];
+        yield ['<?php foo(1, 2) & $bar;'];
+        yield ['<?php foo($bar & $baz);'];
+        yield ['<?php foo($bar, $baz & $qux);'];
+        yield ['<?php foo($bar->baz & $qux);'];
+        yield ['<?php foo(Bar::BAZ & $qux);'];
+        yield ['<?php foo(Bar\Baz::qux & $quux);'];
+        yield ['<?php foo(\Bar\Baz::qux & $quux);'];
+        yield ['<?php foo($bar["mode"] & $baz);'];
+        yield ['<?php foo($bar{"mode"} & $baz);'];
+        yield ['<?php foo(0b11111111 & $bar);'];
+        yield ['<?php foo(127 & $bar);'];
+        yield ['<?php foo("bar" & $baz);'];
+        yield ['<?php foo($bar = BAZ & $qux);'];
+        yield ['<?php function foo($bar = BAZ & QUX) {};'];
+        yield ['<?php function foo($bar = BAZ::QUX & QUUX) {};'];
+        yield ['<?php function foo(array $bar = BAZ & QUX) {};'];
+        yield ['<?php function foo(callable $bar = BAZ & QUX) {};'];
+        yield ['<?php foreach($foos as $foo) { $foo & $bar; }'];
+        yield ['<?php if ($foo instanceof Bar & 0b01010101) {}'];
+
+        if (PHP_VERSION >= 70100) {
+            yield ['<?php function foo(?int $bar = BAZ & QUX) {};'];
+        }
+    }
+
+    private function doTestCode($expected, $code)
+    {
+        $analyzer = new ReferenceAnalyzer();
+
+        $tokens = Tokens::fromCode($code);
+
+        foreach ($tokens as $index => $token) {
+            if ('&' === $token->getContent()) {
+                static::assertSame($expected, $analyzer->isReference($tokens, $index));
+            }
+        }
+    }
+}