* @copyright 2020-2021 Squiz Pty Ltd (ABN 77 084 670 600) * @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 BackfillMatchTokenTest extends AbstractTokenizerTestCase { /** * Test tokenization of match expressions. * * @param string $testMarker The comment prefacing the target token. * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker. * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker. * @param string $testContent The token content to look for. * * @dataProvider dataMatchExpression * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize * * @return void */ public function testMatchExpression($testMarker, $openerOffset, $closerOffset, $testContent='match') { $tokens = $this->phpcsFile->getTokens(); $token = $this->getTargetToken($testMarker, [T_STRING, T_MATCH], $testContent); $tokenArray = $tokens[$token]; $this->assertSame(T_MATCH, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH (code)'); $this->assertSame('T_MATCH', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH (type)'); $this->scopeTestHelper($token, $openerOffset, $closerOffset); $this->parenthesisTestHelper($token); }//end testMatchExpression() /** * Data provider. * * @see testMatchExpression() * * @return array> */ public static function dataMatchExpression() { return [ 'simple_match' => [ 'testMarker' => '/* testMatchSimple */', 'openerOffset' => 6, 'closerOffset' => 33, ], 'no_trailing_comma' => [ 'testMarker' => '/* testMatchNoTrailingComma */', 'openerOffset' => 6, 'closerOffset' => 24, ], 'with_default_case' => [ 'testMarker' => '/* testMatchWithDefault */', 'openerOffset' => 6, 'closerOffset' => 33, ], 'expression_in_condition' => [ 'testMarker' => '/* testMatchExpressionInCondition */', 'openerOffset' => 6, 'closerOffset' => 77, ], 'multicase' => [ 'testMarker' => '/* testMatchMultiCase */', 'openerOffset' => 6, 'closerOffset' => 40, ], 'multicase_trailing_comma_in_case' => [ 'testMarker' => '/* testMatchMultiCaseTrailingCommaInCase */', 'openerOffset' => 6, 'closerOffset' => 47, ], 'in_closure_not_lowercase' => [ 'testMarker' => '/* testMatchInClosureNotLowercase */', 'openerOffset' => 6, 'closerOffset' => 36, 'testContent' => 'Match', ], 'in_arrow_function' => [ 'testMarker' => '/* testMatchInArrowFunction */', 'openerOffset' => 5, 'closerOffset' => 36, ], 'arrow_function_in_match_no_trailing_comma' => [ 'testMarker' => '/* testArrowFunctionInMatchNoTrailingComma */', 'openerOffset' => 6, 'closerOffset' => 44, ], 'in_function_call_param_not_lowercase' => [ 'testMarker' => '/* testMatchInFunctionCallParamNotLowercase */', 'openerOffset' => 8, 'closerOffset' => 32, 'testContent' => 'MATCH', ], 'in_method_call_param' => [ 'testMarker' => '/* testMatchInMethodCallParam */', 'openerOffset' => 5, 'closerOffset' => 13, ], 'discard_result' => [ 'testMarker' => '/* testMatchDiscardResult */', 'openerOffset' => 6, 'closerOffset' => 18, ], 'duplicate_conditions_and_comments' => [ 'testMarker' => '/* testMatchWithDuplicateConditionsWithComments */', 'openerOffset' => 12, 'closerOffset' => 59, ], 'nested_match_outer' => [ 'testMarker' => '/* testNestedMatchOuter */', 'openerOffset' => 6, 'closerOffset' => 33, ], 'nested_match_inner' => [ 'testMarker' => '/* testNestedMatchInner */', 'openerOffset' => 6, 'closerOffset' => 14, ], 'ternary_condition' => [ 'testMarker' => '/* testMatchInTernaryCondition */', 'openerOffset' => 6, 'closerOffset' => 21, ], 'ternary_then' => [ 'testMarker' => '/* testMatchInTernaryThen */', 'openerOffset' => 6, 'closerOffset' => 21, ], 'ternary_else' => [ 'testMarker' => '/* testMatchInTernaryElse */', 'openerOffset' => 6, 'closerOffset' => 21, ], 'array_value' => [ 'testMarker' => '/* testMatchInArrayValue */', 'openerOffset' => 6, 'closerOffset' => 21, ], 'array_key' => [ 'testMarker' => '/* testMatchInArrayKey */', 'openerOffset' => 6, 'closerOffset' => 21, ], 'returning_array' => [ 'testMarker' => '/* testMatchreturningArray */', 'openerOffset' => 6, 'closerOffset' => 125, ], 'nested_in_switch_case_1' => [ 'testMarker' => '/* testMatchWithDefaultNestedInSwitchCase1 */', 'openerOffset' => 6, 'closerOffset' => 25, ], 'nested_in_switch_case_2' => [ 'testMarker' => '/* testMatchWithDefaultNestedInSwitchCase2 */', 'openerOffset' => 6, 'closerOffset' => 25, ], 'nested_in_switch_default' => [ 'testMarker' => '/* testMatchWithDefaultNestedInSwitchDefault */', 'openerOffset' => 6, 'closerOffset' => 25, ], 'match_with_nested_switch' => [ 'testMarker' => '/* testMatchContainingSwitch */', 'openerOffset' => 6, 'closerOffset' => 180, ], 'no_cases' => [ 'testMarker' => '/* testMatchNoCases */', 'openerOffset' => 6, 'closerOffset' => 7, ], 'multi_default' => [ 'testMarker' => '/* testMatchMultiDefault */', 'openerOffset' => 6, 'closerOffset' => 40, ], ]; }//end dataMatchExpression() /** * Verify that "match" keywords which are not match control structures get tokenized as T_STRING * and don't have the extra token array indexes. * * @param string $testMarker The comment prefacing the target token. * @param string $testContent The token content to look for. * * @dataProvider dataNotAMatchStructure * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ public function testNotAMatchStructure($testMarker, $testContent='match') { $tokens = $this->phpcsFile->getTokens(); $token = $this->getTargetToken($testMarker, [T_STRING, T_MATCH], $testContent); $tokenArray = $tokens[$token]; $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)'); $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (type)'); $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set'); $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set'); $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set'); $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set'); $this->assertArrayNotHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is set'); $this->assertArrayNotHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is set'); $next = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($token + 1), null, true); if ($next !== false && $tokens[$next]['code'] === T_OPEN_PARENTHESIS) { $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set for opener after'); } }//end testNotAMatchStructure() /** * Data provider. * * @see testNotAMatchStructure() * * @return array> */ public static function dataNotAMatchStructure() { return [ 'static_method_call' => [ 'testMarker' => '/* testNoMatchStaticMethodCall */', ], 'class_constant_access' => [ 'testMarker' => '/* testNoMatchClassConstantAccess */', 'testContent' => 'MATCH', ], 'class_constant_array_access' => [ 'testMarker' => '/* testNoMatchClassConstantArrayAccessMixedCase */', 'testContent' => 'Match', ], 'method_call' => [ 'testMarker' => '/* testNoMatchMethodCall */', ], 'method_call_uppercase' => [ 'testMarker' => '/* testNoMatchMethodCallUpper */', 'testContent' => 'MATCH', ], 'property_access' => [ 'testMarker' => '/* testNoMatchPropertyAccess */', ], 'namespaced_function_call' => [ 'testMarker' => '/* testNoMatchNamespacedFunctionCall */', ], 'namespace_operator_function_call' => [ 'testMarker' => '/* testNoMatchNamespaceOperatorFunctionCall */', ], 'interface_method_declaration' => [ 'testMarker' => '/* testNoMatchInterfaceMethodDeclaration */', ], 'class_constant_declaration' => [ 'testMarker' => '/* testNoMatchClassConstantDeclarationLower */', ], 'class_method_declaration' => [ 'testMarker' => '/* testNoMatchClassMethodDeclaration */', ], 'property_assigment' => [ 'testMarker' => '/* testNoMatchPropertyAssignment */', ], 'class_instantiation' => [ 'testMarker' => '/* testNoMatchClassInstantiation */', 'testContent' => 'Match', ], 'anon_class_method_declaration' => [ 'testMarker' => '/* testNoMatchAnonClassMethodDeclaration */', 'testContent' => 'maTCH', ], 'class_declaration' => [ 'testMarker' => '/* testNoMatchClassDeclaration */', 'testContent' => 'Match', ], 'interface_declaration' => [ 'testMarker' => '/* testNoMatchInterfaceDeclaration */', 'testContent' => 'Match', ], 'trait_declaration' => [ 'testMarker' => '/* testNoMatchTraitDeclaration */', 'testContent' => 'Match', ], 'constant_declaration' => [ 'testMarker' => '/* testNoMatchConstantDeclaration */', 'testContent' => 'MATCH', ], 'function_declaration' => [ 'testMarker' => '/* testNoMatchFunctionDeclaration */', ], 'namespace_declaration' => [ 'testMarker' => '/* testNoMatchNamespaceDeclaration */', 'testContent' => 'Match', ], 'class_extends_declaration' => [ 'testMarker' => '/* testNoMatchExtendedClassDeclaration */', 'testContent' => 'Match', ], 'class_implements_declaration' => [ 'testMarker' => '/* testNoMatchImplementedClassDeclaration */', 'testContent' => 'Match', ], 'use_statement' => [ 'testMarker' => '/* testNoMatchInUseStatement */', 'testContent' => 'Match', ], 'unsupported_inline_control_structure' => [ 'testMarker' => '/* testNoMatchMissingCurlies */', ], 'unsupported_alternative_syntax' => [ 'testMarker' => '/* testNoMatchAlternativeSyntax */', ], 'live_coding' => [ 'testMarker' => '/* testLiveCoding */', ], ]; }//end dataNotAMatchStructure() /** * Verify that the tokenization of switch structures is not affected by the backfill. * * @param string $testMarker The comment prefacing the target token. * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker. * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker. * * @dataProvider dataSwitchExpression * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ public function testSwitchExpression($testMarker, $openerOffset, $closerOffset) { $token = $this->getTargetToken($testMarker, T_SWITCH); $this->scopeTestHelper($token, $openerOffset, $closerOffset); $this->parenthesisTestHelper($token); }//end testSwitchExpression() /** * Data provider. * * @see testSwitchExpression() * * @return array> */ public static function dataSwitchExpression() { return [ 'switch_containing_match' => [ 'testMarker' => '/* testSwitchContainingMatch */', 'openerOffset' => 6, 'closerOffset' => 174, ], 'match_containing_switch_1' => [ 'testMarker' => '/* testSwitchNestedInMatch1 */', 'openerOffset' => 5, 'closerOffset' => 63, ], 'match_containing_switch_2' => [ 'testMarker' => '/* testSwitchNestedInMatch2 */', 'openerOffset' => 5, 'closerOffset' => 63, ], ]; }//end dataSwitchExpression() /** * Verify that the tokenization of a switch case/default structure containing a match structure * or contained *in* a match structure is not affected by the backfill. * * @param string $testMarker The comment prefacing the target token. * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker. * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker. * * @dataProvider dataSwitchCaseVersusMatch * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ public function testSwitchCaseVersusMatch($testMarker, $openerOffset, $closerOffset) { $token = $this->getTargetToken($testMarker, [T_CASE, T_DEFAULT]); $this->scopeTestHelper($token, $openerOffset, $closerOffset); }//end testSwitchCaseVersusMatch() /** * Data provider. * * @see testSwitchCaseVersusMatch() * * @return array> */ public static function dataSwitchCaseVersusMatch() { return [ 'switch_with_nested_match_case_1' => [ 'testMarker' => '/* testMatchWithDefaultNestedInSwitchCase1 */', 'openerOffset' => 3, 'closerOffset' => 55, ], 'switch_with_nested_match_case_2' => [ 'testMarker' => '/* testMatchWithDefaultNestedInSwitchCase2 */', 'openerOffset' => 4, 'closerOffset' => 21, ], 'switch_with_nested_match_default_case' => [ 'testMarker' => '/* testMatchWithDefaultNestedInSwitchDefault */', 'openerOffset' => 1, 'closerOffset' => 38, ], 'match_with_nested_switch_case' => [ 'testMarker' => '/* testSwitchDefaultNestedInMatchCase */', 'openerOffset' => 1, 'closerOffset' => 18, ], 'match_with_nested_switch_default_case' => [ 'testMarker' => '/* testSwitchDefaultNestedInMatchDefault */', 'openerOffset' => 1, 'closerOffset' => 20, ], ]; }//end dataSwitchCaseVersusMatch() /** * Helper function to verify that all scope related array indexes for a control structure * are set correctly. * * @param string $token The control structure token to check. * @param int $openerOffset The expected offset of the scope opener in relation to * the control structure token. * @param int $closerOffset The expected offset of the scope closer in relation to * the control structure token. * @param bool $skipScopeCloserCheck Whether to skip the scope closer check. * This should be set to "true" when testing nested arrow functions, * where the "inner" arrow function shares a scope closer with the * "outer" arrow function, as the 'scope_condition' for the scope closer * of the "inner" arrow function will point to the "outer" arrow function. * * @return void */ private function scopeTestHelper($token, $openerOffset, $closerOffset, $skipScopeCloserCheck=false) { $tokens = $this->phpcsFile->getTokens(); $tokenArray = $tokens[$token]; $tokenType = $tokenArray['type']; $expectedScopeOpener = ($token + $openerOffset); $expectedScopeCloser = ($token + $closerOffset); $this->assertArrayHasKey('scope_condition', $tokenArray, 'Scope condition is not set'); $this->assertArrayHasKey('scope_opener', $tokenArray, 'Scope opener is not set'); $this->assertArrayHasKey('scope_closer', $tokenArray, 'Scope closer is not set'); $this->assertSame($token, $tokenArray['scope_condition'], 'Scope condition is not the '.$tokenType.' token'); $this->assertSame($expectedScopeOpener, $tokenArray['scope_opener'], 'Scope opener of the '.$tokenType.' token incorrect'); $this->assertSame($expectedScopeCloser, $tokenArray['scope_closer'], 'Scope closer of the '.$tokenType.' token incorrect'); $opener = $tokenArray['scope_opener']; $this->assertArrayHasKey('scope_condition', $tokens[$opener], 'Opener scope condition is not set'); $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Opener scope opener is not set'); $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Opener scope closer is not set'); $this->assertSame($token, $tokens[$opener]['scope_condition'], 'Opener scope condition is not the '.$tokenType.' token'); $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], $tokenType.' opener scope opener token incorrect'); $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], $tokenType.' opener scope closer token incorrect'); $closer = $tokenArray['scope_closer']; $this->assertArrayHasKey('scope_condition', $tokens[$closer], 'Closer scope condition is not set'); $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Closer scope opener is not set'); $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Closer scope closer is not set'); if ($skipScopeCloserCheck === false) { $this->assertSame($token, $tokens[$closer]['scope_condition'], 'Closer scope condition is not the '.$tokenType.' token'); } $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], $tokenType.' closer scope opener token incorrect'); $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], $tokenType.' closer scope closer token incorrect'); if (($opener + 1) !== $closer) { for ($i = ($opener + 1); $i < $closer; $i++) { $this->assertArrayHasKey( $token, $tokens[$i]['conditions'], $tokenType.' condition not added for token belonging to the '.$tokenType.' structure' ); } } }//end scopeTestHelper() /** * Helper function to verify that all parenthesis related array indexes for a control structure * token are set correctly. * * @param int $token The position of the control structure token. * * @return void */ private function parenthesisTestHelper($token) { $tokens = $this->phpcsFile->getTokens(); $tokenArray = $tokens[$token]; $tokenType = $tokenArray['type']; $this->assertArrayHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is not set'); $this->assertArrayHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is not set'); $this->assertArrayHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is not set'); $this->assertSame($token, $tokenArray['parenthesis_owner'], 'Parenthesis owner is not the '.$tokenType.' token'); $opener = $tokenArray['parenthesis_opener']; $this->assertArrayHasKey('parenthesis_owner', $tokens[$opener], 'Opening parenthesis owner is not set'); $this->assertSame($token, $tokens[$opener]['parenthesis_owner'], 'Opening parenthesis owner is not the '.$tokenType.' token'); $closer = $tokenArray['parenthesis_closer']; $this->assertArrayHasKey('parenthesis_owner', $tokens[$closer], 'Closing parenthesis owner is not set'); $this->assertSame($token, $tokens[$closer]['parenthesis_owner'], 'Closing parenthesis owner is not the '.$tokenType.' token'); }//end parenthesisTestHelper() }//end class