diff --git a/src/Tokenizers/Tokenizer.php b/src/Tokenizers/Tokenizer.php index 929e5bda31..95bb44a585 100644 --- a/src/Tokenizers/Tokenizer.php +++ b/src/Tokenizers/Tokenizer.php @@ -628,8 +628,7 @@ private function createTokenMap() $curlyOpeners = []; $this->numTokens = count($this->tokens); - $openers = []; - $openOwner = null; + $openers = []; for ($i = 0; $i < $this->numTokens; $i++) { /* @@ -637,26 +636,35 @@ private function createTokenMap() */ if (isset(Tokens::$parenthesisOpeners[$this->tokens[$i]['code']]) === true) { - $this->tokens[$i]['parenthesis_opener'] = null; - $this->tokens[$i]['parenthesis_closer'] = null; - $this->tokens[$i]['parenthesis_owner'] = $i; - $openOwner = $i; + // Find the next non-empty token. + $find = Tokens::$emptyTokens; + if ($this->tokens[$i]['code'] === T_FUNCTION) { + $find[T_STRING] = T_STRING; + $find[T_BITWISE_AND] = T_BITWISE_AND; + } - if (PHP_CODESNIFFER_VERBOSITY > 1) { - StatusWriter::write("=> Found parenthesis owner at $i", (count($openers) + 1)); + for ($j = ($i + 1); isset($this->tokens[$j], $find[$this->tokens[$j]['code']]) === true; $j++); + if ($j < $this->numTokens && $this->tokens[$j]['code'] === T_OPEN_PARENTHESIS) { + $openers[] = $j; + $this->tokens[$i]['parenthesis_opener'] = $j; + $this->tokens[$i]['parenthesis_closer'] = null; + $this->tokens[$i]['parenthesis_owner'] = $i; + + $this->tokens[$j]['parenthesis_opener'] = $j; + $this->tokens[$j]['parenthesis_owner'] = $i; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + StatusWriter::write("=> Found parenthesis owner at $i", (count($openers) + 1)); + StatusWriter::write("=> Found parenthesis opener at $j for $i", count($openers)); + } + + $i = $j; } } else if ($this->tokens[$i]['code'] === T_OPEN_PARENTHESIS) { $openers[] = $i; $this->tokens[$i]['parenthesis_opener'] = $i; - if ($openOwner !== null) { - if (PHP_CODESNIFFER_VERBOSITY > 1) { - StatusWriter::write("=> Found parenthesis opener at $i for $openOwner", count($openers)); - } - $this->tokens[$openOwner]['parenthesis_opener'] = $i; - $this->tokens[$i]['parenthesis_owner'] = $openOwner; - $openOwner = null; - } else if (PHP_CODESNIFFER_VERBOSITY > 1) { + if (PHP_CODESNIFFER_VERBOSITY > 1) { StatusWriter::write("=> Found unowned parenthesis opener at $i", count($openers)); } } else if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS) { diff --git a/tests/Core/Tokenizers/Tokenizer/CreateTokenMapParenthesesTest.inc b/tests/Core/Tokenizers/Tokenizer/CreateTokenMapParenthesesTest.inc new file mode 100644 index 0000000000..b3dfd27132 --- /dev/null +++ b/tests/Core/Tokenizers/Tokenizer/CreateTokenMapParenthesesTest.inc @@ -0,0 +1,146 @@ + $v /* testForeachParenthesesCloserPlain */) {} + +/* testForeachParenthesesOwnerWithNestedArray */ +foreach /*comment*/ ( + /* testArrayParenthesesOwner */ + array('a', 'b'/* testArrayParenthesesCloser */ ) as $k => $v +/* testForeachParenthesesCloserWithNestedArray */ +) {} + +/* testForeachParenthesesOwnerWithNestedList */ +foreach ( + /* testListParenthesesOwner */ + $array as list($a, $b/* testListParenthesesCloser */) +/* testForeachParenthesesCloserWithNestedList */ +) {} + +/* testSwitchParenthesesOwner */ +switch ($foo/* testSwitchParenthesesCloser */) { + case '1'; + break; +} + +/* testWhileParenthesesOwner */ +while( $bar /* testWhileParenthesesCloser */) {} + +do { + something(); +/* testDoWhileParenthesesOwner */ +} while (true/* testDoWhileParenthesesCloser */); + +try { +/* testCatchParenthesesOwner */ +} catch (MyException | OtherException $e /* testCatchParenthesesCloser */) { +} + +/* testMatchParenthesesOwner */ +$m = match ($baz/* testMatchParenthesesCloser */) { + default => 10, +}; + +/* testFunctionParenthesesOwner */ +function name($a, $b/* testFunctionParenthesesCloser */) {} + +/* testFunctionParenthesesOwnerReturnByRef */ +function &returnByRef ($a, $b/* testFunctionParenthesesCloserReturnByRef */) {} + +class ReservedKeyword { + /* testFunctionParenthesesOwnerKeywordName */ + public function match($a, $b/* testFunctionParenthesesCloserKeywordName */) {} +} + +/* testClosureParenthesesOwner */ +$cl = function($a, $b/* testClosureParenthesesCloser */) {}; + +/* testAnonClassParenthesesOwner */ +$anon = new class($a, $b/* testAnonClassParenthesesCloser */) {}; + +/* testAnonClassNoParentheses */ +$anon = new class { + const FOO = 1; +}; + +// This snippet belongs with the testAnonClassNoParentheses case. Making sure these parentheses are not set for the anon class. +$a = ($b + 10); + +/* testArbitraryParenthesesOpener */ +$a = ($b + $c/* testArbitraryParenthesesCloser */); + +/* testFunctionCallParenthesesOpener */ +do_something($b + $c, $d, false /* testFunctionCallParenthesesCloser */); + +/* testIssetParenthesesOpener */ +$set = isset($b, $c/* testIssetParenthesesCloser */); + +/* testEmptyParenthesesOpener */ +$empty = empty($b /* testEmptyParenthesesCloser */); + +/* testUnsetParenthesesOpener */ +unset ($b, $c /* testUnsetParenthesesCloser */); + +/* testEvalParenthesesOpener */ +$eval = eval("\$str = \"$str\";"/* testEvalParenthesesCloser */); + +/* testExitParenthesesOpener */ +exit ( 101 /* testExitParenthesesCloser */); + +/* testDieParenthesesOpener */ +die ('oopsie' /* testDieParenthesesCloser */); + + +/* + * All together now, let's make things a little more interesting.... + */ + +/* testNestedOuterIfParenthesesOwner */ +if ( + /* testNestedFunctionCallAParenthesesOpener */ + array_map( + /* testNestedClosureParenthesesOwner */ + function ($a, $b /* testNestedClosureParenthesesCloser */) /* testNestedClosureUseParenthesesOpener */ + use (&$c /* testNestedClosureUseParenthesesCloser */) + { + /* testNestedForeachParenthesesOwner */ + foreach ( + /* testNestedFunctionCallBParenthesesOpener */ + array_keys( + /* testNestedFunctionCallCParenthesesOpener */ + function_call( + /* testNestedArrayAParenthesesOwner */ + array(10, 20/* testNestedArrayAParenthesesCloser */) + /* testNestedFunctionCallCParenthesesCloser */ + ) + /* testNestedFunctionCallBParenthesesCloser */ + ) + as + $v + /* testNestedForeachParenthesesCloser */ + ) {} + }, + /* testNestedArrayBParenthesesOwner */ + array( + /* testNestedListParenthesesOwner */ + 'keyA' => list($a, $b/* testNestedListParenthesesCloser */) = $array, + /* testNestedAnonClassParenthesesOwner */ + 'keyB' => new class($foo/* testNestedAnonClassParenthesesCloser */) {}, + /* testNestedArrayBParenthesesCloser */ + ) + /* testNestedFunctionCallAParenthesesCloser */ + ) +/* testNestedOuterIfParenthesesCloser */ +) {} diff --git a/tests/Core/Tokenizers/Tokenizer/CreateTokenMapParenthesesTest.php b/tests/Core/Tokenizers/Tokenizer/CreateTokenMapParenthesesTest.php new file mode 100644 index 0000000000..3022e573be --- /dev/null +++ b/tests/Core/Tokenizers/Tokenizer/CreateTokenMapParenthesesTest.php @@ -0,0 +1,313 @@ +getTargetToken($testMarker, $tokenCode); + $opener = $this->getTargetToken($testMarker, T_OPEN_PARENTHESIS); + $closer = $this->getTargetToken(str_replace('Owner', 'Closer', $testMarker), T_CLOSE_PARENTHESIS); + + $tokenType = Tokens::tokenName($tokenCode); + + $tokens = $this->phpcsFile->getTokens(); + $ownerArray = $tokens[$owner]; + $openerArray = $tokens[$opener]; + $closerArray = $tokens[$closer]; + + $this->assertArrayHasKey('parenthesis_owner', $ownerArray, $tokenType.' token does not have "parenthesis_owner" key'); + $this->assertArrayHasKey('parenthesis_opener', $ownerArray, $tokenType.' token does not have "parenthesis_opener" key'); + $this->assertArrayHasKey('parenthesis_closer', $ownerArray, $tokenType.' token does not have "parenthesis_closer" key'); + $this->assertSame($owner, $ownerArray['parenthesis_owner'], $tokenType.' token "parenthesis_owner" key set incorrectly'); + $this->assertSame($opener, $ownerArray['parenthesis_opener'], $tokenType.' token "parenthesis_opener" key set incorrectly'); + $this->assertSame($closer, $ownerArray['parenthesis_closer'], $tokenType.' token "parenthesis_closer" key set incorrectly'); + + $this->assertArrayHasKey('parenthesis_owner', $openerArray, $tokenType.' opener does not have "parenthesis_owner" key'); + $this->assertArrayHasKey('parenthesis_opener', $openerArray, $tokenType.' opener does not have "parenthesis_opener" key'); + $this->assertArrayHasKey('parenthesis_closer', $openerArray, $tokenType.' opener does not have "parenthesis_closer" key'); + $this->assertSame($owner, $openerArray['parenthesis_owner'], $tokenType.' opener "parenthesis_owner" key set incorrectly'); + $this->assertSame($opener, $openerArray['parenthesis_opener'], $tokenType.' opener "parenthesis_opener" key set incorrectly'); + $this->assertSame($closer, $openerArray['parenthesis_closer'], $tokenType.' opener "parenthesis_closer" key set incorrectly'); + + $this->assertArrayHasKey('parenthesis_owner', $closerArray, $tokenType.' closer does not have "parenthesis_owner" key'); + $this->assertArrayHasKey('parenthesis_opener', $closerArray, $tokenType.' closer does not have "parenthesis_opener" key'); + $this->assertArrayHasKey('parenthesis_closer', $closerArray, $tokenType.' closer does not have "parenthesis_closer" key'); + $this->assertSame($owner, $closerArray['parenthesis_owner'], $tokenType.' closer "parenthesis_owner" key set incorrectly'); + $this->assertSame($opener, $closerArray['parenthesis_opener'], $tokenType.' closer "parenthesis_opener" key set incorrectly'); + $this->assertSame($closer, $closerArray['parenthesis_closer'], $tokenType.' closer "parenthesis_closer" key set incorrectly'); + + }//end testParenthesesWithOwner() + + + /** + * Data provider. + * + * @return array> + */ + public static function dataParenthesesWithOwner() + { + return [ + 'declare' => [ + 'testMarker' => '/* testDeclareParenthesesOwner */', + 'tokenCode' => T_DECLARE, + ], + 'if' => [ + 'testMarker' => '/* testIfParenthesesOwner */', + 'tokenCode' => T_IF, + ], + 'elseif' => [ + 'testMarker' => '/* testElseIfParenthesesOwner */', + 'tokenCode' => T_ELSEIF, + ], + 'for' => [ + 'testMarker' => '/* testForParenthesesOwner */', + 'tokenCode' => T_FOR, + ], + 'foreach' => [ + 'testMarker' => '/* testForeachParenthesesOwnerPlain */', + 'tokenCode' => T_FOREACH, + ], + 'foreach with nested array' => [ + 'testMarker' => '/* testForeachParenthesesOwnerWithNestedArray */', + 'tokenCode' => T_FOREACH, + ], + 'array' => [ + 'testMarker' => '/* testArrayParenthesesOwner */', + 'tokenCode' => T_ARRAY, + ], + 'foreach with nested list' => [ + 'testMarker' => '/* testForeachParenthesesOwnerWithNestedList */', + 'tokenCode' => T_FOREACH, + ], + 'list' => [ + 'testMarker' => '/* testListParenthesesOwner */', + 'tokenCode' => T_LIST, + ], + 'switch' => [ + 'testMarker' => '/* testSwitchParenthesesOwner */', + 'tokenCode' => T_SWITCH, + ], + 'while' => [ + 'testMarker' => '/* testWhileParenthesesOwner */', + 'tokenCode' => T_WHILE, + ], + 'do - while' => [ + 'testMarker' => '/* testDoWhileParenthesesOwner */', + 'tokenCode' => T_WHILE, + ], + 'catch' => [ + 'testMarker' => '/* testCatchParenthesesOwner */', + 'tokenCode' => T_CATCH, + ], + 'match' => [ + 'testMarker' => '/* testMatchParenthesesOwner */', + 'tokenCode' => T_MATCH, + ], + 'function declaration' => [ + 'testMarker' => '/* testFunctionParenthesesOwner */', + 'tokenCode' => T_FUNCTION, + ], + 'function declaration return by ref' => [ + 'testMarker' => '/* testFunctionParenthesesOwnerReturnByRef */', + 'tokenCode' => T_FUNCTION, + ], + 'function declaration, keyword as function name' => [ + 'testMarker' => '/* testFunctionParenthesesOwnerKeywordName */', + 'tokenCode' => T_FUNCTION, + ], + 'closure declaration' => [ + 'testMarker' => '/* testClosureParenthesesOwner */', + 'tokenCode' => T_CLOSURE, + ], + 'anonymous class' => [ + 'testMarker' => '/* testAnonClassParenthesesOwner */', + 'tokenCode' => T_ANON_CLASS, + ], + + 'if - nested outer' => [ + 'testMarker' => '/* testNestedOuterIfParenthesesOwner */', + 'tokenCode' => T_IF, + ], + 'closure - nested' => [ + 'testMarker' => '/* testNestedClosureParenthesesOwner */', + 'tokenCode' => T_CLOSURE, + ], + 'foreach - nested' => [ + 'testMarker' => '/* testNestedForeachParenthesesOwner */', + 'tokenCode' => T_FOREACH, + ], + 'array - nested 1' => [ + 'testMarker' => '/* testNestedArrayAParenthesesOwner */', + 'tokenCode' => T_ARRAY, + ], + 'array - nested 2' => [ + 'testMarker' => '/* testNestedArrayBParenthesesOwner */', + 'tokenCode' => T_ARRAY, + ], + 'list - nested' => [ + 'testMarker' => '/* testNestedListParenthesesOwner */', + 'tokenCode' => T_LIST, + ], + 'anon class - nested' => [ + 'testMarker' => '/* testNestedAnonClassParenthesesOwner */', + 'tokenCode' => T_ANON_CLASS, + ], + ]; + + }//end dataParenthesesWithOwner() + + + /** + * Test parentheses which do *not* have an owner get the correct "parenthesis_*" token indexes set. + * + * @param string $testMarker The comment prefacing the target token. + * + * @dataProvider dataParenthesesWithoutOwner + * + * @return void + */ + public function testParenthesesWithoutOwner($testMarker) + { + $opener = $this->getTargetToken($testMarker, T_OPEN_PARENTHESIS); + $closer = $this->getTargetToken(str_replace('Opener', 'Closer', $testMarker), T_CLOSE_PARENTHESIS); + + $tokens = $this->phpcsFile->getTokens(); + $openerArray = $tokens[$opener]; + $closerArray = $tokens[$closer]; + + $this->assertArrayNotHasKey('parenthesis_owner', $openerArray, 'Opener has "parenthesis_owner" key'); + $this->assertArrayNotHasKey('parenthesis_owner', $closerArray, 'Closer has "parenthesis_owner" key'); + + $this->assertArrayHasKey('parenthesis_opener', $openerArray, 'Opener does not have "parenthesis_opener" key'); + $this->assertArrayHasKey('parenthesis_closer', $openerArray, 'Opener does not have "parenthesis_closer" key'); + $this->assertSame($opener, $openerArray['parenthesis_opener'], 'Opener "parenthesis_opener" key set incorrectly'); + $this->assertSame($closer, $openerArray['parenthesis_closer'], 'Opener "parenthesis_closer" key set incorrectly'); + + $this->assertArrayHasKey('parenthesis_opener', $closerArray, 'Closer does not have "parenthesis_opener" key'); + $this->assertArrayHasKey('parenthesis_closer', $closerArray, 'Closer does not have "parenthesis_closer" key'); + $this->assertSame($opener, $closerArray['parenthesis_opener'], 'Closer "parenthesis_opener" key set incorrectly'); + $this->assertSame($closer, $closerArray['parenthesis_closer'], 'Closer "parenthesis_closer" key set incorrectly'); + + }//end testParenthesesWithoutOwner() + + + /** + * Data provider. + * + * @return array> + */ + public static function dataParenthesesWithoutOwner() + { + return [ + 'arbitrary parentheses, not nested' => [ + 'testMarker' => '/* testArbitraryParenthesesOpener */', + ], + 'function call' => [ + 'testMarker' => '/* testFunctionCallParenthesesOpener */', + ], + 'isset' => [ + 'testMarker' => '/* testIssetParenthesesOpener */', + ], + 'empty' => [ + 'testMarker' => '/* testEmptyParenthesesOpener */', + ], + 'unset' => [ + 'testMarker' => '/* testUnsetParenthesesOpener */', + ], + 'eval' => [ + 'testMarker' => '/* testEvalParenthesesOpener */', + ], + 'exit' => [ + 'testMarker' => '/* testExitParenthesesOpener */', + ], + 'die' => [ + 'testMarker' => '/* testDieParenthesesOpener */', + ], + 'function call - nested 1' => [ + 'testMarker' => '/* testNestedFunctionCallAParenthesesOpener */', + ], + 'closure use - nested' => [ + 'testMarker' => '/* testNestedClosureUseParenthesesOpener */', + ], + 'function call - nested 2' => [ + 'testMarker' => '/* testNestedFunctionCallBParenthesesOpener */', + ], + 'function call - nested 3' => [ + 'testMarker' => '/* testNestedFunctionCallCParenthesesOpener */', + ], + ]; + + }//end dataParenthesesWithoutOwner() + + + /** + * Test parentheses owner tokens when used without parentheses (where possible) do *not* the "parenthesis_*" token indexes set. + * + * @param string $testMarker The comment prefacing the target token. + * @param int|string $tokenCode The token code to look for. + * + * @dataProvider dataParenthesesOwnerWithoutParentheses + * + * @return void + */ + public function testParenthesesOwnerWithoutParentheses($testMarker, $tokenCode) + { + $tokens = $this->phpcsFile->getTokens(); + $target = $this->getTargetToken($testMarker, $tokenCode); + $tokenArray = $tokens[$target]; + + $tokenType = Tokens::tokenName($tokenCode); + + $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, $tokenType.' token has "parenthesis_owner" key'); + $this->assertArrayNotHasKey('parenthesis_opener', $tokenArray, $tokenType.' token has "parenthesis_opener" key'); + $this->assertArrayNotHasKey('parenthesis_closer', $tokenArray, $tokenType.' token has "parenthesis_closer" key'); + + }//end testParenthesesOwnerWithoutParentheses() + + + /** + * Data provider. + * + * @return array> + */ + public static function dataParenthesesOwnerWithoutParentheses() + { + return [ + 'anonymous class without parentheses' => [ + 'testMarker' => '/* testAnonClassNoParentheses */', + 'tokenCode' => T_ANON_CLASS, + ], + ]; + + }//end dataParenthesesOwnerWithoutParentheses() + + +}//end class