Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ PHP 8.2 | New PHPCompatibility.TextStrings.RemovedDollarBraceStringEmbeds sniff #1424

Merged
merged 1 commit into from Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<documentation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://phpcsstandards.github.io/PHPCSDevTools/phpcsdocs.xsd"
title="Removed Dollar Brace String Embeds"
>
<standard>
<![CDATA[
Detect use of select forms of variable embedding in double quoted texts strings and heredocs which are deprecated since PHP 8.2.

Until PHP 8.2, PHP allowed embedding variables in strings with double-quotes (") and heredocs in four different ways:
1. Directly embedding variables (`$foo`)
2. Braces outside the variable (`{$foo}`)
3. Braces after the dollar sign (`${foo}`)
4. Variable variables (`${expr}`, equivalent to `(string) ${expr}`)

As of PHP 8.2, embedding variables and expressions using the syntaxes as per option 3 and 4 is deprecated.
]]>
</standard>
<code_comparison>
<code title="Cross-version compatible: Embedding simple variables and complex expressions in text strings using plain variables or the `{$...}` syntax.">
<![CDATA[
echo <em>"hello $world"</em>;

echo <em>"hello {$world['bar']}"</em>;

echo <em>"hello {$world->bar}"</em>;

echo <em>"hello {$world["${bar['baz']}"]}"</em>;

echo <em>"hello {${world->{${'bar'}}}}"</em>;
]]>
</code>
<code title="PHP &lt; 8.2: Embedding simple variables and complex expressions in text strings using the `${...}` syntax">
<![CDATA[
echo <em>"hello ${world}"</em>;

echo <em>"hello ${world['bar']}"</em>;

echo <em>"hello ${world->bar}"</em>;

echo <em>"hello ${world["${bar['baz']}"]}"</em>;

echo <em>"hello ${world->{${'bar'}}}"</em>;
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,134 @@
<?php
/**
* PHPCompatibility, an external standard for PHP_CodeSniffer.
*
* @package PHPCompatibility
* @copyright 2012-2022 PHPCompatibility Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCompatibility/PHPCompatibility
*/

namespace PHPCompatibility\Sniffs\TextStrings;

use PHPCompatibility\Sniff;
use PHP_CodeSniffer\Files\File;
use PHPCSUtils\Utils\GetTokensAsString;
use PHPCSUtils\Utils\TextStrings;

/**
* Detect use of select forms of variable embedding in heredocs and double strings as deprecated per PHP 8.2.
*
* > PHP allows embedding variables in strings with double-quotes (") and heredoc in various ways.
* > 1. Directly embedding variables (`$foo`)
* > 2. Braces outside the variable (`{$foo}`)
* > 3. Braces after the dollar sign (`${foo}`)
* > 4. Variable variables (`${expr}`, equivalent to `(string) ${expr}`)
* >
* > [...] to deprecate options 3 and 4 in PHP 8.2 and remove them in PHP 9.0.
*
* PHP version 8.2
* PHP version 9.0
*
* @link https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation
*
* @since 10.0.0
*/
class RemovedDollarBraceStringEmbedsSniff extends Sniff
{

/**
* Returns an array of tokens this test wants to listen for.
*
* @since 10.0.0
*
* @return array
*/
public function register()
{
return [
\T_DOUBLE_QUOTED_STRING,
\T_START_HEREDOC,
\T_DOLLAR_OPEN_CURLY_BRACES,
];
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @since 10.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
*
* @return void|int Void or a stack pointer to skip forward.
*/
public function process(File $phpcsFile, $stackPtr)
{
if ($this->supportsAbove('8.2') === false) {
return;
}

$tokens = $phpcsFile->getTokens();

/*
* Defensive coding, this code is not expected to ever actually be hit since PHPCS#3604
* (included in 3.7.0), but _will_ be hit if a file containing a PHP 7.3 indented heredoc/nowdocs
* is scanned with PHPCS on PHP < 7.3. People shouldn't do that, but hey, we can't stop them.
*/
if ($tokens[$stackPtr]['code'] === \T_DOLLAR_OPEN_CURLY_BRACES) {
// @codeCoverageIgnoreStart
if ($tokens[($stackPtr - 1)]['code'] === \T_DOUBLE_QUOTED_STRING) {
--$stackPtr;
} else {
// Throw an error anyway, though it won't be very informative.
$message = 'Using ${} in strings is deprecated since PHP 8.2, use {$var} or {${expr}} instead.';
$code = 'DeprecatedDollarBraceEmbed';
$phpcsFile->addWarning($message, $stackPtr, $code);
return;
}
// @codeCoverageIgnoreEnd
}

$endOfString = TextStrings::getEndOfCompleteTextString($phpcsFile, $stackPtr);
$startOfString = $stackPtr;
if ($tokens[$stackPtr]['code'] === \T_START_HEREDOC) {
$startOfString = ($stackPtr + 1);
}

$contents = GetTokensAsString::normal($phpcsFile, $startOfString, $endOfString);
if (\strpos($contents, '${') === false) {
// No interpolation found or only interpolations which are still supported.
return ($endOfString + 1);
}

$embeds = TextStrings::getEmbeds($contents);
foreach ($embeds as $offset => $embed) {
if (\strpos($embed, '${') !== 0) {
continue;
}

// Figure out the stack pointer to throw the warning on.
$errorPtr = $startOfString;
$length = 0;
while (($length + $tokens[$errorPtr]['length']) < $offset) {
$length += $tokens[$errorPtr]['length'];
++$errorPtr;
}

// Type 4.
$message = 'Using %s (variable variables) in strings is deprecated since PHP 8.2, use {${expr}} instead.';
$code = 'DeprecatedExpressionSyntax';
if (\preg_match('`^\$\{(?P<varname>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]+)(?:\[([\'"])?[^\$\{\}\]]+(?:\2)?\])?\}$`', $embed) === 1) {
// Type 3.
$message = 'Using ${var} in strings is deprecated since PHP 8.2, use {$var} instead. Found: %s';
$code = 'DeprecatedVariableSyntax';
}

$phpcsFile->addWarning($message, $errorPtr, $code, [$embed]);

}

return ($endOfString + 1);
}
}
@@ -0,0 +1,90 @@
<?php

/*
* Embedded variables which are supported cross-version.
*/

// Type 1: directly embedding variables.
echo "$foo";
echo "$$foo";
echo "$foo[bar]";
echo "$foo->bar";
$text = "some text $var some text";

$heredoc = <<<EOD
some text $var some text
EOD;

// Type 2: Braces around the variable/expression.
echo "{$foo}";
echo "{$$foo}";
echo "{$foo['bar']}";
echo "{$foo->bar}";
echo "{$foo->bar()}";
echo "{$foo['bar']->baz()()}";
echo "{${$bar}}";
echo "{$foo()}";
echo "{${$object->getMethod()}}"
$text = "some text {$var} some text";

$heredoc = <<<"EOD"
some text {$var} some text
EOD;

/*
* Not our target.
*/

// Ordinary variable variables outside strings.
$foo = ${'bar'};

// Heredoc without embeds.
echo <<<EOD
Some text
EOD;

// Not actually interpolated - $ is escaped. The second $foo is to force T_DOUBLE_QUOTED_STRING tokenization.
echo "\${foo} and $foo";
echo "\${foo[\"bar\"]} and $foo";
echo "$\{foo} and $foo";


/*
* PHP 8.2: deprecated forms of embedding variables.
*/

// Type 3: Braces after the dollar sign.
echo "${foo}";
echo "${foo['bar']}";
$text = "some text ${foo} some ${text}";

$heredoc = <<<EOD
some text ${foo} some text
EOD;

echo "\\${foo}"; // Not actually escaped, the backslash escapes the backslash, not the dollar sign.

// Type 4: Variable variables.
echo "${$bar}";
echo "${(foo)}";
echo "${foo->bar}";
echo "${$object->getMethod()}"
$text = "some text ${(foo)} some text";
echo "${substr('laruence', 0, 2)}";

echo "${foo["${bar}"]}";
echo "${foo["${bar['baz']}"]}";
echo "${foo->{$baz}}";
echo "${foo->{${'a'}}}";
echo "${foo->{"${'a'}"}}";

// Verify correct handling of stack pointers in multi-token code.
$text = "Line without embed
some text ${foo["${bar}"]} some text
some text ${foo["${bar['baz']}"]} some text
some text ${foo->{${'a'}}} some text
";

$heredoc = <<<"EOD"
some text ${(foo)} some text
EOD;
@@ -0,0 +1,42 @@
<?php

/**
* The tests involving PHP 7.3+ indented heredocs are in a separate test case file
* as any code after an indented heredoc will be tokenizer garbage on PHP < 7.3.
*/

// No embeds.
$php73IndentedHeredoc = <<<"EOD"
some text some text
EOD;

/*
* Embedded variables which are supported cross-version.
*/

// Type 1: directly embedding variables.
$php73IndentedHeredoc = <<<"EOD"
some text $foo[bar] some text
EOD;

// Type 2: Braces around the variable/expression.
$php73IndentedHeredoc = <<<EOD
some text {${$bar}} some text
EOD;

/*
* PHP 8.2: deprecated forms of embedding variables.
*/

// Type 3: Braces after the dollar sign.
$php73IndentedHeredoc = <<<"EOD"
some text ${foo['bar']} some text
EOD;

// Type 4: Variable variables.
$php73IndentedHeredoc = <<<EOD
Line without embed
some text ${$object->getMethod()} some text
some text ${foo["${bar['baz']}"]} some text
some text ${foo->{${'a'}}} some text
EOD;