Browse Source

feature #2415 Add IsNullFixer (kalessil, keradus)

This PR was squashed before being merged into the 2.1-dev branch (closes #2415).

Discussion
----------

Add IsNullFixer

- [x] Re-create the fixer
- [x] Fix bugs discovered in #1304 (assignments, ternaries)
- [x] Add option for yoda/no-yoda fixing style

Commits
-------

9acff046 Add IsNullFixer
Dariusz Ruminski 8 years ago
parent
commit
878b5b58eb

+ 4 - 0
README.rst

@@ -270,6 +270,10 @@ Choose from the list of available rules:
 * **indentation_type** [@PSR2, @Symfony]
    | Code MUST use configured indentation type.
 
+* **is_null**
+   | Replaces is_null(parameter) expression with ``null === parameter``.
+   | *Rule is: configurable, risky.*
+
 * **line_ending** [@PSR2, @Symfony]
    | All PHP files must use same line ending.
 

+ 200 - 0
src/Fixer/LanguageConstruct/IsNullFixer.php

@@ -0,0 +1,200 @@
+<?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\LanguageConstruct;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
+use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Vladimir Reznichenko <kalessil@gmail.com>
+ */
+final class IsNullFixer extends AbstractFixer implements ConfigurableFixerInterface
+{
+    private static $configurableOptions = array('use_yoda_style');
+    private static $defaultConfiguration = array('use_yoda_style' => true);
+
+    /**
+     * @var array<string, bool>
+     */
+    private $configuration;
+
+    /**
+     * 'use_yoda_style' can be configured with a boolean value.
+     *
+     * @param string[]|null $configuration
+     *
+     * @throws InvalidFixerConfigurationException
+     */
+    public function configure(array $configuration = null)
+    {
+        if (null === $configuration) {
+            $this->configuration = self::$defaultConfiguration;
+
+            return;
+        }
+
+        $this->configuration = array();
+        /** @var $option string */
+        foreach ($configuration as $option => $value) {
+            if (!in_array($option, self::$configurableOptions, true)) {
+                throw new InvalidFixerConfigurationException($this->getName(), sprintf('Unknown configuration item "%s", expected any of "%s".', $option, implode('", "', self::$configurableOptions)));
+            }
+
+            if (!is_bool($value)) {
+                throw new InvalidFixerConfigurationException($this->getName(), sprintf('Expected boolean got "%s".', is_object($value) ? get_class($value) : gettype($value)));
+            }
+
+            $this->configuration[$option] = $value;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isTokenKindFound(T_STRING);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function fix(\SplFileInfo $file, Tokens $tokens)
+    {
+        static $sequenceNeeded = array(array(T_STRING, 'is_null'), '(');
+
+        $currIndex = 0;
+        while (null !== $currIndex) {
+            $matches = $tokens->findSequence($sequenceNeeded, $currIndex, $tokens->count() - 1, false);
+
+            // stop looping if didn't find any new matches
+            if (null === $matches) {
+                break;
+            }
+
+            // 0 and 1 accordingly are "is_null", "(" tokens
+            $matches = array_keys($matches);
+
+            // move the cursor just after the sequence
+            list($isNullIndex, $currIndex) = $matches;
+
+            // skip all expressions which are not a function reference
+            $inversionCandidateIndex = $prevTokenIndex = $tokens->getPrevMeaningfulToken($matches[0]);
+            $prevToken = $tokens[$prevTokenIndex];
+            if ($prevToken->isGivenKind(array(T_DOUBLE_COLON, T_NEW, T_OBJECT_OPERATOR, T_FUNCTION))) {
+                continue;
+            }
+
+            // handle function references with namespaces
+            if ($prevToken->isGivenKind(T_NS_SEPARATOR)) {
+                $inversionCandidateIndex = $twicePrevTokenIndex = $tokens->getPrevMeaningfulToken($prevTokenIndex);
+                /** @var Token $twicePrevToken */
+                $twicePrevToken = $tokens[$twicePrevTokenIndex];
+                if ($twicePrevToken->isGivenKind(array(T_DOUBLE_COLON, T_NEW, T_OBJECT_OPERATOR, T_FUNCTION, T_STRING, CT::T_NAMESPACE_OPERATOR))) {
+                    continue;
+                }
+
+                // get rid of the root namespace when it used and check if the inversion operator provided
+                $tokens->removeTrailingWhitespace($prevTokenIndex);
+                $tokens[$prevTokenIndex]->clear();
+            }
+
+            // check if inversion being used, text comparison is due to not existing constant
+            $isInvertedNullCheck = false;
+            if ($tokens[$inversionCandidateIndex]->equals('!')) {
+                $isInvertedNullCheck = true;
+
+                // get rid of inverting for proper transformations
+                $tokens->removeTrailingWhitespace($inversionCandidateIndex);
+                $tokens[$inversionCandidateIndex]->clear();
+            }
+
+            /* before getting rind of `()` around a parameter, ensure it's not assignment/ternary invariant */
+            $referenceEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $matches[1]);
+            $isContainingDangerousConstructs = false;
+            for ($paramTokenIndex = $matches[1]; $paramTokenIndex <= $referenceEnd; ++$paramTokenIndex) {
+                if (in_array($tokens[$paramTokenIndex]->getContent(), array('?', '?:', '='), true)) {
+                    $isContainingDangerousConstructs = true;
+                    break;
+                }
+            }
+
+            if (!$isContainingDangerousConstructs) {
+                // closing parenthesis removed with leading spaces
+                $tokens->removeLeadingWhitespace($referenceEnd);
+                $tokens[$referenceEnd]->clear();
+
+                // opening parenthesis removed with trailing spaces
+                $tokens->removeLeadingWhitespace($matches[1]);
+                $tokens->removeTrailingWhitespace($matches[1]);
+                $tokens[$matches[1]]->clear();
+            }
+
+            // sequence which we'll use as a replacement
+            $replacement = array(
+                new Token(array(T_STRING, 'null')),
+                new Token(array(T_WHITESPACE, ' ')),
+                new Token($isInvertedNullCheck ? array(T_IS_NOT_IDENTICAL, '!==') : array(T_IS_IDENTICAL, '===')),
+                new Token(array(T_WHITESPACE, ' ')),
+            );
+
+            if (true === $this->configuration['use_yoda_style']) {
+                $tokens->overrideRange($isNullIndex, $isNullIndex, $replacement);
+            } else {
+                $replacement = array_reverse($replacement);
+                if ($isContainingDangerousConstructs) {
+                    array_unshift($replacement, new Token(array(')')));
+                }
+
+                $tokens[$isNullIndex]->clear();
+                $tokens->removeTrailingWhitespace($referenceEnd);
+                $tokens->overrideRange($referenceEnd, $referenceEnd, $replacement);
+            }
+
+            // nested is_null calls support
+            $currIndex = $isNullIndex;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'Replaces is_null(parameter) expression with `null === parameter`.',
+            array(
+                new CodeSample("<?php\n\$a = is_null(\$b);"),
+                new CodeSample("<?php\n\$a = is_null(\$b);", array('use_yoda_style' => false)),
+            ),
+            null,
+            'The following can be configured: `use_yoda_style => boolean`',
+            self::$defaultConfiguration,
+            'Risky when the function `is_null()` is overridden.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isRisky()
+    {
+        return true;
+    }
+}

+ 158 - 0
tests/Fixer/LanguageConstruct/IsNullFixerTest.php

@@ -0,0 +1,158 @@
+<?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\LanguageConstruct;
+
+use PhpCsFixer\Fixer\LanguageConstruct\IsNullFixer;
+use PhpCsFixer\Test\AbstractFixerTestCase;
+
+/**
+ * @author Vladimir Reznichenko <kalessil@gmail.com>
+ *
+ * @internal
+ */
+final class IsNullFixerTest extends AbstractFixerTestCase
+{
+    public function testConfigurationWrongOption()
+    {
+        $fixer = new IsNullFixer();
+
+        $this->setExpectedException(
+            'PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException',
+            'Unknown configuration item "yoda", expected any of "use_yoda_style".'
+        );
+        $fixer->configure(array('yoda' => true));
+    }
+
+    public function testConfigurationWrongValue()
+    {
+        $fixer = new IsNullFixer();
+
+        $this->setExpectedException(
+            'PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException',
+            'Expected boolean got "integer"'
+        );
+        $fixer->configure(array('use_yoda_style' => -1));
+    }
+
+    public function testCorrectConfiguration()
+    {
+        $fixer = new IsNullFixer();
+        $fixer->configure(array('use_yoda_style' => false));
+
+        $configuration = static::getObjectAttribute($fixer, 'configuration');
+        static::assertFalse($configuration['use_yoda_style']);
+    }
+
+    /**
+     * @dataProvider provideExamples
+     *
+     * @param string      $expected
+     * @param null|string $input
+     */
+    public function testYodaFix($expected, $input = null)
+    {
+        $this->fixer->configure(array('use_yoda_style' => true));
+        $this->doTest($expected, $input);
+    }
+
+    public function testNonYodaFix()
+    {
+        $this->fixer->configure(array('use_yoda_style' => false));
+
+        $this->doTest('<?php $x = $y === null;', '<?php $x = is_null($y);');
+        $this->doTest(
+            '<?php $b = a(a(a(b() === null) === null) === null) === null;',
+            '<?php $b = \is_null(a(\is_null(a(\is_null(a(\is_null(b())))))));'
+        );
+    }
+
+    public function provideExamples()
+    {
+        $multiLinePatternToFix = <<<'FIX'
+<?php $x =
+is_null
+
+(
+    json_decode
+    (
+        $x
+    )
+
+)
+
+;
+FIX;
+        $multiLinePatternFixed = <<<'FIXED'
+<?php $x =
+null === json_decode
+    (
+        $x
+    )
+
+;
+FIXED;
+
+        return array(
+            array('<?php $x = "is_null";'),
+
+            array('<?php $x = ClassA::is_null(json_decode($x));'),
+            array('<?php $x = ScopeA\\is_null(json_decode($x));'),
+            array('<?php $x = namespace\\is_null(json_decode($x));'),
+            array('<?php $x = $object->is_null(json_decode($x));'),
+
+            array('<?php $x = new \\is_null(json_decode($x));'),
+            array('<?php $x = new is_null(json_decode($x));'),
+            array('<?php $x = new ScopeB\\is_null(json_decode($x));'),
+
+            array('<?php is_nullSmth(json_decode($x));'),
+            array('<?php smth_is_null(json_decode($x));'),
+
+            array('<?php "SELECT ... is_null(json_decode($x)) ...";'),
+            array('<?php "SELECT ... is_null(json_decode($x)) ...";'),
+            array('<?php "test" . "is_null" . "in concatenation";'),
+
+            array('<?php $x = null === json_decode($x);', '<?php $x = is_null(json_decode($x));'),
+            array('<?php $x = null !== json_decode($x);', '<?php $x = !is_null(json_decode($x));'),
+            array('<?php $x = null !== json_decode($x);', '<?php $x = ! is_null(json_decode($x));'),
+            array('<?php $x = null !== json_decode($x);', '<?php $x = ! is_null( json_decode($x) );'),
+
+            array('<?php $x = null === json_decode($x);', '<?php $x = \\is_null(json_decode($x));'),
+            array('<?php $x = null !== json_decode($x);', '<?php $x = !\\is_null(json_decode($x));'),
+            array('<?php $x = null !== json_decode($x);', '<?php $x = ! \\is_null(json_decode($x));'),
+            array('<?php $x = null !== json_decode($x);', '<?php $x = ! \\is_null( json_decode($x) );'),
+
+            array('<?php $x = null === json_decode($x).".dist";', '<?php $x = is_null(json_decode($x)).".dist";'),
+            array('<?php $x = null !== json_decode($x).".dist";', '<?php $x = !is_null(json_decode($x)).".dist";'),
+            array('<?php $x = null === json_decode($x).".dist";', '<?php $x = \\is_null(json_decode($x)).".dist";'),
+            array('<?php $x = null !== json_decode($x).".dist";', '<?php $x = !\\is_null(json_decode($x)).".dist";'),
+
+            array($multiLinePatternFixed, $multiLinePatternToFix),
+            array(
+                '<?php $x = /**/null === /**/ /** x*//**//** */json_decode($x)/***//*xx*/;',
+                '<?php $x = /**/is_null/**/ /** x*/(/**//** */json_decode($x)/***/)/*xx*/;',
+            ),
+            array(
+                '<?php $x = null === (null === $x ? z(null === $y) : z(null === $z));',
+                '<?php $x = is_null(is_null($x) ? z(is_null($y)) : z(is_null($z)));',
+            ),
+            array(
+                '<?php $x = null === ($x = array());',
+                '<?php $x = is_null($x = array());',
+            ),
+            array(
+                '<?php null === a(null === a(null === a(null === b())));',
+                '<?php \is_null(a(\is_null(a(\is_null(a(\is_null(b())))))));',
+            ),
+        );
+    }
+}