EscapeImplicitBackslashesFixer.php 6.23 KB
Newer Older
冯超鹏's avatar
冯超鹏 committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
<?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\StringNotation;

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\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * @author Filippo Tessarotto <zoeslam@gmail.com>
 */
final class EscapeImplicitBackslashesFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
{
    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        $codeSample = <<<'EOF'
<?php

$singleQuoted = 'String with \" and My\Prefix\\';

$doubleQuoted = "Interpret my \n but not my \a";

$hereDoc = <<<HEREDOC
Interpret my \100 but not my \999
HEREDOC;

EOF;

        return new FixerDefinition(
            'Escape implicit backslashes in strings and heredocs to ease the understanding of which are special chars interpreted by PHP and which not.',
            [
                new CodeSample($codeSample),
                new CodeSample(
                    $codeSample,
                    ['single_quoted' => true]
                ),
                new CodeSample(
                    $codeSample,
                    ['double_quoted' => false]
                ),
                new CodeSample(
                    $codeSample,
                    ['heredoc_syntax' => false]
                ),
            ],
            'In PHP double-quoted strings and heredocs some chars like `n`, `$` or `u` have special meanings if preceded by a backslash '
            .'(and some are special only if followed by other special chars), while a backslash preceding other chars are interpreted like a plain '
            .'backslash. The precise list of those special chars is hard to remember and to identify quickly: this fixer escapes backslashes '
            ."that do not start a special interpretation with the char after them.\n"
            .'It is possible to fix also single-quoted strings: in this case there is no special chars apart from single-quote and backslash '
            .'itself, so the fixer simply ensure that all backslashes are escaped. Both single and double backslashes are allowed in single-quoted '
            .'strings, so the purpose in this context is mainly to have a uniformed way to have them written all over the codebase.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function isCandidate(Tokens $tokens)
    {
        return $tokens->isAnyTokenKindsFound([T_ENCAPSED_AND_WHITESPACE, T_CONSTANT_ENCAPSED_STRING]);
    }

    /**
     * {@inheritdoc}
     *
     * Must run before HeredocToNowdocFixer, SingleQuoteFixer.
     * Must run after BacktickToShellExecFixer.
     */
    public function getPriority()
    {
        return 1;
    }

    /**
     * {@inheritdoc}
     */
    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
    {
        static $singleQuotedRegex = '/(?<!\\\\)\\\\((?:\\\\\\\\)*)(?![\\\'\\\\])/';
        static $doubleQuotedRegex = '/(?<!\\\\)\\\\((?:\\\\\\\\)*)(?![efnrtv$"\\\\0-7]|x[0-9A-Fa-f]|u{)/';
        static $heredocSyntaxRegex = '/(?<!\\\\)\\\\((?:\\\\\\\\)*)(?![efnrtv$\\\\0-7]|x[0-9A-Fa-f]|u{)/';

        $doubleQuoteOpened = false;
        foreach ($tokens as $index => $token) {
            $content = $token->getContent();
            if ($token->equalsAny(['"', 'b"', 'B"'])) {
                $doubleQuoteOpened = !$doubleQuoteOpened;
            }
            if (!$token->isGivenKind([T_ENCAPSED_AND_WHITESPACE, T_CONSTANT_ENCAPSED_STRING]) || false === strpos($content, '\\')) {
                continue;
            }

            // Nowdoc syntax
            if ($token->isGivenKind(T_ENCAPSED_AND_WHITESPACE) && '\'' === substr(rtrim($tokens[$index - 1]->getContent()), -1)) {
                continue;
            }

            $firstTwoCharacters = strtolower(substr($content, 0, 2));
            $isSingleQuotedString = $token->isGivenKind(T_CONSTANT_ENCAPSED_STRING) && ('\'' === $content[0] || 'b\'' === $firstTwoCharacters);
            $isDoubleQuotedString =
                ($token->isGivenKind(T_CONSTANT_ENCAPSED_STRING) && ('"' === $content[0] || 'b"' === $firstTwoCharacters))
                || ($token->isGivenKind(T_ENCAPSED_AND_WHITESPACE) && $doubleQuoteOpened)
            ;
            $isHeredocSyntax = !$isSingleQuotedString && !$isDoubleQuotedString;
            if (
                (false === $this->configuration['single_quoted'] && $isSingleQuotedString)
                || (false === $this->configuration['double_quoted'] && $isDoubleQuotedString)
                || (false === $this->configuration['heredoc_syntax'] && $isHeredocSyntax)
            ) {
                continue;
            }

            $regex = $heredocSyntaxRegex;
            if ($isSingleQuotedString) {
                $regex = $singleQuotedRegex;
            } elseif ($isDoubleQuotedString) {
                $regex = $doubleQuotedRegex;
            }

            $newContent = Preg::replace($regex, '\\\\\\\\$1', $content);
            if ($newContent !== $content) {
                $tokens[$index] = new Token([$token->getId(), $newContent]);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function createConfigurationDefinition()
    {
        return new FixerConfigurationResolver([
            (new FixerOptionBuilder('single_quoted', 'Whether to fix single-quoted strings.'))
                ->setAllowedTypes(['bool'])
                ->setDefault(false)
                ->getOption(),
            (new FixerOptionBuilder('double_quoted', 'Whether to fix double-quoted strings.'))
                ->setAllowedTypes(['bool'])
                ->setDefault(true)
                ->getOption(),
            (new FixerOptionBuilder('heredoc_syntax', 'Whether to fix heredoc syntax.'))
                ->setAllowedTypes(['bool'])
                ->setDefault(true)
                ->getOption(),
        ]);
    }
}