Browse Source

Add BacktickToShellExecFixer

Filippo Tessarotto 7 years ago
parent
commit
45e7c8525d

+ 4 - 0
README.rst

@@ -257,6 +257,10 @@ Choose from the list of available rules:
   - ``syntax`` (``'long'``, ``'short'``): whether to use the ``long`` or ``short`` array
     syntax; defaults to ``'long'``
 
+* **backtick_to_shell_exec**
+
+  Converts backtick operators to shell_exec calls.
+
 * **binary_operator_spaces** [@Symfony]
 
   Binary operators should be surrounded by space as configured.

+ 146 - 0
src/Fixer/Alias/BacktickToShellExecFixer.php

@@ -0,0 +1,146 @@
+<?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\Alias;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Filippo Tessarotto <zoeslam@gmail.com>
+ */
+final class BacktickToShellExecFixer extends AbstractFixer
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isTokenKindFound('`');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'Converts backtick operators to shell_exec calls.',
+            [
+                new CodeSample(
+<<<'EOT'
+<?php
+$plain = `ls -lah`;
+$withVar = `ls -lah $var1 ${var2} {$var3} {$var4[0]} {$var5->call()}`;
+
+EOT
+                ),
+            ],
+            'Convertion is done only when it is non risky, so when special chars like single-quotes, double-quotes and backticks are not used inside the command.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getPriority()
+    {
+        // Should run before escape_implicit_backslashes
+        return 2;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        $backtickStarted = false;
+        $backtickTokens = [];
+        for ($index = $tokens->count() - 1; $index > 0; --$index) {
+            $token = $tokens[$index];
+            if (!$token->equals('`')) {
+                if ($backtickStarted) {
+                    $backtickTokens[$index] = $token;
+                }
+
+                continue;
+            }
+
+            $backtickTokens[$index] = $token;
+            if ($backtickStarted) {
+                $this->fixBackticks($tokens, $backtickTokens);
+                $backtickTokens = [];
+            }
+            $backtickStarted = !$backtickStarted;
+        }
+    }
+
+    /**
+     * Override backtick code with corresponding double-quoted string.
+     *
+     * @param Tokens $tokens
+     * @param array  $backtickTokens
+     */
+    private function fixBackticks(Tokens $tokens, array $backtickTokens)
+    {
+        // Track indexes for final override
+        ksort($backtickTokens);
+        $openingBacktickIndex = key($backtickTokens);
+        end($backtickTokens);
+        $closingBacktickIndex = key($backtickTokens);
+
+        // Strip enclosing backticks
+        array_shift($backtickTokens);
+        array_pop($backtickTokens);
+
+        // Double-quoted strings are parsed differenly if they contain
+        // variables or not, so we need to build the new token array accordingly
+        $count = count($backtickTokens);
+
+        $newTokens = [
+            new Token([T_STRING, 'shell_exec']),
+            new Token('('),
+        ];
+        if (1 !== $count) {
+            $newTokens[] = new Token('"');
+        }
+        foreach ($backtickTokens as $token) {
+            if (!$token->isGivenKind(T_ENCAPSED_AND_WHITESPACE)) {
+                $newTokens[] = $token;
+
+                continue;
+            }
+            $content = $token->getContent();
+            // Escaping special chars depends on the context: too tricky
+            if (preg_match('/[`"\']/u', $content)) {
+                return;
+            }
+
+            $kind = T_ENCAPSED_AND_WHITESPACE;
+            if (1 === $count) {
+                $content = '"'.$content.'"';
+                $kind = T_CONSTANT_ENCAPSED_STRING;
+            }
+
+            $newTokens[] = new Token([$kind, $content]);
+        }
+        if (1 !== $count) {
+            $newTokens[] = new Token('"');
+        }
+        $newTokens[] = new Token(')');
+
+        $tokens->overrideRange($openingBacktickIndex, $closingBacktickIndex, $newTokens);
+    }
+}

+ 1 - 0
tests/AutoReview/FixerFactoryTest.php

@@ -59,6 +59,7 @@ final class FixerFactoryTest extends TestCase
         $cases = [
             [$fixers['array_syntax'], $fixers['binary_operator_spaces']], // tested also in: array_syntax,binary_operator_spaces.test
             [$fixers['array_syntax'], $fixers['ternary_operator_spaces']], // tested also in: array_syntax,ternary_operator_spaces.test
+            [$fixers['backtick_to_shell_exec'], $fixers['escape_implicit_backslashes']], // tested also in: backtick_to_shell_exec,escape_implicit_backslashes.test
             [$fixers['blank_line_after_opening_tag'], $fixers['no_blank_lines_before_namespace']], // tested also in: blank_line_after_opening_tag,no_blank_lines_before_namespace.test
             [$fixers['class_attributes_separation'], $fixers['braces']],
             [$fixers['class_attributes_separation'], $fixers['indentation_type']],

+ 74 - 0
tests/Fixer/Alias/BacktickToShellExecFixerTest.php

@@ -0,0 +1,74 @@
+<?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\Alias;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @author Filippo Tessarotto <zoeslam@gmail.com>
+ *
+ * @internal
+ *
+ * @covers \PhpCsFixer\Fixer\Alias\BacktickToShellExecFixer
+ */
+final class BacktickToShellExecFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @param string      $expected
+     * @param null|string $input
+     *
+     * @dataProvider provideFixCases
+     */
+    public function testFix($expected, $input = null)
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public function provideFixCases()
+    {
+        return [
+            'plain' => [
+                '<?php shell_exec("ls -lah");',
+                '<?php `ls -lah`;',
+            ],
+            'with variables' => [
+                '<?php shell_exec("$var1 ls ${var2} -lah {$var3} file1.txt {$var4[0]} file2.txt {$var5->call()}");',
+                '<?php `$var1 ls ${var2} -lah {$var3} file1.txt {$var4[0]} file2.txt {$var5->call()}`;',
+            ],
+            'with single quote' => [
+<<<'EOT'
+<?php
+`echo a\'b`;
+`echo 'ab'`;
+EOT
+,
+            ],
+            'with double quote' => [
+<<<'EOT'
+<?php
+`echo a\"b`;
+`echo 'a"b'`;
+EOT
+,
+            ],
+            'with backtick' => [
+<<<'EOT'
+<?php
+`echo 'a\`b'`;
+`echo a\\\`b`;
+EOT
+,
+            ],
+        ];
+    }
+}

+ 11 - 0
tests/Fixtures/Integration/priority/backtick_to_shell_exec,escape_implicit_backslashes.test

@@ -0,0 +1,11 @@
+--TEST--
+Integration of fixers: backtick_to_shell_exec,escape_implicit_backslashes.
+--RULESET--
+{"backtick_to_shell_exec": true, "escape_implicit_backslashes": true}
+--EXPECT--
+<?php
+$var = shell_exec("ls a\\b");
+
+--INPUT--
+<?php
+$var = `ls a\b`;