diff --git a/src/Standards/Generic/Docs/Strings/UnnecessaryHeredocStandard.xml b/src/Standards/Generic/Docs/Strings/UnnecessaryHeredocStandard.xml new file mode 100644 index 0000000000..e0ca14f470 --- /dev/null +++ b/src/Standards/Generic/Docs/Strings/UnnecessaryHeredocStandard.xml @@ -0,0 +1,39 @@ + + + + + + + <<<'EOD' +some text +EOD; + ]]> + + + << +some text +EOD; + ]]> + + + + + <<<"EOD" +some $text +EOD; + ]]> + + + <<<"EOD" +some text +EOD; + ]]> + + + diff --git a/src/Standards/Generic/Sniffs/Strings/UnnecessaryHeredocSniff.php b/src/Standards/Generic/Sniffs/Strings/UnnecessaryHeredocSniff.php new file mode 100644 index 0000000000..bf923ebe56 --- /dev/null +++ b/src/Standards/Generic/Sniffs/Strings/UnnecessaryHeredocSniff.php @@ -0,0 +1,97 @@ + + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Strings; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +class UnnecessaryHeredocSniff implements Sniff +{ + + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + return [T_START_HEREDOC]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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 + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]['scope_closer']) === false) { + // Just to be safe. Shouldn't be possible as in that case, the opener shouldn't be tokenized + // to T_START_HEREDOC by PHP. + return; + } + + $closer = $tokens[$stackPtr]['scope_closer']; + $body = ''; + + // Collect all the tokens within the heredoc body. + for ($i = ($stackPtr + 1); $i < $closer; $i++) { + $body .= $tokens[$i]['content']; + } + + $tokenizedBody = token_get_all(sprintf("", $body)); + foreach ($tokenizedBody as $ptr => $bodyToken) { + if (is_array($bodyToken) === false) { + continue; + } + + if ($bodyToken[0] === T_DOLLAR_OPEN_CURLY_BRACES + || $bodyToken[0] === T_VARIABLE + ) { + // Contains interpolation or expression. + $phpcsFile->recordMetric($stackPtr, 'Heredoc contains interpolation or expression', 'yes'); + return; + } + + if ($bodyToken[0] === T_CURLY_OPEN + && is_array($tokenizedBody[($ptr + 1)]) === false + && $tokenizedBody[($ptr + 1)] === '$' + ) { + // Contains interpolation or expression. + $phpcsFile->recordMetric($stackPtr, 'Heredoc contains interpolation or expression', 'yes'); + return; + } + }//end foreach + + $phpcsFile->recordMetric($stackPtr, 'Heredoc contains interpolation or expression', 'no'); + + $warning = 'Detected heredoc without interpolation or expressions. Use nowdoc syntax instead'; + + $fix = $phpcsFile->addFixableWarning($warning, $stackPtr, 'Found'); + if ($fix === true) { + $identifier = trim(ltrim($tokens[$stackPtr]['content'], '<')); + $replaceWith = "'".trim($identifier, '"')."'"; + $replacement = str_replace($identifier, $replaceWith, $tokens[$stackPtr]['content']); + $phpcsFile->fixer->replaceToken($stackPtr, $replacement); + } + + }//end process() + + +}//end class diff --git a/src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.1.inc b/src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.1.inc new file mode 100644 index 0000000000..abaad16d63 --- /dev/null +++ b/src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.1.inc @@ -0,0 +1,108 @@ +bar} +END; + +$heredoc = <<< "END" +some ${beers::softdrink} +END; + +$heredoc = <<< END +{${$object->getName()}} text +END; + +$heredoc = <<<"END" +some {${getName()}} +END; + +$heredoc = <<baz()()} +END; + +$heredoc = <<values[3]->name} text +END; + +$heredoc = <<<"END" +some ${$bar} +END; + +$heredoc = <<bar} text +END; + +$heredoc = <<<"END" +${foo["${bar}"]} text +END; + +$heredoc = <<{${'a'}}} text +END; + +$heredoc = <<{$baz[1]}} +END; + +$heredoc = <<john's wife greeted $people->robert. +END; + +$heredoc = <<bar} +END; + +$heredoc = <<< "END" +some ${beers::softdrink} +END; + +$heredoc = <<< END +{${$object->getName()}} text +END; + +$heredoc = <<<"END" +some {${getName()}} +END; + +$heredoc = <<baz()()} +END; + +$heredoc = <<values[3]->name} text +END; + +$heredoc = <<<"END" +some ${$bar} +END; + +$heredoc = <<bar} text +END; + +$heredoc = <<<"END" +${foo["${bar}"]} text +END; + +$heredoc = <<{${'a'}}} text +END; + +$heredoc = <<{$baz[1]}} +END; + +$heredoc = <<john's wife greeted $people->robert. +END; + +$heredoc = <<bar} + END; + +$heredoc = <<< "END" + some ${beers::softdrink} + END; + +$heredoc = <<< END + {${$object->getName()}} text + END; + +$heredoc = <<<"END" + some {${getName()}} + END; + +$heredoc = <<baz()()} + END; + +$heredoc = <<values[3]->name} text + END; + +$heredoc = <<<"END" + some ${$bar} + END; + +$heredoc = <<bar} text + END; + +$heredoc = <<<"END" + ${foo["${bar}"]} text + END; + +$heredoc = <<{${'a'}}} text + END; + +$heredoc = <<{$baz[1]}} + END; + +$heredoc = <<john's wife greeted $people->robert. + END; + +$heredoc = <<bar} + END; + +$heredoc = <<< "END" + some ${beers::softdrink} + END; + +$heredoc = <<< END + {${$object->getName()}} text + END; + +$heredoc = <<<"END" + some {${getName()}} + END; + +$heredoc = <<baz()()} + END; + +$heredoc = <<values[3]->name} text + END; + +$heredoc = <<<"END" + some ${$bar} + END; + +$heredoc = <<bar} text + END; + +$heredoc = <<<"END" + ${foo["${bar}"]} text + END; + +$heredoc = <<{${'a'}}} text + END; + +$heredoc = <<{$baz[1]}} + END; + +$heredoc = <<john's wife greeted $people->robert. + END; + +$heredoc = << + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Tests\Strings; + +use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; + +/** + * Unit test class for the UnnecessaryHeredoc sniff. + * + * @covers \PHP_CodeSniffer\Standards\Generic\Sniffs\Strings\UnnecessaryHeredocSniff + */ +final class UnnecessaryHeredocUnitTest extends AbstractSniffUnitTest +{ + + + /** + * Returns the lines where errors should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of errors that should occur on that line. + * + * @return array + */ + public function getErrorList() + { + return []; + + }//end getErrorList() + + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @param string $testFile The name of the file being tested. + * + * @return array + */ + public function getWarningList($testFile='') + { + $warnings = [ + 100 => 1, + 104 => 1, + ]; + + switch ($testFile) { + case 'UnnecessaryHeredocUnitTest.1.inc': + return $warnings; + + case 'UnnecessaryHeredocUnitTest.2.inc': + if (PHP_VERSION_ID >= 70300) { + return $warnings; + } + + // PHP 7.2 or lower: PHP version which doesn't support flexible heredocs/nowdocs yet. + return []; + + default: + return []; + } + + }//end getWarningList() + + +}//end class