* @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\Tokenizers\PHP;
use PHP_CodeSniffer\Tests\Core\Tokenizers\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 int $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