diff --git a/PHPCompatibility/Docs/TextStrings/RemovedDollarBraceStringEmbedsStandard.xml b/PHPCompatibility/Docs/TextStrings/RemovedDollarBraceStringEmbedsStandard.xml new file mode 100644 index 000000000..d30323495 --- /dev/null +++ b/PHPCompatibility/Docs/TextStrings/RemovedDollarBraceStringEmbedsStandard.xml @@ -0,0 +1,47 @@ + + + + + + + + "hello $world"; + +echo "hello {$world['bar']}"; + +echo "hello {$world->bar}"; + +echo "hello {$world["${bar['baz']}"]}"; + +echo "hello {${world->{${'bar'}}}}"; + ]]> + + + "hello ${world}"; + +echo "hello ${world['bar']}"; + +echo "hello ${world->bar}"; + +echo "hello ${world["${bar['baz']}"]}"; + +echo "hello ${world->{${'bar'}}}"; + ]]> + + + diff --git a/PHPCompatibility/Sniffs/TextStrings/RemovedDollarBraceStringEmbedsSniff.php b/PHPCompatibility/Sniffs/TextStrings/RemovedDollarBraceStringEmbedsSniff.php new file mode 100644 index 000000000..468ebed19 --- /dev/null +++ b/PHPCompatibility/Sniffs/TextStrings/RemovedDollarBraceStringEmbedsSniff.php @@ -0,0 +1,134 @@ + 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[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); + } +} diff --git a/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.1.inc b/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.1.inc new file mode 100644 index 000000000..4ec71d187 --- /dev/null +++ b/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.1.inc @@ -0,0 +1,90 @@ +bar"; +$text = "some text $var some text"; + +$heredoc = <<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 <<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; diff --git a/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.2.inc b/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.2.inc new file mode 100644 index 000000000..f637fbf1e --- /dev/null +++ b/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.2.inc @@ -0,0 +1,42 @@ +getMethod()} some text + some text ${foo["${bar['baz']}"]} some text + some text ${foo->{${'a'}}} some text + EOD; diff --git a/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.php b/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.php new file mode 100644 index 000000000..25d0c9396 --- /dev/null +++ b/PHPCompatibility/Tests/TextStrings/RemovedDollarBraceStringEmbedsUnitTest.php @@ -0,0 +1,256 @@ +sniffFile(__DIR__ . '/' . self::TEST_FILE, '8.2'); + $this->assertWarning($file, $line, 'Using ${var} in strings is deprecated since PHP 8.2, use {$var} instead. Found: ' . $found); + } + + /** + * Data provider. + * + * @see testRemovedDollarBraceStringEmbedsType3() + * + * @return array + */ + public function dataRemovedDollarBraceStringEmbedsType3() + { + return [ + [57, '${foo}'], + [58, '${foo[\'bar\']}'], + [59, '${foo}'], + [59, '${text}'], + [62, '${foo}'], + [65, '${foo}'], + ]; + } + + + /** + * Test that variable embeds of "type 4" - Variable variables (“${expr}”, equivalent to + * (string) ${expr}) - are correctly detected. + * + * @dataProvider dataRemovedDollarBraceStringEmbedsType4 + * + * @param int $line The line number. + * @param string $found The embedded expression found. + * + * @return void + */ + public function testRemovedDollarBraceStringEmbedsType4($line, $found) + { + $file = $this->sniffFile(__DIR__ . '/' . self::TEST_FILE, '8.2'); + $this->assertWarning($file, $line, "Using {$found} (variable variables) in strings is deprecated since PHP 8.2, use {\${expr}} instead."); + } + + /** + * Data provider. + * + * @see testRemovedDollarBraceStringEmbedsType4() + * + * @return array + */ + public function dataRemovedDollarBraceStringEmbedsType4() + { + return [ + [68, '${$bar}'], + [69, '${(foo)}'], + [70, '${foo->bar}'], + [71, '${$object->getMethod()}'], + [72, '${(foo)}'], + [73, '${substr(\'laruence\', 0, 2)}'], + [75, '${foo["${bar}"]}'], + [76, '${foo["${bar[\'baz\']}"]}'], + [77, '${foo->{$baz}}'], + [78, '${foo->{${\'a\'}}}'], + [79, '${foo->{"${\'a\'}"}}'], + [83, '${foo["${bar}"]}'], + [84, '${foo["${bar[\'baz\']}"]}'], + [85, '${foo->{${\'a\'}}}'], + [89, '${(foo)}'], + ]; + } + + + /** + * Test that variable embeds of "type 3" - Braces after the dollar sign (“${foo}”) - + * are correctly detected in PHP 7.3+ indented heredocs. + * + * @dataProvider dataRemovedDollarBraceStringEmbedsType3InIndentedHeredoc + * + * @param int $line The line number. + * @param string $found The embedded variable found. + * + * @return void + */ + public function testRemovedDollarBraceStringEmbedsType3InIndentedHeredoc($line, $found) + { + if (\PHP_VERSION_ID < 70300) { + $this->markTestSkipped('Test code involving PHP 7.3 heredocs will not tokenize correctly on PHP < 7.3'); + } + + $file = $this->sniffFile(__DIR__ . '/' . self::TEST_FILE_PHP73HEREDOCS, '8.2'); + $this->assertWarning($file, $line, 'Using ${var} in strings is deprecated since PHP 8.2, use {$var} instead. Found: ' . $found); + } + + /** + * Data provider. + * + * @see testRemovedDollarBraceStringEmbedsType3InIndentedHeredoc() + * + * @return array + */ + public function dataRemovedDollarBraceStringEmbedsType3InIndentedHeredoc() + { + return [ + [33, '${foo[\'bar\']}'], + ]; + } + + + /** + * Test that variable embeds of "type 4" - Variable variables (“${expr}”, equivalent to + * (string) ${expr}) - are correctly detected in PHP 7.3+ indented heredocs. + * + * @dataProvider dataRemovedDollarBraceStringEmbedsType4InIndentedHeredoc + * + * @param int $line The line number. + * @param string $found The embedded expression found. + * + * @return void + */ + public function testRemovedDollarBraceStringEmbedsType4InIndentedHeredoc($line, $found) + { + if (\PHP_VERSION_ID < 70300) { + $this->markTestSkipped('Test code involving PHP 7.3 heredocs will not tokenize correctly on PHP < 7.3'); + } + + $file = $this->sniffFile(__DIR__ . '/' . self::TEST_FILE_PHP73HEREDOCS, '8.2'); + $this->assertWarning($file, $line, "Using {$found} (variable variables) in strings is deprecated since PHP 8.2, use {\${expr}} instead."); + } + + /** + * Data provider. + * + * @see testRemovedDollarBraceStringEmbedsType4InIndentedHeredoc() + * + * @return array + */ + public function dataRemovedDollarBraceStringEmbedsType4InIndentedHeredoc() + { + return [ + [39, '${$object->getMethod()}'], + [40, '${foo["${bar[\'baz\']}"]}'], + [41, '${foo->{${\'a\'}}}'], + ]; + } + + + /** + * Verify the sniff does not throw false positives for valid code. + * + * @dataProvider dataTestFiles + * + * @param string $testFile File name for the test case file to use. + * @param int $lines Number of lines at the top of the file for which we don't expect errors. + * + * @return void + */ + public function testNoFalsePositives($testFile, $lines) + { + if ($testFile === self::TEST_FILE_PHP73HEREDOCS && \PHP_VERSION_ID < 70300) { + $this->markTestSkipped('Test code involving PHP 7.3 heredocs will not tokenize correctly on PHP < 7.3'); + } + + $file = $this->sniffFile(__DIR__ . '/' . $testFile, '8.2'); + + // No errors expected on the first # lines. + for ($line = 1; $line <= $lines; $line++) { + $this->assertNoViolation($file, $line); + } + } + + + /** + * Verify no notices are thrown at all. + * + * @dataProvider dataTestFiles + * + * @param string $testFile File name for the test case file to use. + * + * @return void + */ + public function testNoViolationsInFileOnValidVersion($testFile) + { + if ($testFile === self::TEST_FILE_PHP73HEREDOCS && \PHP_VERSION_ID < 70300) { + $this->markTestSkipped('Test code involving PHP 7.3 heredocs will not tokenize correctly on PHP < 7.3'); + } + + $file = $this->sniffFile(__DIR__ . '/' . $testFile, '8.1'); + $this->assertNoViolation($file); + } + + + /** + * Data provider. + * + * @return array + */ + public function dataTestFiles() + { + return [ + [self::TEST_FILE, 51], + [self::TEST_FILE_PHP73HEREDOCS, 26], + ]; + } +}