diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59df883a74..d402c1ade0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,11 @@ jobs: runs-on: ubuntu-latest name: "Build Phar on PHP: 8.0" + permissions: + id-token: write + contents: read + attestations: write + steps: - name: Checkout code uses: actions/checkout@v4 @@ -39,6 +44,17 @@ jobs: - name: Build the phar run: php scripts/build-phar.php + # Provide provenance for generated binaries. + # Only attests the build artifacts which will be used in the published releases as per the guidelines in "what to attest". + # https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds + - name: Generate artifact attestations + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest-build-provenance@v1 + with: + subject-path: | + ${{ github.workspace }}/phpcs.phar + ${{ github.workspace }}/phpcbf.phar + - name: Upload the PHPCS phar uses: actions/upload-artifact@v4 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4172e1cdab..ae6e056d42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -396,7 +396,7 @@ _Nothing yet._ - **_In contrast to earlier information, the `squizlabs/php_codesniffer` package now points to the new repository and everything will continue to work as before._** - PHIVE users may need to clear the PHIVE URL cache. - PHIVE users who don't use the package alias, but refer to the package URL, will need to update the URL from `https://squizlabs.github.io/PHP_CodeSniffer/phars/` to `https://phars.phpcodesniffer.com/phars/`. - - Users who download the PHAR files using curl or wget, will need to update the download URL from `https://squizlabs.github.io/PHP_CodeSniffer/[phpcs|phpcbf].phar` or `https://github.com/squizlabs/PHP_CodeSnifffer/releases/latest/download/[phpcs|phpcbf].phar` to `https://phars.phpcodesniffer.com/[phpcs|phpcbf].phar`. + - Users who download the PHAR files using curl or wget, will need to update the download URL from `https://squizlabs.github.io/PHP_CodeSniffer/[phpcs|phpcbf].phar` or `https://github.com/squizlabs/PHP_CodeSniffer/releases/latest/download/[phpcs|phpcbf].phar` to `https://phars.phpcodesniffer.com/[phpcs|phpcbf].phar`. - For users who install PHP_CodeSniffer via the [Setup-PHP](https://github.com/shivammathur/setup-php/) action runner for GitHub Actions, nothing changes. - Users using a git clone will need to update the clone address from `git@github.com:squizlabs/PHP_CodeSniffer.git` to `git@github.com:PHPCSStandards/PHP_CodeSniffer.git`. - Contributors will need to fork the new repo and add both the new fork as well as the new repo as remotes to their local git copy of PHP_CodeSniffer. diff --git a/src/Sniffs/AbstractPatternSniff.php b/src/Sniffs/AbstractPatternSniff.php index e895365688..d9528dccf8 100644 --- a/src/Sniffs/AbstractPatternSniff.php +++ b/src/Sniffs/AbstractPatternSniff.php @@ -416,6 +416,11 @@ protected function processPattern($patternInfo, File $phpcsFile, $stackPtr) $lastAddedStackPtr = null; $patternLen = count($pattern); + if (($stackPtr + $patternLen - $patternInfo['listen_pos']) > $phpcsFile->numTokens) { + // Pattern can never match as there are not enough tokens left in the file. + return false; + } + for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) { if (isset($tokens[$stackPtr]) === false) { break; diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php index 1a36917171..76886471d7 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php @@ -27,15 +27,12 @@ public function register() { $targets = Tokens::$contextSensitiveKeywords; $targets += [ + T_ANON_CLASS => T_ANON_CLASS, T_CLOSURE => T_CLOSURE, - T_EMPTY => T_EMPTY, T_ENUM_CASE => T_ENUM_CASE, - T_EVAL => T_EVAL, - T_ISSET => T_ISSET, T_MATCH_DEFAULT => T_MATCH_DEFAULT, T_PARENT => T_PARENT, T_SELF => T_SELF, - T_UNSET => T_UNSET, ]; return $targets; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc index 37579d3217..a50ac22853 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc @@ -44,5 +44,14 @@ EnuM ENUM: string Case HEARTS; } +new Class {}; +new clasS extends stdClass {}; +new class {}; + +if (isset($a) && !empty($a)) { unset($a); } +if (ISSET($a) && !Empty($a)) { UnSeT($a); } +eval('foo'); +eVaL('foo'); + __HALT_COMPILER(); // An exception due to phar support. function diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed index 7063327ae8..5e15765146 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed @@ -44,5 +44,14 @@ enum ENUM: string case HEARTS; } +new class {}; +new class extends stdClass {}; +new class {}; + +if (isset($a) && !empty($a)) { unset($a); } +if (isset($a) && !empty($a)) { unset($a); } +eval('foo'); +eval('foo'); + __HALT_COMPILER(); // An exception due to phar support. function diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php index 17f0e25d3e..6d337504f4 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php @@ -48,6 +48,10 @@ public function getErrorList() 39 => 2, 42 => 1, 44 => 1, + 47 => 1, + 48 => 1, + 52 => 3, + 54 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php b/src/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php index 4c54aa4618..8ee4de45a8 100644 --- a/src/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php +++ b/src/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php @@ -221,15 +221,23 @@ protected function getDeclarationNameWithNamespace(array $tokens, $stackPtr) */ protected function getNamespaceOfScope(File $phpcsFile, $stackPtr) { - $namespace = '\\'; - $namespaceDeclaration = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr); + $namespace = '\\'; + $tokens = $phpcsFile->getTokens(); + + while (($namespaceDeclaration = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr)) !== false) { + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($namespaceDeclaration + 1), null, true); + if ($tokens[$nextNonEmpty]['code'] === T_NS_SEPARATOR) { + // Namespace operator. Ignore. + $stackPtr = ($namespaceDeclaration - 1); + continue; + } - if ($namespaceDeclaration !== false) { $endOfNamespaceDeclaration = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_CLOSE_TAG], $namespaceDeclaration); $namespace = $this->getDeclarationNameWithNamespace( $phpcsFile->getTokens(), ($endOfNamespaceDeclaration - 1) ); + break; } return $namespace; diff --git a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php index b79e6b3ec6..44429012a1 100644 --- a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php +++ b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php @@ -354,16 +354,23 @@ public function addMissingBracketsError($phpcsFile, $stackPtr) } if ($tokens[$after]['code'] === T_OPEN_PARENTHESIS) { + if (isset($tokens[$after]['parenthesis_closer']) === false) { + // Live coding/parse error. Ignore. + return; + } + $after = $tokens[$after]['parenthesis_closer']; continue; } - if ($tokens[$after]['code'] === T_OPEN_SQUARE_BRACKET) { - $after = $tokens[$after]['bracket_closer']; - continue; - } + if (($tokens[$after]['code'] === T_OPEN_SQUARE_BRACKET + || $tokens[$after]['code'] === T_OPEN_SHORT_ARRAY) + ) { + if (isset($tokens[$after]['bracket_closer']) === false) { + // Live coding/parse error. Ignore. + return; + } - if ($tokens[$after]['code'] === T_OPEN_SHORT_ARRAY) { $after = $tokens[$after]['bracket_closer']; continue; } diff --git a/src/Standards/Squiz/Sniffs/PHP/EmbeddedPhpSniff.php b/src/Standards/Squiz/Sniffs/PHP/EmbeddedPhpSniff.php index 0be6118e43..63a1cdd09a 100644 --- a/src/Standards/Squiz/Sniffs/PHP/EmbeddedPhpSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/EmbeddedPhpSniff.php @@ -379,10 +379,14 @@ private function validateInlineEmbeddedPhp($phpcsFile, $stackPtr, $closeTag) } // Check that there is one, and only one space at the start of the statement. - $leadingSpace = 0; - if ($tokens[$stackPtr]['code'] === T_OPEN_TAG) { + $leadingSpace = 0; + $isLongOpenTag = false; + if ($tokens[$stackPtr]['code'] === T_OPEN_TAG + && stripos($tokens[$stackPtr]['content'], 'addFixableError($error, $stackPtr, 'SpacingAfterOpen', $data); if ($fix === true) { - if ($tokens[$stackPtr]['code'] === T_OPEN_TAG) { + if ($isLongOpenTag === true) { $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); } else if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) { // Short open tag with too much whitespace. diff --git a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc index f2c4713a66..4f17813866 100644 --- a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc @@ -183,3 +183,17 @@ class Baz { \EndsIn\CloseTag\Baz::something(); } } + +// Issue PHPCSStandards/PHP_CodeSniffer#553. +namespace TestMe; + +namespace\functionCall(); +namespace\anotherFunctionCall(); + +class SelfMemberReference +{ + public function falseNegative() + { + $testResults[] = \TestMe\SelfMemberReference::test(); + } +} diff --git a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc.fixed index 95689d4a9f..770429dc82 100644 --- a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc.fixed @@ -171,3 +171,17 @@ class Baz { self::something(); } } + +// Issue PHPCSStandards/PHP_CodeSniffer#553. +namespace TestMe; + +namespace\functionCall(); +namespace\anotherFunctionCall(); + +class SelfMemberReference +{ + public function falseNegative() + { + $testResults[] = self::test(); + } +} diff --git a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.php b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.php index 180a010631..84b9c15e55 100644 --- a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.php @@ -47,6 +47,7 @@ public function getErrorList() 162 => 1, 171 => 1, 183 => 1, + 197 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc similarity index 100% rename from src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc rename to src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc.fixed similarity index 100% rename from src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed rename to src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc.fixed diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.2.inc b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.2.inc new file mode 100644 index 0000000000..1284f121fc --- /dev/null +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.2.inc @@ -0,0 +1,7 @@ + 1, 6 => 1, diff --git a/src/Standards/Squiz/Tests/Functions/FunctionDeclarationUnitTest.inc b/src/Standards/Squiz/Tests/Functions/FunctionDeclarationUnitTest.1.inc similarity index 100% rename from src/Standards/Squiz/Tests/Functions/FunctionDeclarationUnitTest.inc rename to src/Standards/Squiz/Tests/Functions/FunctionDeclarationUnitTest.1.inc diff --git a/src/Standards/Squiz/Tests/Functions/FunctionDeclarationUnitTest.2.inc b/src/Standards/Squiz/Tests/Functions/FunctionDeclarationUnitTest.2.inc new file mode 100644 index 0000000000..51a55e92ad --- /dev/null +++ b/src/Standards/Squiz/Tests/Functions/FunctionDeclarationUnitTest.2.inc @@ -0,0 +1,5 @@ + */ - public function getErrorList() + public function getErrorList($testFile='') { - return [ - 55 => 1, - 68 => 1, - ]; + switch ($testFile) { + case 'FunctionDeclarationUnitTest.1.inc': + return [ + 55 => 1, + 68 => 1, + ]; + + default: + return []; + }//end switch }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/PHP/EmbeddedPhpUnitTest.1.inc b/src/Standards/Squiz/Tests/PHP/EmbeddedPhpUnitTest.1.inc index f28c559894..015468357d 100644 --- a/src/Standards/Squiz/Tests/PHP/EmbeddedPhpUnitTest.1.inc +++ b/src/Standards/Squiz/Tests/PHP/EmbeddedPhpUnitTest.1.inc @@ -265,6 +265,10 @@ echo 'the PHP tag is correctly indented as an indent less than the previous code echo 'the PHP tag is incorrectly indented as the indent is more than 4 different from the indent of the previous code'; ?> + + + + + + + + + + + + + + + + + + + + + + + + + + 1, 263 => 1, 264 => 1, + 270 => 1, ]; case 'EmbeddedPhpUnitTest.2.inc': @@ -190,6 +191,16 @@ public function getErrorList($testFile='') 22 => 2, ]; + case 'EmbeddedPhpUnitTest.24.inc': + $shortOpenTagDirective = (bool) ini_get('short_open_tag'); + if ($shortOpenTagDirective === true) { + return [ + 18 => 1, + 20 => 1, + ]; + } + return []; + default: return []; }//end switch diff --git a/src/Tokenizers/Tokenizer.php b/src/Tokenizers/Tokenizer.php index ba41f8da88..68238d3023 100644 --- a/src/Tokenizers/Tokenizer.php +++ b/src/Tokenizers/Tokenizer.php @@ -193,6 +193,8 @@ private function createPositionMap() T_DOC_COMMENT_STRING => true, T_CONSTANT_ENCAPSED_STRING => true, T_DOUBLE_QUOTED_STRING => true, + T_START_HEREDOC => true, + T_START_NOWDOC => true, T_HEREDOC => true, T_NOWDOC => true, T_END_HEREDOC => true, diff --git a/tests/Core/Tokenizer/PHP/HeredocNowdocTest.inc b/tests/Core/Tokenizer/PHP/HeredocNowdocTest.inc new file mode 100644 index 0000000000..5041dda157 --- /dev/null +++ b/tests/Core/Tokenizer/PHP/HeredocNowdocTest.inc @@ -0,0 +1,39 @@ + + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer\PHP; + +use PHP_CodeSniffer\Tests\Core\Tokenizer\AbstractTokenizerTestCase; +use PHP_CodeSniffer\Util\Tokens; + +/** + * Tests the tokenization for heredoc/nowdoc constructs. + * + * Verifies that: + * - Nowdoc opener/closers are retokenized from `T_[START_|END_]HEREDOC` to `T_[START_|END_]NOWDOC`. + * - The contents of the heredoc/nowdoc is tokenized as `T_HEREDOC`/`T_NOWDOC`. + * - Each line of the contents has its own token, which includes the new line char. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + */ +final class HeredocNowdocTest extends AbstractTokenizerTestCase +{ + + + /** + * Verify tokenization a heredoc construct. + * + * @phpcs:disable Squiz.Arrays.ArrayDeclaration.SpaceBeforeDoubleArrow -- Readability is better with alignment. + * + * @return void + */ + public function testHeredocSingleLine() + { + $expectedSequence = [ + [T_START_HEREDOC => '<< 'Some $var text'."\n"], + [T_END_HEREDOC => 'EOD'], + ]; + + $target = $this->getTargetToken('/* '.__FUNCTION__.' */', T_START_HEREDOC); + + $this->checkTokenSequence($target, $expectedSequence); + + }//end testHeredocSingleLine() + + + /** + * Verify tokenization a nowdoc construct. + * + * @phpcs:disable Squiz.Arrays.ArrayDeclaration.SpaceBeforeDoubleArrow -- Readability is better with alignment. + * + * @return void + */ + public function testNowdocSingleLine() + { + $expectedSequence = [ + [T_START_NOWDOC => "<<<'MARKER'\n"], + [T_NOWDOC => 'Some text'."\n"], + [T_END_NOWDOC => 'MARKER'], + ]; + + $target = $this->getTargetToken('/* '.__FUNCTION__.' */', T_START_NOWDOC); + + $this->checkTokenSequence($target, $expectedSequence); + + }//end testNowdocSingleLine() + + + /** + * Verify tokenization a multiline heredoc construct. + * + * @phpcs:disable Squiz.Arrays.ArrayDeclaration.SpaceBeforeDoubleArrow -- Readability is better with alignment. + * + * @return void + */ + public function testHeredocMultiLine() + { + $expectedSequence = [ + [T_START_HEREDOC => '<<<"😬"'."\n"], + [T_HEREDOC => 'Lorum ipsum'."\n"], + [T_HEREDOC => 'Some $var text'."\n"], + [T_HEREDOC => 'dolor sit amet'."\n"], + [T_END_HEREDOC => '😬'], + ]; + + $target = $this->getTargetToken('/* '.__FUNCTION__.' */', T_START_HEREDOC); + + $this->checkTokenSequence($target, $expectedSequence); + + }//end testHeredocMultiLine() + + + /** + * Verify tokenization a multiline testNowdocSingleLine construct. + * + * @phpcs:disable Squiz.Arrays.ArrayDeclaration.SpaceBeforeDoubleArrow -- Readability is better with alignment. + * + * @return void + */ + public function testNowdocMultiLine() + { + $expectedSequence = [ + [T_START_NOWDOC => "<<<'multi_line'\n"], + [T_NOWDOC => 'Lorum ipsum'."\n"], + [T_NOWDOC => 'Some text'."\n"], + [T_NOWDOC => 'dolor sit amet'."\n"], + [T_END_NOWDOC => 'multi_line'], + ]; + + $target = $this->getTargetToken('/* '.__FUNCTION__.' */', T_START_NOWDOC); + + $this->checkTokenSequence($target, $expectedSequence); + + }//end testNowdocMultiLine() + + + /** + * Verify tokenization a multiline heredoc construct. + * + * @phpcs:disable Squiz.Arrays.ArrayDeclaration.SpaceBeforeDoubleArrow -- Readability is better with alignment. + * + * @return void + */ + public function testHeredocEndsOnBlankLine() + { + $expectedSequence = [ + [T_START_HEREDOC => '<< 'Lorum ipsum'."\n"], + [T_HEREDOC => 'dolor sit amet'."\n"], + [T_HEREDOC => "\n"], + [T_END_HEREDOC => 'EOD'], + ]; + + $target = $this->getTargetToken('/* '.__FUNCTION__.' */', T_START_HEREDOC); + + $this->checkTokenSequence($target, $expectedSequence); + + }//end testHeredocEndsOnBlankLine() + + + /** + * Verify tokenization a multiline testNowdocSingleLine construct. + * + * @phpcs:disable Squiz.Arrays.ArrayDeclaration.SpaceBeforeDoubleArrow -- Readability is better with alignment. + * + * @return void + */ + public function testNowdocEndsOnBlankLine() + { + $expectedSequence = [ + [T_START_NOWDOC => "<<<'EOD'\n"], + [T_NOWDOC => 'Lorum ipsum'."\n"], + [T_NOWDOC => 'dolor sit amet'."\n"], + [T_NOWDOC => "\n"], + [T_END_NOWDOC => 'EOD'], + ]; + + $target = $this->getTargetToken('/* '.__FUNCTION__.' */', T_START_NOWDOC); + + $this->checkTokenSequence($target, $expectedSequence); + + }//end testNowdocEndsOnBlankLine() + + + /** + * Test helper. Check a token sequence complies with an expected token sequence. + * + * @param int $startPtr The position in the file to start checking from. + * @param array> $expectedSequence The consecutive token constants and their contents to expect. + * + * @return void + */ + private function checkTokenSequence($startPtr, array $expectedSequence) + { + $tokens = $this->phpcsFile->getTokens(); + + $sequenceKey = 0; + $sequenceCount = count($expectedSequence); + + for ($i = $startPtr; $sequenceKey < $sequenceCount; $i++, $sequenceKey++) { + $currentItem = $expectedSequence[$sequenceKey]; + $expectedCode = key($currentItem); + $expectedType = Tokens::tokenName($expectedCode); + $expectedContent = current($currentItem); + $errorMsgSuffix = PHP_EOL.'(StackPtr: '.$i.' | Position in sequence: '.$sequenceKey.' | Expected: '.$expectedType.')'; + + $this->assertSame( + $expectedCode, + $tokens[$i]['code'], + 'Token tokenized as '.Tokens::tokenName($tokens[$i]['code']).', not '.$expectedType.' (code)'.$errorMsgSuffix + ); + + $this->assertSame( + $expectedType, + $tokens[$i]['type'], + 'Token tokenized as '.$tokens[$i]['type'].', not '.$expectedType.' (type)'.$errorMsgSuffix + ); + + $this->assertSame( + $expectedContent, + $tokens[$i]['content'], + 'Token content did not match expectations'.$errorMsgSuffix + ); + }//end for + + }//end checkTokenSequence() + + +}//end class diff --git a/tests/Core/Tokenizer/PHP/HeredocParseErrorTest.inc b/tests/Core/Tokenizer/PHP/HeredocParseErrorTest.inc new file mode 100644 index 0000000000..d552b12832 --- /dev/null +++ b/tests/Core/Tokenizer/PHP/HeredocParseErrorTest.inc @@ -0,0 +1,11 @@ +>>>>>> master diff --git a/tests/Core/Tokenizer/PHP/HeredocParseErrorTest.php b/tests/Core/Tokenizer/PHP/HeredocParseErrorTest.php new file mode 100644 index 0000000000..7cca49aac3 --- /dev/null +++ b/tests/Core/Tokenizer/PHP/HeredocParseErrorTest.php @@ -0,0 +1,41 @@ + + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer\PHP; + +use PHP_CodeSniffer\Tests\Core\Tokenizer\AbstractTokenizerTestCase; + +/** + * Tests the tokenization for an unclosed heredoc construct. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + */ +final class HeredocParseErrorTest extends AbstractTokenizerTestCase +{ + + + /** + * Verify that a heredoc (and nowdoc) start token is retokenized to T_STRING if no closer is found. + * + * @return void + */ + public function testMergeConflict() + { + $tokens = $this->phpcsFile->getTokens(); + + $token = $this->getTargetToken('/* testUnclosedHeredoc */', [T_START_HEREDOC, T_STRING], '<<< HEAD'."\n"); + $tokenArray = $tokens[$token]; + + $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_START_HEREDOC (code)'); + $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_START_HEREDOC (type)'); + + }//end testMergeConflict() + + +}//end class diff --git a/tests/Core/Tokenizer/Tokenizer/HeredocNowdocOpenerTest.inc b/tests/Core/Tokenizer/Tokenizer/HeredocNowdocOpenerTest.inc new file mode 100644 index 0000000000..dc2b2f2dc2 --- /dev/null +++ b/tests/Core/Tokenizer/Tokenizer/HeredocNowdocOpenerTest.inc @@ -0,0 +1,31 @@ + + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\Tokenizer\AbstractTokenizerTestCase; + +/** + * Heredoc/nowdoc opener token test. + */ +final class HeredocNowdocOpenerTest extends AbstractTokenizerTestCase +{ + + + /** + * Verify that spaces/tabs in a heredoc/nowdoc opener token get the tab replacement treatment. + * + * @param string $testMarker The comment prefacing the target token. + * @param array $expected Expectations for the token array. + * + * @dataProvider dataHeredocNowdocOpenerTabReplacement + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * + * @return void + */ + public function testHeredocNowdocOpenerTabReplacement($testMarker, $expected) + { + $tokens = $this->phpcsFile->getTokens(); + $opener = $this->getTargetToken($testMarker, [T_START_HEREDOC, T_START_NOWDOC]); + + foreach ($expected as $key => $value) { + if ($key === 'orig_content' && $value === null) { + $this->assertArrayNotHasKey($key, $tokens[$opener], "Unexpected 'orig_content' key found in the token array."); + continue; + } + + $this->assertArrayHasKey($key, $tokens[$opener], "Key $key not found in the token array."); + $this->assertSame($value, $tokens[$opener][$key], "Value for key $key does not match expectation."); + } + + }//end testHeredocNowdocOpenerTabReplacement() + + + /** + * Data provider. + * + * @see testHeredocNowdocOpenerTabReplacement() + * + * @return array>> + */ + public static function dataHeredocNowdocOpenerTabReplacement() + { + return [ + 'Heredoc opener without space' => [ + 'testMarker' => '/* testHeredocOpenerNoSpace */', + 'expected' => [ + 'length' => 6, + 'content' => '<< null, + ], + ], + 'Nowdoc opener without space' => [ + 'testMarker' => '/* testNowdocOpenerNoSpace */', + 'expected' => [ + 'length' => 8, + 'content' => "<<<'EOD' +", + 'orig_content' => null, + ], + ], + 'Heredoc opener with space(s)' => [ + 'testMarker' => '/* testHeredocOpenerHasSpace */', + 'expected' => [ + 'length' => 7, + 'content' => '<<< END +', + 'orig_content' => null, + ], + ], + 'Nowdoc opener with space(s)' => [ + 'testMarker' => '/* testNowdocOpenerHasSpace */', + 'expected' => [ + 'length' => 21, + 'content' => "<<< 'END' +", + 'orig_content' => null, + ], + ], + 'Heredoc opener with tab(s)' => [ + 'testMarker' => '/* testHeredocOpenerHasTab */', + 'expected' => [ + 'length' => 18, + 'content' => '<<< "END" +', + 'orig_content' => '<<< "END" +', + ], + ], + 'Nowdoc opener with tab(s)' => [ + 'testMarker' => '/* testNowdocOpenerHasTab */', + 'expected' => [ + 'length' => 11, + 'content' => "<<< 'END' +", + 'orig_content' => "<<< 'END' +", + ], + ], + ]; + + }//end dataHeredocNowdocOpenerTabReplacement() + + +}//end class