From 6995755d9903d09a22b499bcf7cedd89263331f4 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Oct 2024 03:32:26 +0200 Subject: [PATCH] :sparkles: New `Generic.Strings.UnnecessaryHeredoc` sniff New `Generic.Strings.UnnecessaryHeredoc` sniff which encourages the use of nowdocs instead of heredocs, when there is no interpolation or expressions in the body text. This sniff will hopefully help with the PERCS work as it intends to cover the following PER rule: > A nowdoc SHOULD be used wherever possible. Heredoc MAY be used when a nowdoc does not satisfy requirements. Includes fixer. Includes tests. Includes XML docs. --- .../Strings/UnnecessaryHeredocStandard.xml | 39 +++++++ .../Strings/UnnecessaryHeredocSniff.php | 93 +++++++++++++++ .../Strings/UnnecessaryHeredocUnitTest.1.inc | 108 ++++++++++++++++++ .../UnnecessaryHeredocUnitTest.1.inc.fixed | 108 ++++++++++++++++++ .../Strings/UnnecessaryHeredocUnitTest.2.inc | 108 ++++++++++++++++++ .../UnnecessaryHeredocUnitTest.2.inc.fixed | 108 ++++++++++++++++++ .../Strings/UnnecessaryHeredocUnitTest.3.inc | 6 + .../Strings/UnnecessaryHeredocUnitTest.php | 74 ++++++++++++ 8 files changed, 644 insertions(+) create mode 100644 src/Standards/Generic/Docs/Strings/UnnecessaryHeredocStandard.xml create mode 100644 src/Standards/Generic/Sniffs/Strings/UnnecessaryHeredocSniff.php create mode 100644 src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.1.inc create mode 100644 src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.1.inc.fixed create mode 100644 src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.2.inc create mode 100644 src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.2.inc.fixed create mode 100644 src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.3.inc create mode 100644 src/Standards/Generic/Tests/Strings/UnnecessaryHeredocUnitTest.php 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..2f8cfa7cc6 --- /dev/null +++ b/src/Standards/Generic/Sniffs/Strings/UnnecessaryHeredocSniff.php @@ -0,0 +1,93 @@ + + * @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. + return; + } + + if ($bodyToken[0] === T_CURLY_OPEN + && is_array($tokenizedBody[($ptr + 1)]) === false + && $tokenizedBody[($ptr + 1)] === '$' + ) { + // Contains interpolation or expression. + return; + } + } + + $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