Skip to content

Commit

Permalink
PHPCS 4.x | Tokenizer: fix stray "parenthesis_..." indexes being set
Browse files Browse the repository at this point in the history
Commit 08824f3 added support for `T_USE` tokens for closures being parentheses owners.

However, the net effect was that *every* `T_USE` token - including import/trait use statement `T_USE` tokens, would have the `parenthesis_owner`, `parenthesis_opener` and `parenthesis_closer` indexes set.

For closure `use` statements, those were set correctly.

However, for import/trait `use` statements, the `parenthesis_owner` would point to the `T_USE` keyword which doesn't have parentheses and the `parenthesis_opener` and `parenthesis_closer`  indexes would be `null` in most cases, but they would still be set.

Also, in some cases, the `parenthesis_opener` and `parenthesis_closer`  indexes for import/trait `use` statements would incorrectly be set if to arbitrary, unrelated parenthesis, if no open curly or open square bracket was encountered between the `use` statement and the next set of parenthesis.

That makes the `parenthesis_...` indexes harder and less intuitive to work with as any of the `parenthesis_...` indexes could be set, even when the token has no parenthesis.

I've fixed this now by:
* Not just resetting the `$openOwner` variable when we know there won't be any parenthesis, but by also resetting the `parenthesis_...` indexes of the incorrectly set owner.
* Doing the reset on more tokens, including doing it on a `T_OPEN_USE_GROUP` token, as the `tokenize()` method has already run, so group use tokens have already been retokenized to their dedicated token.

I've also added a few `continue` statements to skip the rest of the code within the loop when we know in advance none of the other conditions would match anyway.

Includes adding dedicated unit tests verifying the correct setting of the `parenthesis_...` indexes.
  • Loading branch information
jrfnl committed May 7, 2021
1 parent 014c3d0 commit 44fba8d
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 8 deletions.
28 changes: 20 additions & 8 deletions src/Tokenizers/Tokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,8 @@ private function createTokenMap()
if (PHP_CODESNIFFER_VERBOSITY > 1) {
Common::printStatusMessage("=> Found parenthesis owner at $i", (count($openers) + 1));
}

continue;
} else if ($this->tokens[$i]['code'] === T_OPEN_PARENTHESIS) {
$openers[] = $i;
$this->tokens[$i]['parenthesis_opener'] = $i;
Expand All @@ -665,6 +667,8 @@ private function createTokenMap()
} else if (PHP_CODESNIFFER_VERBOSITY > 1) {
Common::printStatusMessage("=> Found unowned parenthesis opener at $i", count($openers));
}

continue;
} else if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
// Did we set an owner for this set of parenthesis?
$numOpeners = count($openers);
Expand Down Expand Up @@ -721,6 +725,22 @@ private function createTokenMap()
echo "=> Found unowned attribute closer at $i for $opener".PHP_EOL;
}
}//end if

continue;
} else if ($openOwner !== null
&& ($this->tokens[$i]['code'] === T_OPEN_CURLY_BRACKET
|| $this->tokens[$i]['code'] === T_OPEN_SQUARE_BRACKET
|| $this->tokens[$i]['code'] === T_OPEN_USE_GROUP
|| $this->tokens[$i]['code'] === T_SEMICOLON
|| $this->tokens[$i]['code'] === T_CLOSE_TAG)
) {
// This must have been a T_USE for an import/trait use statement, not a closure.
unset(
$this->tokens[$openOwner]['parenthesis_opener'],
$this->tokens[$openOwner]['parenthesis_closer'],
$this->tokens[$openOwner]['parenthesis_owner']
);
$openOwner = null;
}//end if

/*
Expand All @@ -734,10 +754,6 @@ private function createTokenMap()
if (PHP_CODESNIFFER_VERBOSITY > 1) {
Common::printStatusMessage("=> Found square bracket opener at $i", (count($squareOpeners) + count($curlyOpeners)));
}

if ($openOwner !== null) {
$openOwner = null;
}
break;
case T_OPEN_CURLY_BRACKET:
if (isset($this->tokens[$i]['scope_closer']) === false) {
Expand All @@ -747,10 +763,6 @@ private function createTokenMap()
Common::printStatusMessage("=> Found curly bracket opener at $i", (count($squareOpeners) + count($curlyOpeners)));
}
}

if ($openOwner !== null) {
$openOwner = null;
}
break;
case T_CLOSE_SQUARE_BRACKET:
if (empty($squareOpeners) === false) {
Expand Down
36 changes: 36 additions & 0 deletions tests/Core/Tokenizer/UseParenthesisOwnerTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/* testUseImportSimple */
use Imported\ClassName;

$a = (1 + 2);

/* testUseImportGroup */
use Imported\Package\{
Classname,
};

$a = (1 + 2);

$closure = function($param) /* testClosureUse */ use ($var) {};

class ClassUsingTrait {
/* testUseTrait */
use MyTopLevelTrait { oldfunction as Foo; }

public function functionName() {
$closure = function($param) /* testClosureUseNestedInClass */ use ($var) {} ?>
<?php

$anon_class = new class($param) {
/* testUseTraitInNestedAnonClass */
use MyNestedTrait { oldfunction as Foo; }
};

return $anon_class('foo');
}
}

// Intentional parse error. Live coding. This has to be the last test in the file.
/* testLiveCoding */
use
189 changes: 189 additions & 0 deletions tests/Core/Tokenizer/UseParenthesisOwnerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php
/**
* Tests the adding of the "parenthesis" keys to closure use tokens.
*
* @author Juliette Reinders Folmer <[email protected]>
* @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/

namespace PHP_CodeSniffer\Tests\Core\Tokenizer;

use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;

class UseParenthesisOwnerTest extends AbstractMethodUnitTest
{


/**
* Test that a non-closure use token does not get assigned the parenthesis_... indexes.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
*
* @dataProvider dataUseNotClosure
* @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
*
* @return void
*/
public function testUseNotClosure($testMarker)
{
$tokens = self::$phpcsFile->getTokens();

$use = $this->getTargetToken($testMarker, T_USE);
$this->assertArrayNotHasKey('parenthesis_owner', $tokens[$use]);
$this->assertArrayNotHasKey('parenthesis_opener', $tokens[$use]);
$this->assertArrayNotHasKey('parenthesis_closer', $tokens[$use]);

}//end testUseNotClosure()


/**
* Test that the next open/close parenthesis after a non-closure use token
* do not get assigned the use keyword as a parenthesis owner.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
* @param int|null $expectedOwnerCode Optional. If an owner is expected for the parentheses, the token
* constant with is expected as the 'code'.
* If left at the default (null), the parentheses will be tested to
* *not* have an owner.
*
* @dataProvider dataUseNotClosure
* @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
*
* @return void
*/
public function testUseNotClosureNextOpenClose($testMarker, $expectedOwnerCode=null)
{
$tokens = self::$phpcsFile->getTokens();
$opener = $this->getTargetToken($testMarker, T_OPEN_PARENTHESIS);
$closer = $this->getTargetToken($testMarker, T_CLOSE_PARENTHESIS);

$this->assertArrayHasKey('parenthesis_opener', $tokens[$opener]);
$this->assertArrayHasKey('parenthesis_closer', $tokens[$opener]);
$this->assertSame($opener, $tokens[$opener]['parenthesis_opener']);
$this->assertSame($closer, $tokens[$opener]['parenthesis_closer']);

$this->assertArrayHasKey('parenthesis_opener', $tokens[$closer]);
$this->assertArrayHasKey('parenthesis_closer', $tokens[$closer]);
$this->assertSame($opener, $tokens[$closer]['parenthesis_opener']);
$this->assertSame($closer, $tokens[$closer]['parenthesis_closer']);

if ($expectedOwnerCode === null) {
$this->assertArrayNotHasKey('parenthesis_owner', $tokens[$opener]);
$this->assertArrayNotHasKey('parenthesis_owner', $tokens[$closer]);
} else {
$this->assertArrayHasKey('parenthesis_owner', $tokens[$opener]);
$this->assertArrayHasKey('parenthesis_owner', $tokens[$closer]);
$this->assertSame($expectedOwnerCode, $tokens[$tokens[$opener]['parenthesis_owner']]['code']);
$this->assertSame($expectedOwnerCode, $tokens[$tokens[$closer]['parenthesis_owner']]['code']);
}

}//end testUseNotClosureNextOpenClose()


/**
* Data provider.
*
* @see testUseNotClosure()
* @see testUseNotClosureNextOpenClose()
*
* @return array
*/
public function dataUseNotClosure()
{
return [
['/* testUseImportSimple */'],
['/* testUseImportGroup */'],
[
'/* testUseTrait */',
T_FUNCTION,
],
['/* testUseTraitInNestedAnonClass */'],
];

}//end dataUseNotClosure()


/**
* Test that a closure use token gets assigned a parenthesis owner, opener and closer;
* and that the opener/closer get the closure use token assigned as owner.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
*
* @dataProvider dataClosureUse
* @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
*
* @return void
*/
public function testClosureUse($testMarker)
{
$tokens = self::$phpcsFile->getTokens();
$use = $this->getTargetToken($testMarker, T_USE);
$opener = $this->getTargetToken($testMarker, T_OPEN_PARENTHESIS);
$closer = $this->getTargetToken($testMarker, T_CLOSE_PARENTHESIS);

$this->assertArrayHasKey('parenthesis_owner', $tokens[$use]);
$this->assertArrayHasKey('parenthesis_opener', $tokens[$use]);
$this->assertArrayHasKey('parenthesis_closer', $tokens[$use]);
$this->assertSame($use, $tokens[$use]['parenthesis_owner']);
$this->assertSame($opener, $tokens[$use]['parenthesis_opener']);
$this->assertSame($closer, $tokens[$use]['parenthesis_closer']);

$this->assertArrayHasKey('parenthesis_owner', $tokens[$opener]);
$this->assertArrayHasKey('parenthesis_opener', $tokens[$opener]);
$this->assertArrayHasKey('parenthesis_closer', $tokens[$opener]);
$this->assertSame($use, $tokens[$opener]['parenthesis_owner']);
$this->assertSame($opener, $tokens[$opener]['parenthesis_opener']);
$this->assertSame($closer, $tokens[$opener]['parenthesis_closer']);

$this->assertArrayHasKey('parenthesis_owner', $tokens[$closer]);
$this->assertArrayHasKey('parenthesis_opener', $tokens[$closer]);
$this->assertArrayHasKey('parenthesis_closer', $tokens[$closer]);
$this->assertSame($use, $tokens[$closer]['parenthesis_owner']);
$this->assertSame($opener, $tokens[$closer]['parenthesis_opener']);
$this->assertSame($closer, $tokens[$closer]['parenthesis_closer']);

}//end testClosureUse()


/**
* Data provider.
*
* @see testClosureUse()
*
* @return array
*/
public function dataClosureUse()
{
return [
['/* testClosureUse */'],
['/* testClosureUseNestedInClass */'],
];

}//end dataClosureUse()


/**
* Test (and document) the behaviour of the parentheses setting during live coding, when a
* `use` token is encountered at the very end of a file.
*
* @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
*
* @return void
*/
public function testLiveCoding()
{
$tokens = self::$phpcsFile->getTokens();
$use = $this->getTargetToken('/* testLiveCoding */', T_USE);

$this->assertArrayHasKey('parenthesis_owner', $tokens[$use]);
$this->assertArrayHasKey('parenthesis_opener', $tokens[$use]);
$this->assertArrayHasKey('parenthesis_closer', $tokens[$use]);
$this->assertSame($use, $tokens[$use]['parenthesis_owner']);
$this->assertNull($tokens[$use]['parenthesis_opener']);
$this->assertNull($tokens[$use]['parenthesis_closer']);

}//end testLiveCoding()


}//end class

0 comments on commit 44fba8d

Please sign in to comment.