Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Hotfix] PHP Tokenizer: improve arrow function backfill consistency #2860

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/Files/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -1254,9 +1254,7 @@ public function getDeclarationName($stackPtr)

$content = null;
for ($i = $stackPtr; $i < $this->numTokens; $i++) {
if ($this->tokens[$i]['code'] === T_STRING
|| $this->tokens[$i]['code'] === T_FN
) {
if ($this->tokens[$i]['code'] === T_STRING) {
$content = $this->tokens[$i]['content'];
break;
}
Expand Down
4 changes: 1 addition & 3 deletions src/Sniffs/AbstractPatternSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -614,9 +614,7 @@ protected function processPattern($patternInfo, File $phpcsFile, $stackPtr)
$stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
}//end if
} else if ($pattern[$i]['type'] === 'string') {
if ($tokens[$stackPtr]['code'] !== T_STRING
&& $tokens[$stackPtr]['code'] !== T_FN
) {
if ($tokens[$stackPtr]['code'] !== T_STRING) {
$hasError = true;
}

Expand Down
8 changes: 7 additions & 1 deletion src/Tokenizers/PHP.php
Original file line number Diff line number Diff line change
Expand Up @@ -1894,7 +1894,7 @@ protected function processAdditional()
$this->tokens[$i]['scope_closer'] = $scopeCloser;
$this->tokens[$i]['parenthesis_owner'] = $i;
$this->tokens[$i]['parenthesis_opener'] = $x;
$this->tokens[$i]['parenthesis_closer'] = $this->tokens[$x]['parenthesis_closer'];
$this->tokens[$i]['parenthesis_closer'] = $closer;

$this->tokens[$arrow]['code'] = T_FN_ARROW;
$this->tokens[$arrow]['type'] = 'T_FN_ARROW';
Expand All @@ -1918,6 +1918,12 @@ protected function processAdditional()
}//end if
}//end if
}//end if

// If after all that, the extra tokens are not set, this is not an arrow function.
if (isset($this->tokens[$i]['scope_closer']) === false) {
$this->tokens[$i]['code'] = T_STRING;
$this->tokens[$i]['type'] = 'T_STRING';
}
} else if ($this->tokens[$i]['code'] === T_OPEN_SQUARE_BRACKET) {
if (isset($this->tokens[$i]['bracket_closer']) === false) {
continue;
Expand Down
43 changes: 43 additions & 0 deletions tests/Core/Tokenizer/BackfillFnTokenTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,49 @@ fn(array $a) : array => $a;
/* testTernary */
$fn = fn($a) => $a ? /* testTernaryThen */ fn() : string => 'a' : /* testTernaryElse */ fn() : string => 'b';

/* testConstantDeclaration */
const FN = 'a';

class Foo {
/* testStaticMethodName */
public static function fn($param) {
/* testNestedInMethod */
$fn = fn($c) => $callable($factory($c), $c);
}

public function foo() {
/* testPropertyAssignment */
$this->fn = 'a';
}
}

$anon = new class() {
/* testAnonClassMethodName */
protected function fn($param) {
}
}

/* testNonArrowStaticMethodCall */
$a = Foo::fn($param);

/* testNonArrowStaticMethodCallWithChaining */
$a = Foo::fn($param)->another();

/* testNonArrowStaticConstant */
$a = MyClass::FN;

/* testNonArrowStaticConstantDeref */
$a = MyClass::FN[$a];

/* testNonArrowObjectMethodCall */
$a = $obj->fn($param);

/* testNonArrowNamespacedFunctionCall */
$a = MyNS\Sub\fn($param);

/* testNonArrowNamespaceOperatorFunctionCall */
$a = namespace\fn($param);

/* testLiveCoding */
// Intentional parse error. This has to be the last test in the file.
$fn = fn
102 changes: 69 additions & 33 deletions tests/Core/Tokenizer/BackfillFnTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,28 +101,6 @@ public function testComments()
}//end testComments()


/**
* Test a function called fn.
*
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
*
* @return void
*/
public function testFunctionName()
{
$tokens = self::$phpcsFile->getTokens();

$token = $this->getTargetToken('/* testFunctionName */', T_FN);
$this->assertFalse(array_key_exists('scope_condition', $tokens[$token]), 'Scope condition is set');
$this->assertFalse(array_key_exists('scope_opener', $tokens[$token]), 'Scope opener is set');
$this->assertFalse(array_key_exists('scope_closer', $tokens[$token]), 'Scope closer is set');
$this->assertFalse(array_key_exists('parenthesis_owner', $tokens[$token]), 'Parenthesis owner is set');
$this->assertFalse(array_key_exists('parenthesis_opener', $tokens[$token]), 'Parenthesis opener is set');
$this->assertFalse(array_key_exists('parenthesis_closer', $tokens[$token]), 'Parenthesis closer is set');

}//end testFunctionName()


/**
* Test nested arrow functions.
*
Expand Down Expand Up @@ -553,27 +531,85 @@ public function testTernary()


/**
* Test that the backfill presumes T_FN during live coding, but doesn't set the additional index keys.
* Test arrow function nested within a method declaration.
*
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
*
* @return void
*/
public function testLiveCoding()
public function testNestedInMethod()
{
$tokens = self::$phpcsFile->getTokens();

$token = $this->getTargetToken('/* testNestedInMethod */', T_FN);
$this->backfillHelper($token);

$this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
$this->assertSame($tokens[$token]['scope_closer'], ($token + 17), 'Scope closer is not the semicolon token');

$opener = $tokens[$token]['scope_opener'];
$this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
$this->assertSame($tokens[$opener]['scope_closer'], ($token + 17), 'Opener scope closer is not the semicolon token');

$closer = $tokens[$token]['scope_opener'];
$this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
$this->assertSame($tokens[$closer]['scope_closer'], ($token + 17), 'Closer scope closer is not the semicolon token');

}//end testNestedInMethod()


/**
* Verify that "fn" keywords which are not arrow functions get tokenized as T_STRING and don't
* have the extra token array indexes.
*
* @param string $testMarker The comment prefacing the target token.
*
* @dataProvider dataNotAnArrowFunction
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
*
* @return void
*/
public function testNotAnArrowFunction($testMarker)
{
$tokens = self::$phpcsFile->getTokens();

$token = $this->getTargetToken('/* testLiveCoding */', [T_STRING, T_FN]);
$this->assertSame($tokens[$token]['code'], T_FN, 'Token not tokenized as T_FN');
$token = $this->getTargetToken('/* testFunctionName */', [T_STRING, T_FN], 'fn');
$tokenArray = $tokens[$token];

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

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

$this->assertArrayNotHasKey('scope_condition', $tokens[$token], 'Scope condition is set');
$this->assertArrayNotHasKey('scope_opener', $tokens[$token], 'Scope opener is set');
$this->assertArrayNotHasKey('scope_closer', $tokens[$token], 'Scope closer is set');
$this->assertArrayNotHasKey('parenthesis_owner', $tokens[$token], 'Parenthesis owner is set');
$this->assertArrayNotHasKey('parenthesis_opener', $tokens[$token], 'Parenthesis opener is set');
$this->assertArrayNotHasKey('parenthesis_closer', $tokens[$token], 'Parenthesis closer is set');
}//end testNotAnArrowFunction()


/**
* Data provider.
*
* @see testNotAnArrowFunction()
*
* @return array
*/
public function dataNotAnArrowFunction()
{
return [
['/* testFunctionName */'],
['/* testStaticMethodName */'],
['/* testAnonClassMethodName */'],
['/* testNonArrowStaticMethodCall */'],
['/* testNonArrowStaticMethodCallWithChaining */'],
['/* testNonArrowObjectMethodCall */'],
['/* testNonArrowNamespacedFunctionCall */'],
['/* testNonArrowNamespaceOperatorFunctionCall */'],
['/* testLiveCoding */'],
];

}//end testLiveCoding()
}//end dataNotAnArrowFunction()


/**
Expand Down