Skip to content

Commit

Permalink
Merge pull request squizlabs#645 from PHPCSStandards/feature/tokenize…
Browse files Browse the repository at this point in the history
…r-php-yield-from-add-tests+minor-bug-fix

Tokenizer/PHP: add tests for tokenization of yield and yield from + minor bug fix
  • Loading branch information
jrfnl authored Oct 25, 2024
2 parents de3ca90 + df8bfe9 commit aafc72f
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 46 deletions.
67 changes: 21 additions & 46 deletions src/Tokenizers/PHP.php
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,27 @@ protected function tokenize($string)
echo PHP_EOL;
}

/*
Before PHP 5.5, the yield keyword was tokenized as
T_STRING. So look for and change this token in
earlier versions.
*/

if (PHP_VERSION_ID < 50500
&& $tokenIsArray === true
&& $token[0] === T_STRING
&& strtolower($token[1]) === 'yield'
&& isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
) {
// Could still be a context sensitive keyword or "yield from" and potentially multi-line,
// so adjust the token stack in place.
$token[0] = T_YIELD;

if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo "\t\t* token $stackPtr changed from T_STRING to T_YIELD".PHP_EOL;
}
}

/*
Tokenize context sensitive keyword as string when it should be string.
*/
Expand Down Expand Up @@ -1499,7 +1520,6 @@ protected function tokenize($string)
*/

if (PHP_VERSION_ID < 70000
&& PHP_VERSION_ID >= 50500
&& $tokenIsArray === true
&& $token[0] === T_YIELD
&& isset($tokens[($stackPtr + 1)]) === true
Expand All @@ -1524,51 +1544,6 @@ protected function tokenize($string)
$tokens[($stackPtr + 2)] = null;
}

/*
Before PHP 5.5, the yield keyword was tokenized as
T_STRING. So look for and change this token in
earlier versions.
Checks also if it is just "yield" or "yield from".
*/

if (PHP_VERSION_ID < 50500
&& $tokenIsArray === true
&& $token[0] === T_STRING
&& strtolower($token[1]) === 'yield'
&& isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
) {
if (isset($tokens[($stackPtr + 1)]) === true
&& isset($tokens[($stackPtr + 2)]) === true
&& $tokens[($stackPtr + 1)][0] === T_WHITESPACE
&& $tokens[($stackPtr + 2)][0] === T_STRING
&& strtolower($tokens[($stackPtr + 2)][1]) === 'from'
) {
// Could be multi-line, so just just the token stack.
$token[0] = T_YIELD_FROM;
$token[1] .= $tokens[($stackPtr + 1)][1].$tokens[($stackPtr + 2)][1];

if (PHP_CODESNIFFER_VERBOSITY > 1) {
for ($i = ($stackPtr + 1); $i <= ($stackPtr + 2); $i++) {
$type = Tokens::tokenName($tokens[$i][0]);
$content = Common::prepareForOutput($tokens[$i][1]);
echo "\t\t* token $i merged into T_YIELD_FROM; was: $type => $content".PHP_EOL;
}
}

$tokens[($stackPtr + 1)] = null;
$tokens[($stackPtr + 2)] = null;
} else {
$newToken = [];
$newToken['code'] = T_YIELD;
$newToken['type'] = 'T_YIELD';
$newToken['content'] = $token[1];
$finalTokens[$newStackPtr] = $newToken;

$newStackPtr++;
continue;
}//end if
}//end if

/*
Before PHP 5.6, the ... operator was tokenized as three
T_STRING_CONCAT tokens in a row. So look for and combine
Expand Down
52 changes: 52 additions & 0 deletions tests/Core/Tokenizers/PHP/YieldTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

function generator()
{
/* testYield */
yield 1;

/* testYieldFollowedByComment */
YIELD/*comment*/ 2;

/* testYieldFrom */
yield from gen2();

/* testYieldFromWithExtraSpacesBetween */
Yield From gen2();

/* testYieldFromWithTabBetween */
yield from gen2();

/* testYieldFromSplitByNewLines */
yield

FROM
gen2();
}

/* testYieldUsedAsClassName */
class Yield {
/* testYieldUsedAsClassConstantName */
const Type YIELD = 'foo';

/* testYieldUsedAsMethodName */
public function yield() {
/* testYieldUsedAsPropertyName1 */
echo $obj->yield;

/* testYieldUsedAsPropertyName2 */
echo $obj?->yield();

/* testYieldUsedForClassConstantAccess1 */
echo MyClass::YIELD;
/* testFromUsedForClassConstantAccess1 */
echo MyClass::FROM;
}

/* testYieldUsedAsMethodNameReturnByRef */
public function &yield() {}
}

function myGen() {
/* testYieldLiveCoding */
yield
241 changes: 241 additions & 0 deletions tests/Core/Tokenizers/PHP/YieldTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
<?php
/**
* Tests the tokenization of the `yield` and `yield from` keywords.
*
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 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;

/**
* Tests the tokenization of the `yield` and `yield from` keywords.
*
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
*/
final class YieldTest extends AbstractTokenizerTestCase
{


/**
* Test that the yield keyword is tokenized as such.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
*
* @dataProvider dataYieldKeyword
*
* @return void
*/
public function testYieldKeyword($testMarker)
{
$tokens = $this->phpcsFile->getTokens();
$target = $this->getTargetToken($testMarker, [T_YIELD, T_YIELD_FROM, T_STRING]);
$tokenArray = $tokens[$target];

$this->assertSame(T_YIELD, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_YIELD (code)');

// This assertion would fail on PHP 5.4 with PHPUnit 4 as PHPUnit polyfills the `T_YIELD` token too, but
// with a different value, which causes the token 'type' to be set to `UNKNOWN`.
// This issue _only_ occurs when running the tests, not when running PHPCS outside of a test situation.
// The PHPUnit polyfilled token is declared in the PHP_CodeCoverage_Report_HTML_Renderer_File class
// in vendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/File.php.
if (PHP_VERSION_ID >= 50500) {
$this->assertSame('T_YIELD', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_YIELD (type)');
}

}//end testYieldKeyword()


/**
* Data provider.
*
* @see testYieldKeyword()
*
* @return array<string, array<string>>
*/
public static function dataYieldKeyword()
{
return [
'yield' => ['/* testYield */'],
'yield followed by comment' => ['/* testYieldFollowedByComment */'],
'yield at end of file, live coding' => ['/* testYieldLiveCoding */'],
];

}//end dataYieldKeyword()


/**
* Test that the yield from keyword is tokenized as a single token when it in on a single line
* and only has whitespace between the words.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
* @param string $content Optional. The test token content to search for.
* Defaults to null.
*
* @dataProvider dataYieldFromKeywordSingleToken
*
* @return void
*/
public function testYieldFromKeywordSingleToken($testMarker, $content=null)
{
$tokens = $this->phpcsFile->getTokens();
$target = $this->getTargetToken($testMarker, [T_YIELD, T_YIELD_FROM, T_STRING], $content);
$tokenArray = $tokens[$target];

$this->assertSame(T_YIELD_FROM, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_YIELD_FROM (code)');
$this->assertSame('T_YIELD_FROM', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_YIELD_FROM (type)');

}//end testYieldFromKeywordSingleToken()


/**
* Data provider.
*
* @see testYieldFromKeywordSingleToken()
*
* @return array<string, array<string>>
*/
public static function dataYieldFromKeywordSingleToken()
{
return [
'yield from' => [
'testMarker' => '/* testYieldFrom */',
],
'yield from with extra space between' => [
'testMarker' => '/* testYieldFromWithExtraSpacesBetween */',
],
'yield from with tab between' => [
'testMarker' => '/* testYieldFromWithTabBetween */',
],
];

}//end dataYieldFromKeywordSingleToken()


/**
* Test that the yield from keyword is tokenized as a single token when it in on a single line
* and only has whitespace between the words.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
* @param array<array<string, string>> $expectedTokens The tokenization expected.
*
* @dataProvider dataYieldFromKeywordMultiToken
*
* @return void
*/
public function testYieldFromKeywordMultiToken($testMarker, $expectedTokens)
{
$tokens = $this->phpcsFile->getTokens();
$target = $this->getTargetToken($testMarker, [T_YIELD, T_YIELD_FROM, T_STRING]);

foreach ($expectedTokens as $nr => $tokenInfo) {
$this->assertSame(
constant($tokenInfo['type']),
$tokens[$target]['code'],
'Token tokenized as '.Tokens::tokenName($tokens[$target]['code']).', not '.$tokenInfo['type'].' (code)'
);
$this->assertSame(
$tokenInfo['type'],
$tokens[$target]['type'],
'Token tokenized as '.$tokens[$target]['type'].', not '.$tokenInfo['type'].' (type)'
);
$this->assertSame(
$tokenInfo['content'],
$tokens[$target]['content'],
'Content of token '.($nr + 1).' ('.$tokens[$target]['type'].') does not match expectations'
);

++$target;
}

}//end testYieldFromKeywordMultiToken()


/**
* Data provider.
*
* @see testYieldFromKeywordMultiToken()
*
* @return array<string, array<string, string|array<array<string, string>>>>
*/
public static function dataYieldFromKeywordMultiToken()
{
return [
'yield from with new line' => [
'testMarker' => '/* testYieldFromSplitByNewLines */',
'expectedTokens' => [
[
'type' => 'T_YIELD_FROM',
'content' => 'yield
',
],
[
'type' => 'T_YIELD_FROM',
'content' => '
',
],
[
'type' => 'T_YIELD_FROM',
'content' => ' FROM',
],
[
'type' => 'T_WHITESPACE',
'content' => '
',
],
],
],
];

}//end dataYieldFromKeywordMultiToken()


/**
* Test that 'yield' or 'from' when not used as the reserved keyword are tokenized as `T_STRING`.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
*
* @dataProvider dataYieldNonKeyword
*
* @return void
*/
public function testYieldNonKeyword($testMarker)
{
$tokens = $this->phpcsFile->getTokens();
$target = $this->getTargetToken($testMarker, [T_YIELD, T_YIELD_FROM, T_STRING]);
$tokenArray = $tokens[$target];

$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)');

}//end testYieldNonKeyword()


/**
* Data provider.
*
* @see testYieldNonKeyword()
*
* @return array<string, array<string>>
*/
public static function dataYieldNonKeyword()
{
return [
'yield used as class name' => ['/* testYieldUsedAsClassName */'],
'yield used as class constant name' => ['/* testYieldUsedAsClassConstantName */'],
'yield used as method name' => ['/* testYieldUsedAsMethodName */'],
'yield used as property access 1' => ['/* testYieldUsedAsPropertyName1 */'],
'yield used as property access 2' => ['/* testYieldUsedAsPropertyName2 */'],
'yield used as class constant access' => ['/* testYieldUsedForClassConstantAccess1 */'],
'from used as class constant access' => ['/* testFromUsedForClassConstantAccess1 */'],
'yield used as method name with ref' => ['/* testYieldUsedAsMethodNameReturnByRef */'],
];

}//end dataYieldNonKeyword()


}//end class

0 comments on commit aafc72f

Please sign in to comment.