diff --git a/PHPCSUtils/Utils/TextStrings.php b/PHPCSUtils/Utils/TextStrings.php index 384b5985..9cb06870 100644 --- a/PHPCSUtils/Utils/TextStrings.php +++ b/PHPCSUtils/Utils/TextStrings.php @@ -33,6 +33,10 @@ class TextStrings * where the content matching might result in false positives/false negatives if the text * were to be examined line by line. * + * Additionally, this method correctly handles a particular type of double quoted string + * with an embedded expression which is incorrectly tokenized in PHPCS itself prior to + * PHPCS version 3.x.x. + * * @since 1.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. @@ -100,6 +104,48 @@ public static function getCompleteTextString(File $phpcsFile, $stackPtr, $stripQ ++$current; } while (isset($tokens[$current]) && $tokens[$current]['code'] === $targetType); + if ($targetType === \T_DOUBLE_QUOTED_STRING) { + /* + * BC for PHPCS < ??. + * Prior to PHPCS 3.x.x, when a select group of embedded variables/expressions was encountered + * in a double quoted string, the embed would not be tokenized as part of the T_DOUBLE_QUOTED_STRING, + * but would still have the PHP native tokenization. + */ + if (isset($tokens[$current]) && $tokens[$current]['code'] === \T_DOLLAR_OPEN_CURLY_BRACES) { + $embeddedContent = $tokens[$current]['content']; + $nestedVars = [$current]; + $foundEnd = false; + + for ($current = ($current + 1); $current < $phpcsFile->numTokens; $current++) { + if ($tokens[$current]['code'] === \T_DOUBLE_QUOTED_STRING + && empty($nestedVars) === true + ) { + $embeddedContent .= self::getCompleteTextString($phpcsFile, $current, false); + $foundEnd = true; + break; + } + + $embeddedContent .= $tokens[$current]['content']; + + if (\strpos($tokens[$current]['content'], '{') !== false) { + $nestedVars[] = $current; + } + + if (\strpos($tokens[$current]['content'], '}') !== false) { + \array_pop($nestedVars); + } + } + + /* + * Only accept this as one of the broken tokenizations if this is not a parse error + * or if we reached the end of the file. + */ + if ($foundEnd === true || $current === $phpcsFile->numTokens) { + $string .= $embeddedContent; + } + } + } + if ($stripNewline === true) { // Heredoc/nowdoc: strip the new line at the end of the string to emulate how PHP sees the string. $string = \rtrim($string, "\r\n"); diff --git a/Tests/Utils/TextStrings/GetCompleteTextString3604Test.inc b/Tests/Utils/TextStrings/GetCompleteTextString3604Test.inc new file mode 100644 index 00000000..460fbdd1 --- /dev/null +++ b/Tests/Utils/TextStrings/GetCompleteTextString3604Test.inc @@ -0,0 +1,53 @@ +bar"; +/* testProperty2 */ +"{$foo->bar}"; + +/* testMethod1 */ +"{$foo->bar()}"; + +/* testClosure1 */ +"{$foo()}"; + +/* testChain1 */ +"{$foo['bar']->baz()()}"; + +/* testVariableVar1 */ +"${$bar}"; +/* testVariableVar2 */ +"${(foo)}"; +/* testVariableVar3 */ +"${foo->bar}"; + +/* testNested1 */ +"${foo["${bar}"]}"; +/* testNested2 */ +"${foo["${bar['baz']}"]}"; +/* testNested3 */ +"${foo->{$baz}}"; +/* testNested4 */ +"${foo->{${'a'}}}"; +/* testNested5 */ +"${foo->{"${'a'}"}}"; + +/* testParseError */ +"${foo["${bar diff --git a/Tests/Utils/TextStrings/GetCompleteTextString3604Test.php b/Tests/Utils/TextStrings/GetCompleteTextString3604Test.php new file mode 100644 index 00000000..62f70a72 --- /dev/null +++ b/Tests/Utils/TextStrings/GetCompleteTextString3604Test.php @@ -0,0 +1,140 @@ +getTargetToken($testMarker, \T_DOUBLE_QUOTED_STRING); + + $result = TextStrings::getCompleteTextString(self::$phpcsFile, $stackPtr); + $this->assertSame($expectedContent, $result); + } + + /** + * Data provider. + * + * @see testGetCompleteTextString() For the array format. + * + * @return array + */ + public function dataGetCompleteTextString() + { + return [ + 'Simple embedded variable 1' => [ + 'testMarker' => '/* testSimple1 */', + 'expectedContent' => '$foo', + ], + 'Simple embedded variable 2' => [ + 'testMarker' => '/* testSimple2 */', + 'expectedContent' => '{$foo}', + ], + 'Simple embedded variable 3' => [ + 'testMarker' => '/* testSimple3 */', + 'expectedContent' => '${foo}', + ], + 'Embedded array access 1' => [ + 'testMarker' => '/* testDIM1 */', + 'expectedContent' => '$foo[bar]', + ], + 'Embedded array access 2' => [ + 'testMarker' => '/* testDIM2 */', + 'expectedContent' => '{$foo[\'bar\']}', + ], + 'Embedded array access 3' => [ + 'testMarker' => '/* testDIM3 */', + 'expectedContent' => '${foo[\'bar\']}', + ], + 'Embedded property access 1' => [ + 'testMarker' => '/* testProperty1 */', + 'expectedContent' => '$foo->bar', + ], + 'Embedded property access 2' => [ + 'testMarker' => '/* testProperty2 */', + 'expectedContent' => '{$foo->bar}', + ], + 'Embedded method call 1' => [ + 'testMarker' => '/* testMethod1 */', + 'expectedContent' => '{$foo->bar()}', + ], + 'Embedded closure call 1' => [ + 'testMarker' => '/* testClosure1 */', + 'expectedContent' => '{$foo()}', + ], + 'Embedded chained array access -> method call -> call' => [ + 'testMarker' => '/* testChain1 */', + 'expectedContent' => '{$foo[\'bar\']->baz()()}', + ], + 'Embedded variable variable 1' => [ + 'testMarker' => '/* testVariableVar1 */', + 'expectedContent' => '${$bar}', + ], + 'Embedded variable variable 1' => [ + 'testMarker' => '/* testVariableVar2 */', + 'expectedContent' => '${(foo)}', + ], + 'Embedded variable variable 2' => [ + 'testMarker' => '/* testVariableVar3 */', + 'expectedContent' => '${foo->bar}', + ], + 'Embedded nested variable variable 1' => [ + 'testMarker' => '/* testNested1 */', + 'expectedContent' => '${foo["${bar}"]}', + ], + 'Embedded nested variable variable 2' => [ + 'testMarker' => '/* testNested2 */', + 'expectedContent' => '${foo["${bar[\'baz\']}"]}', + ], + 'Embedded nested variable variable 3' => [ + 'testMarker' => '/* testNested3 */', + 'expectedContent' => '${foo->{$baz}}', + ], + 'Embedded nested variable variable 4' => [ + 'testMarker' => '/* testNested4 */', + 'expectedContent' => '${foo->{${\'a\'}}}', + ], + 'Embedded nested variable variable 5' => [ + 'testMarker' => '/* testNested5 */', + 'expectedContent' => '${foo->{"${\'a\'}"}}', + ], + 'Parse error at end of file' => [ + 'testMarker' => '/* testParseError */', + 'expectedContent' => '"${foo["${bar +', + ], + ]; + } +}