* @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; final class DNFTypesTest extends AbstractTokenizerTestCase { /** * Test that parentheses when **not** used in a type declaration are correctly tokenized. * * @param string $testMarker The comment prefacing the target token. * @param bool $skipCheckInside Optional. Skip checking correct token type inside the parentheses. * Use judiciously for combined normal + DNF tests only. * * @dataProvider dataNormalParentheses * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ public function testNormalParentheses($testMarker, $skipCheckInside=false) { $tokens = $this->phpcsFile->getTokens(); $openPtr = $this->getTargetToken($testMarker, [T_OPEN_PARENTHESIS, T_TYPE_OPEN_PARENTHESIS]); $opener = $tokens[$openPtr]; $this->assertSame('(', $opener['content'], 'Content of type open parenthesis is not "("'); $this->assertSame(T_OPEN_PARENTHESIS, $opener['code'], 'Token tokenized as '.$opener['type'].', not T_OPEN_PARENTHESIS (code)'); $this->assertSame('T_OPEN_PARENTHESIS', $opener['type'], 'Token tokenized as '.$opener['type'].', not T_OPEN_PARENTHESIS (type)'); $closePtr = $opener['parenthesis_closer']; $closer = $tokens[$closePtr]; $this->assertSame(')', $closer['content'], 'Content of type close parenthesis is not ")"'); $this->assertSame(T_CLOSE_PARENTHESIS, $closer['code'], 'Token tokenized as '.$closer['type'].', not T_CLOSE_PARENTHESIS (code)'); $this->assertSame('T_CLOSE_PARENTHESIS', $closer['type'], 'Token tokenized as '.$closer['type'].', not T_CLOSE_PARENTHESIS (type)'); if ($skipCheckInside === false) { for ($i = ($openPtr + 1); $i < $closePtr; $i++) { // If there are ampersands, make sure these are tokenized as bitwise and. if ($tokens[$i]['content'] === '&') { $this->assertSame(T_BITWISE_AND, $tokens[$i]['code'], 'Token tokenized as '.$tokens[$i]['type'].', not T_BITWISE_AND (code)'); $this->assertSame('T_BITWISE_AND', $tokens[$i]['type'], 'Token tokenized as '.$tokens[$i]['type'].', not T_BITWISE_AND (type)'); } // If there are pipes, make sure these are tokenized as bitwise or. if ($tokens[$i]['content'] === '|') { $this->assertSame(T_BITWISE_OR, $tokens[$i]['code'], 'Token tokenized as '.$tokens[$i]['type'].', not T_BITWISE_OR (code)'); $this->assertSame('T_BITWISE_OR', $tokens[$i]['type'], 'Token tokenized as '.$tokens[$i]['type'].', not T_BITWISE_OR (type)'); } } } $before = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($openPtr - 1), null, true); if ($before !== false && $tokens[$before]['content'] === '|') { $this->assertSame( T_BITWISE_OR, $tokens[$before]['code'], 'Token before tokenized as '.$tokens[$before]['type'].', not T_BITWISE_OR (code)' ); $this->assertSame( 'T_BITWISE_OR', $tokens[$before]['type'], 'Token before tokenized as '.$tokens[$before]['type'].', not T_BITWISE_OR (type)' ); } $after = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($closePtr + 1), null, true); if ($after !== false && $tokens[$after]['content'] === '|') { $this->assertSame( T_BITWISE_OR, $tokens[$after]['code'], 'Token after tokenized as '.$tokens[$after]['type'].', not T_BITWISE_OR (code)' ); $this->assertSame( 'T_BITWISE_OR', $tokens[$after]['type'], 'Token after tokenized as '.$tokens[$after]['type'].', not T_BITWISE_OR (type)' ); } }//end testNormalParentheses() /** * Data provider. * * @see testNormalParentheses() * * @return array> */ public static function dataNormalParentheses() { // "Owner" offsets are relative to the open parenthesis. return [ 'parens without owner' => [ 'testMarker' => '/* testParensNoOwner */', ], 'parens without owner in ternary then' => [ 'testMarker' => '/* testParensNoOwnerInTernary */', ], 'parens without owner in short ternary' => [ 'testMarker' => '/* testParensNoOwnerInShortTernary */', ], 'parens with owner: function; & in default value' => [ 'testMarker' => '/* testParensOwnerFunctionAmpersandInDefaultValue */', ], 'parens with owner: closure; param declared by & ref' => [ 'testMarker' => '/* testParensOwnerClosureAmpersandParamRef */', ], 'parens with owner: if' => [ 'testMarker' => '/* testParensOwnerIf */', ], 'parens without owner in if condition' => [ 'testMarker' => '/* testParensNoOwnerInIfCondition */', ], 'parens with owner: for' => [ 'testMarker' => '/* testParensOwnerFor */', ], 'parens without owner in for condition' => [ 'testMarker' => '/* testParensNoOwnerInForCondition */', ], 'parens with owner: match' => [ 'testMarker' => '/* testParensOwnerMatch */', ], 'parens with owner: array' => [ 'testMarker' => '/* testParensOwnerArray */', ], 'parens without owner in array; function call with & in callable' => [ 'testMarker' => '/* testParensNoOwnerFunctionCallWithAmpersandInCallable */', ], 'parens with owner: fn; & in return value' => [ 'testMarker' => '/* testParensOwnerArrowFn */', ], 'parens with owner: list with reference vars' => [ 'testMarker' => '/* testParensOwnerListWithRefVars */', ], 'parens without owner, function call with DNF look-a-like param' => [ 'testMarker' => '/* testParensNoOwnerFunctionCallwithDNFLookALikeParam */', ], 'parens without owner, function call, named param' => [ 'testMarker' => '/* testParensNoOwnerFunctionCallWithDNFLookALikeNamedParamPlain */', ], 'parens without owner, function call, named param + bitwise or' => [ 'testMarker' => '/* testParensNoOwnerFunctionCallWithDNFLookALikeNamedParamUnion */', ], 'parens without owner, function call, named param + bitwise and' => [ 'testMarker' => '/* testParensNoOwnerFunctionCallWithDNFLookALikeNamedParamIntersect */', ], 'parens without owner in OO const default value' => [ 'testMarker' => '/* testParensNoOwnerOOConstDefaultValue */', ], 'parens without owner in property default 1' => [ 'testMarker' => '/* testParensNoOwnerPropertyDefaultValue1 */', ], 'parens without owner in property default 2' => [ 'testMarker' => '/* testParensNoOwnerPropertyDefaultValue2 */', ], 'parens without owner in param default value' => [ 'testMarker' => '/* testParensNoOwnerParamDefaultValue */', ], 'parens without owner in return statement 1' => [ 'testMarker' => '/* testParensNoOwnerInReturnValue1 */', ], 'parens without owner in return statement 2' => [ 'testMarker' => '/* testParensNoOwnerInReturnValue2 */', ], 'parens without owner in return statement 3' => [ 'testMarker' => '/* testParensNoOwnerInReturnValue3 */', ], 'parens with owner: closure; & in default value' => [ 'testMarker' => '/* testParensOwnerClosureAmpersandInDefaultValue */', ], 'parens with owner: fn; dnf used within' => [ 'testMarker' => '/* testParensOwnerArrowDNFUsedWithin */', 'skipCheckInside' => true, ], 'parens without owner: default value for param in arrow function' => [ 'testMarker' => '/* testParensNoOwnerAmpersandInDefaultValue */', ], 'parens without owner in arrow function return expression' => [ 'testMarker' => '/* testParensNoOwnerInArrowReturnExpression */', ], ]; }//end dataNormalParentheses() /** * Test that parentheses when used in a DNF type declaration are correctly tokenized. * * Includes verifying that: * - the tokens between the parentheses all have a "nested_parenthesis" key. * - all ampersands between the parentheses are tokenized as T_TYPE_INTERSECTION. * * @param string $testMarker The comment prefacing the target token. * * @dataProvider dataDNFTypeParentheses * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ public function testDNFTypeParentheses($testMarker) { $tokens = $this->phpcsFile->getTokens(); $openPtr = $this->getTargetToken($testMarker, [T_OPEN_PARENTHESIS, T_TYPE_OPEN_PARENTHESIS]); $opener = $tokens[$openPtr]; $this->assertSame('(', $opener['content'], 'Content of type open parenthesis is not "("'); $this->assertSame(T_TYPE_OPEN_PARENTHESIS, $opener['code'], 'Token tokenized as '.$opener['type'].', not T_TYPE_OPEN_PARENTHESIS (code)'); $this->assertSame('T_TYPE_OPEN_PARENTHESIS', $opener['type'], 'Token tokenized as '.$opener['type'].', not T_TYPE_OPEN_PARENTHESIS (type)'); $closePtr = $opener['parenthesis_closer']; $closer = $tokens[$closePtr]; $this->assertSame(')', $closer['content'], 'Content of type close parenthesis is not ")"'); $this->assertSame(T_TYPE_CLOSE_PARENTHESIS, $closer['code'], 'Token tokenized as '.$closer['type'].', not T_TYPE_CLOSE_PARENTHESIS (code)'); $this->assertSame('T_TYPE_CLOSE_PARENTHESIS', $closer['type'], 'Token tokenized as '.$closer['type'].', not T_TYPE_CLOSE_PARENTHESIS (type)'); $intersectionCount = 0; for ($i = ($openPtr + 1); $i < $closePtr; $i++) { if ($tokens[$i]['content'] === '&') { $this->assertSame( T_TYPE_INTERSECTION, $tokens[$i]['code'], 'Token tokenized as '.$tokens[$i]['type'].', not T_TYPE_INTERSECTION (code)' ); $this->assertSame( 'T_TYPE_INTERSECTION', $tokens[$i]['type'], 'Token tokenized as '.$tokens[$i]['type'].', not T_TYPE_INTERSECTION (type)' ); ++$intersectionCount; } // Not valid, but that's irrelevant for the tokenization. if ($tokens[$i]['content'] === '|') { $this->assertSame(T_TYPE_UNION, $tokens[$i]['code'], 'Token tokenized as '.$tokens[$i]['type'].', not T_TYPE_UNION (code)'); $this->assertSame('T_TYPE_UNION', $tokens[$i]['type'], 'Token tokenized as '.$tokens[$i]['type'].', not T_TYPE_UNION (type)'); // For the purposes of this test, presume it was intended as an intersection. ++$intersectionCount; } }//end for $this->assertGreaterThanOrEqual(1, $intersectionCount, 'Did not find an intersection "&" between the DNF type parentheses'); $before = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($openPtr - 1), null, true); if ($before !== false && $tokens[$before]['content'] === '|') { $this->assertSame( T_TYPE_UNION, $tokens[$before]['code'], 'Token before tokenized as '.$tokens[$before]['type'].', not T_TYPE_UNION (code)' ); $this->assertSame( 'T_TYPE_UNION', $tokens[$before]['type'], 'Token before tokenized as '.$tokens[$before]['type'].', not T_TYPE_UNION (type)' ); } // Invalid, but that's not relevant for the tokenization. if ($before !== false && $tokens[$before]['content'] === '?') { $this->assertSame( T_NULLABLE, $tokens[$before]['code'], 'Token before tokenized as '.$tokens[$before]['type'].', not T_NULLABLE (code)' ); $this->assertSame( 'T_NULLABLE', $tokens[$before]['type'], 'Token before tokenized as '.$tokens[$before]['type'].', not T_NULLABLE (type)' ); } $after = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($closePtr + 1), null, true); if ($after !== false && $tokens[$after]['content'] === '|') { $this->assertSame( T_TYPE_UNION, $tokens[$after]['code'], 'Token after tokenized as '.$tokens[$after]['type'].', not T_TYPE_UNION (code)' ); $this->assertSame( 'T_TYPE_UNION', $tokens[$after]['type'], 'Token after tokenized as '.$tokens[$after]['type'].', not T_TYPE_UNION (type)' ); } }//end testDNFTypeParentheses() /** * Data provider. * * @see testDNFTypeParentheses() * * @return array> */ public static function dataDNFTypeParentheses() { return [ 'OO const type: unqualified classes' => [ 'testMarker' => '/* testDNFTypeOOConstUnqualifiedClasses */', ], 'OO const type: modifiers in reverse order' => [ 'testMarker' => '/* testDNFTypeOOConstReverseModifierOrder */', ], 'OO const type: multi-dnf part 1' => [ 'testMarker' => '/* testDNFTypeOOConstMulti1 */', ], 'OO const type: multi-dnf part 2' => [ 'testMarker' => '/* testDNFTypeOOConstMulti2 */', ], 'OO const type: multi-dnf part 3' => [ 'testMarker' => '/* testDNFTypeOOConstMulti3 */', ], 'OO const type: namespace relative classes' => [ 'testMarker' => '/* testDNFTypeOOConstNamespaceRelative */', ], 'OO const type: partially qualified classes' => [ 'testMarker' => '/* testDNFTypeOOConstPartiallyQualified */', ], 'OO const type: fully qualified classes' => [ 'testMarker' => '/* testDNFTypeOOConstFullyQualified */', ], 'OO property type: unqualified classes' => [ 'testMarker' => '/* testDNFTypePropertyUnqualifiedClasses */', ], 'OO property type: modifiers in reverse order' => [ 'testMarker' => '/* testDNFTypePropertyReverseModifierOrder */', ], 'OO property type: multi-dnf namespace relative classes' => [ 'testMarker' => '/* testDNFTypePropertyMultiNamespaceRelative */', ], 'OO property type: multi-dnf partially qualified classes' => [ 'testMarker' => '/* testDNFTypePropertyMultiPartiallyQualified */', ], 'OO property type: multi-dnf fully qualified classes' => [ 'testMarker' => '/* testDNFTypePropertyMultiFullyQualified */', ], 'OO property type: multi-dnf with readonly keyword 1' => [ 'testMarker' => '/* testDNFTypePropertyWithReadOnlyKeyword1 */', ], 'OO property type: multi-dnf with readonly keyword 2' => [ 'testMarker' => '/* testDNFTypePropertyWithReadOnlyKeyword2 */', ], 'OO property type: with static and readonly keywords' => [ 'testMarker' => '/* testDNFTypePropertyWithStaticAndReadOnlyKeywords */', ], 'OO property type: with only static keyword' => [ 'testMarker' => '/* testDNFTypePropertyWithOnlyStaticKeyword */', ], 'OO method param type: first param' => [ 'testMarker' => '/* testDNFTypeParam1WithAttribute */', ], 'OO method param type: second param, first DNF' => [ 'testMarker' => '/* testDNFTypeParam2 */', ], 'OO method param type: second param, second DNF' => [ 'testMarker' => '/* testDNFTypeParam3 */', ], 'OO method param type: namespace relative classes' => [ 'testMarker' => '/* testDNFTypeParamNamespaceRelative */', ], 'OO method param type: partially qualified classes' => [ 'testMarker' => '/* testDNFTypeParamPartiallyQualified */', ], 'OO method param type: fully qualified classes' => [ 'testMarker' => '/* testDNFTypeParamFullyQualified */', ], 'Constructor property promotion with multi DNF 1' => [ 'testMarker' => '/* testDNFTypeConstructorPropertyPromotion1 */', ], 'Constructor property promotion with multi DNF 2' => [ 'testMarker' => '/* testDNFTypeConstructorPropertyPromotion2 */', ], 'OO method return type: multi DNF 1' => [ 'testMarker' => '/* testDNFTypeReturnType1 */', ], 'OO method return type: multi DNF 2' => [ 'testMarker' => '/* testDNFTypeReturnType2 */', ], 'OO abstract method return type: multi DNF 1' => [ 'testMarker' => '/* testDNFTypeAbstractMethodReturnType1 */', ], 'OO abstract method return type: multi DNF 2' => [ 'testMarker' => '/* testDNFTypeAbstractMethodReturnType2 */', ], 'OO method return type: namespace relative classes' => [ 'testMarker' => '/* testDNFTypeReturnTypeNamespaceRelative */', ], 'OO method return type: partially qualified classes' => [ 'testMarker' => '/* testDNFTypeReturnPartiallyQualified */', ], 'OO method return type: fully qualified classes' => [ 'testMarker' => '/* testDNFTypeReturnFullyQualified */', ], 'function param type: with reference' => [ 'testMarker' => '/* testDNFTypeWithReference */', ], 'function param type: with spread' => [ 'testMarker' => '/* testDNFTypeWithSpreadOperator */', ], 'closure param type: with illegal nullable' => [ 'testMarker' => '/* testDNFTypeClosureParamIllegalNullable */', ], 'closure return type' => [ 'testMarker' => '/* testDNFTypeClosureReturn */', ], 'arrow function param type' => [ 'testMarker' => '/* testDNFTypeArrowParam */', ], 'arrow function return type' => [ 'testMarker' => '/* testDNFTypeArrowReturnType */', ], 'arrow function param type with return by ref' => [ 'testMarker' => '/* testDNFTypeArrowParamWithReturnByRef */', ], 'illegal syntax: unnecessary parentheses (no union)' => [ 'testMarker' => '/* testDNFTypeParamIllegalUnnecessaryParens */', ], 'illegal syntax: union within parentheses, intersect outside' => [ 'testMarker' => '/* testDNFTypeParamIllegalIntersectUnionReversed */', ], 'illegal syntax: nested parentheses' => [ 'testMarker' => '/* testDNFTypeParamIllegalNestedParens */', ], ]; }//end dataDNFTypeParentheses() }//end class