* Dariusz Rumiński * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Tests\Tokenizer; use PhpCsFixer\Tests\TestCase; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Tokenizer\TokensAnalyzer; /** * @author Dariusz Rumiński * @author Max Voloshin * @author Gregor Harlan * @author SpacePossum * * @internal * * @covers \PhpCsFixer\Tokenizer\TokensAnalyzer */ final class TokensAnalyzerTest extends TestCase { /** * @dataProvider provideGetClassyElementsCases */ public function testGetClassyElements(array $expectedElements, string $source): void { $tokens = Tokens::fromCode($source); foreach ($expectedElements as $index => $element) { $expectedElements[$index] = [ 'token' => $tokens[$index], 'type' => $element['type'], 'classIndex' => $element['classIndex'], ]; } $tokensAnalyzer = new TokensAnalyzer($tokens); static::assertSame( $expectedElements, $tokensAnalyzer->getClassyElements() ); } public function provideGetClassyElementsCases() { yield 'trait import' => [ [ 10 => [ 'type' => 'trait_import', 'classIndex' => 4, ], 19 => [ 'type' => 'trait_import', 'classIndex' => 4, ], 24 => [ 'type' => 'const', 'classIndex' => 4, ], 35 => [ 'type' => 'method', 'classIndex' => 4, ], 55 => [ 'type' => 'trait_import', 'classIndex' => 49, ], 64 => [ 'type' => 'method', 'classIndex' => 49, ], ], 'bar(); } }', ]; yield [ [ 9 => [ 'type' => 'property', 'classIndex' => 1, ], 14 => [ 'type' => 'property', 'classIndex' => 1, ], 19 => [ 'type' => 'property', 'classIndex' => 1, ], 28 => [ 'type' => 'property', 'classIndex' => 1, ], 42 => [ 'type' => 'const', 'classIndex' => 1, ], 53 => [ 'type' => 'method', 'classIndex' => 1, ], 83 => [ 'type' => 'method', 'classIndex' => 1, ], 140 => [ 'type' => 'method', 'classIndex' => 1, ], 164 => [ 'type' => 'const', 'classIndex' => 158, ], 173 => [ 'type' => 'trait_import', 'classIndex' => 158, ], ], <<<'PHP' getClassyElements(); static::assertSame( [ 11 => [ 'token' => $tokens[11], 'type' => 'property', 'classIndex' => 1, ], 19 => [ 'token' => $tokens[19], 'type' => 'property', 'classIndex' => 1, ], 26 => [ 'token' => $tokens[26], 'type' => 'property', 'classIndex' => 1, ], 41 => [ 'token' => $tokens[41], 'type' => 'property', 'classIndex' => 1, ], ], $elements ); } public function testGetClassyElementsWithAnonymousClass(): void { $source = <<<'PHP' getClassyElements(); static::assertSame( [ 9 => [ 'token' => $tokens[9], 'type' => 'property', // $A 'classIndex' => 1, ], 14 => [ 'token' => $tokens[14], 'type' => 'method', // B 'classIndex' => 1, ], 33 => [ 'token' => $tokens[33], 'type' => 'property', // $level1 'classIndex' => 26, ], 38 => [ 'token' => $tokens[38], 'type' => 'method', // XYZ 'classIndex' => 26, ], 56 => [ 'token' => $tokens[56], 'type' => 'property', // $level2 'classIndex' => 50, ], 74 => [ 'token' => $tokens[74], 'type' => 'method', // C 'classIndex' => 1, ], ], $elements ); } public function testGetClassyElementsWithMultipleAnonymousClass(): void { $source = <<<'PHP' getClassyElements(); static::assertSame( [ 9 => [ 'token' => $tokens[9], 'type' => 'method', 'classIndex' => 1, ], 27 => [ 'token' => $tokens[27], 'type' => 'method', 'classIndex' => 21, ], 44 => [ 'token' => $tokens[44], 'type' => 'method', 'classIndex' => 1, ], 64 => [ 'token' => $tokens[64], 'type' => 'method', 'classIndex' => 56, ], 82 => [ 'token' => $tokens[82], 'type' => 'method', 'classIndex' => 76, ], 100 => [ 'token' => $tokens[100], 'type' => 'method', 'classIndex' => 94, ], 118 => [ 'token' => $tokens[118], 'type' => 'method', 'classIndex' => 112, ], 139 => [ 'token' => $tokens[139], 'type' => 'method', 'classIndex' => 112, ], 170 => [ 'token' => $tokens[170], 'type' => 'method', 'classIndex' => 76, ], 188 => [ 'token' => $tokens[188], 'type' => 'method', 'classIndex' => 182, ], 206 => [ 'token' => $tokens[206], 'type' => 'method', 'classIndex' => 200, ], 242 => [ 'token' => $tokens[242], 'type' => 'method', 'classIndex' => 56, ], ], $elements ); } /** * @requires PHP 7.4 */ public function testGetClassyElements74(): void { $source = <<<'PHP' getClassyElements(); $expected = []; foreach ([11, 23, 31, 44, 51, 54, 61, 69] as $index) { $expected[$index] = [ 'token' => $tokens[$index], 'type' => 'property', 'classIndex' => 1, ]; } static::assertSame($expected, $elements); } /** * @dataProvider provideIsAnonymousClassCases */ public function testIsAnonymousClass(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $expectedValue) { static::assertSame($expectedValue, $tokensAnalyzer->isAnonymousClass($index)); } } public function provideIsAnonymousClassCases() { return [ [ ' false], ], [ ' true], ], [ ' true], ], [ ' false, 19 => true], ], [ 'a) implements B{}) extends C{};', [7 => true, 11 => true], ], [ ' false], ], ]; } /** * @dataProvider provideIsLambdaCases */ public function testIsLambda(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isLambda) { static::assertSame($isLambda, $tokensAnalyzer->isLambda($index)); } } public function provideIsLambdaCases() { return [ [ ' false], ], [ ' false], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], ]; } /** * @dataProvider provideIsLambda70Cases * @requires PHP 7.0 */ public function testIsLambda70(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $expectedValue) { static::assertSame($expectedValue, $tokensAnalyzer->isLambda($index)); } } public function provideIsLambda70Cases() { return [ [ ' true], ], [ ' false], ], ]; } /** * @dataProvider provideIsLambda74Cases * @requires PHP 7.4 */ public function testIsLambda74(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $expectedValue) { static::assertSame($expectedValue, $tokensAnalyzer->isLambda($index)); } } public function provideIsLambda74Cases() { return [ [ ' [];', [5 => true], ], [ ' [];', [5 => true], ], ]; } /** * @dataProvider provideIsLambda71Cases * @requires PHP 7.1 */ public function testIsLambda71(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $expectedValue) { static::assertSame($expectedValue, $tokensAnalyzer->isLambda($index)); } } public function provideIsLambda71Cases() { return [ [ ' true], ], [ ' false], ], [ ' true], ], [ ' true], ], [ ' false], ], ]; } /** * @dataProvider provideIsLambda80Cases * @requires PHP 8.0 */ public function testIsLambda80(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $expectedValue) { static::assertSame($expectedValue, $tokensAnalyzer->isLambda($index)); } } public function provideIsLambda80Cases() { return [ [ ' true], ], [ ' true], ], [ ' true], ], ]; } public function testIsLambdaInvalid(): void { $this->expectException(\LogicException::class); $this->expectExceptionMessage('No T_FUNCTION or T_FN at given index 0, got "T_OPEN_TAG".'); $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode('isLambda(0); } /** * @dataProvider provideIsConstantInvocationCases */ public function testIsConstantInvocation(string $source, array $expected): void { $this->doIsConstantInvocationTest($source, $expected); } public function provideIsConstantInvocationCases() { return [ [ ' true], ], [ ' true], ], [ ' false, 5 => false, 7 => true], ], [ ' true, 7 => true, 11 => true], ], [ ' true, 7 => true, 11 => true], ], [ ' true], ], [ ' true], ], [ ' true, 5 => true], ], [ ' false, 3 => true, 6 => false, 8 => true], ], [ ' true, 8 => true], ], [ ' true, 7 => false, 9 => false, 11 => true], ], [ ' BAR; }', [3 => false, 11 => true, 16 => true, 20 => true], ], [ ' true], ], [ ' false], ], [ ' false], ], [ ' false, 7 => false, 9 => false], ], [ ' false, 8 => false], ], [ ' false], ], [ ' false], ], [ ' false], ], [ ' false, 7 => false], ], [ ' false, 7 => false], ], [ ' false, 7 => false, 10 => false, 13 => false], ], [ ' false, 9 => false], ], [ ' false, 9 => false], ], [ ' false, 9 => false, 12 => false, 16 => false, 18 => false, 22 => false], ], [ ' false, 6 => false, 11 => false, 17 => false], ], [ ' false], ], [ ' false, 3 => false], ], [ ' false, 3 => false], ], [ ' false], ], [ ' false], ], [ ' false], ], [ ' true], ], [ ' false, 6 => false], ], [ ' false, 3 => true, 7 => true], ], [ ' false, 7 => false, 10 => false, 13 => false], ], [ ' false, 5 => false, 8 => false, 10 => false, 13 => false, 15 => false], ], ]; } /** * @dataProvider provideIsConstantInvocation70Cases * @requires PHP 7.0 */ public function testIsConstantInvocation70(string $source, array $expected): void { $this->doIsConstantInvocationTest($source, $expected); } public function provideIsConstantInvocation70Cases() { return [ [ ' false, 8 => false], ], [ ' false, 5 => false, 8 => false, 11 => false, 15 => false, 18 => false], ], ]; } /** * @dataProvider provideIsConstantInvocation71Cases * @requires PHP 7.1 */ public function testIsConstantInvocation71(string $source, array $expected): void { $this->doIsConstantInvocationTest($source, $expected); } public function provideIsConstantInvocation71Cases() { return [ [ ' false, 6 => false], ], [ ' false, 9 => false], ], [ ' false, 11 => false, 13 => false], ], [ ' false, 11 => false, 16 => false], ], [ ' false, 11 => false, 17 => false], ], [ ' false, 11 => false, 17 => false], ], [ ' false, 11 => false, 18 => false], ], ]; } /** * @dataProvider provideIsConstantInvocationPhp80Cases * @requires PHP 8.0 */ public function testIsConstantInvocationPhp80(string $source, array $expected): void { $this->doIsConstantInvocationTest($source, $expected); } public function provideIsConstantInvocationPhp80Cases() { yield [ 'b?->c;', [3 => false, 5 => false], ]; yield [ ' false], ]; yield [ ' false], ]; yield [ ' false, 13 => false], ]; yield [ ' false, 7 => false], ]; yield [ ' false, 9 => false], ]; } public function testIsConstantInvocationInvalid(): void { $this->expectException(\LogicException::class); $this->expectExceptionMessage('No T_STRING at given index 0, got "T_OPEN_TAG".'); $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode('isConstantInvocation(0); } /** * @requires PHP 8.0 */ public function testIsConstantInvocationForNullSafeObjectOperator(): void { $tokens = Tokens::fromCode('b?->c;'); $tokensAnalyzer = new TokensAnalyzer($tokens); foreach ($tokens as $index => $token) { if (!$token->isGivenKind(T_STRING)) { continue; } static::assertFalse($tokensAnalyzer->isConstantInvocation($index)); } } /** * @dataProvider provideIsUnarySuccessorOperatorCases */ public function testIsUnarySuccessorOperator(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isUnary) { static::assertSame($isUnary, $tokensAnalyzer->isUnarySuccessorOperator($index)); if ($isUnary) { static::assertFalse($tokensAnalyzer->isUnaryPredecessorOperator($index)); static::assertFalse($tokensAnalyzer->isBinaryOperator($index)); } } } public function provideIsUnarySuccessorOperatorCases() { return [ [ ' true], ], [ ' true], ], [ ' true], ], [ ' true, 4 => false], ], [ ' true], ], [ 'bar++;', [4 => true], ], [ '{"bar"}++;', [6 => true], ], 'array access' => [ ' true], ], 'array curly access' => [ ' true], ], ]; } /** * @dataProvider provideIsUnaryPredecessorOperatorCases */ public function testIsUnaryPredecessorOperator(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isUnary) { static::assertSame($isUnary, $tokensAnalyzer->isUnaryPredecessorOperator($index)); if ($isUnary) { static::assertFalse($tokensAnalyzer->isUnarySuccessorOperator($index)); static::assertFalse($tokensAnalyzer->isBinaryOperator($index)); } } } public function provideIsUnaryPredecessorOperatorCases() { return [ [ ' true], ], [ ' true], ], [ ' true], ], [ ' false, 5 => true], ], [ ' true, 2 => true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true, 8 => true], ], [ ' true, 11 => true, 17 => true], ], [ ' true], ], [ ' true, 6 => true], ], [ ' true], ], [ ' true], ], [ ' true], ], ]; } /** * @dataProvider provideIsBinaryOperatorCases */ public function testIsBinaryOperator(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isBinary) { static::assertSame($isBinary, $tokensAnalyzer->isBinaryOperator($index)); if ($isBinary) { static::assertFalse($tokensAnalyzer->isUnarySuccessorOperator($index)); static::assertFalse($tokensAnalyzer->isUnaryPredecessorOperator($index)); } } } public function provideIsBinaryOperatorCases() { $cases = [ [ ' true], ], [ ' true], ], [ '', [3 => true], ], [ '', [3 => true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' "c", );', [3 => true, 9 => true, 12 => false], ], [ ' true, 5 => false], ], [ ' true, 5 => false, 8 => true, 10 => false], ], [ ' true, 5 => false], ], [ ' false, 4 => true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], [ ' true], ], ]; $operators = [ '+', '-', '*', '/', '%', '<', '>', '|', '^', '&=', '&&', '||', '.=', '/=', '==', '>=', '===', '!=', '<>', '!==', '<=', 'and', 'or', 'xor', '-=', '%=', '*=', '|=', '+=', '<<', '<<=', '>>', '>>=', '^', ]; foreach ($operators as $operator) { $cases[] = [ ' true], ]; } return $cases; } /** * @dataProvider provideIsBinaryOperator70Cases * @requires PHP 7.0 */ public function testIsBinaryOperator70(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isBinary) { static::assertSame($isBinary, $tokensAnalyzer->isBinaryOperator($index)); if ($isBinary) { static::assertFalse($tokensAnalyzer->isUnarySuccessorOperator($index)); static::assertFalse($tokensAnalyzer->isUnaryPredecessorOperator($index)); } } } public function provideIsBinaryOperator70Cases() { return [ [ ' $b;', [3 => true], ], [ ' true], ], ]; } /** * @dataProvider provideIsArrayCases */ public function testIsArray(string $source, int $tokenIndex, bool $isMultiLineArray = false): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); static::assertTrue($tokensAnalyzer->isArray($tokenIndex), 'Expected to be an array.'); static::assertSame($isMultiLineArray, $tokensAnalyzer->isArrayMultiLine($tokenIndex), sprintf('Expected %sto be a multiline array', $isMultiLineArray ? '' : 'not ')); } public function provideIsArrayCases() { return [ [ ' 1); ', 2, ], [ ' 2]; ', 2, false, ], [ ' 3 ); ', 2, true, ], [ ' 4 ]; ', 2, true, ], [ ' array(5, 6, 7), 8 => new \Exception(\'Ellow\') ); ', 2, true, ], [ // mix short array syntax ' [9, 10, 11], 12 => new \Exception(\'Ellow\') ); ', 2, true, ], // Windows/Max EOL testing [ " 13);\r\n", 1, ], [ " 14,\r\n 'b' => 15\r\n );\r\n", 2, true, ], ]; } /** * @param int[] $tokenIndexes * * @dataProvider provideIsArray71Cases * @requires PHP 7.1 */ public function testIsArray71(string $source, array $tokenIndexes): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); foreach ($tokens as $index => $token) { $expect = \in_array($index, $tokenIndexes, true); static::assertSame( $expect, $tokensAnalyzer->isArray($index), sprintf('Expected %sarray, got @ %d "%s".', $expect ? '' : 'no ', $index, var_export($token, true)) ); } } public function provideIsArray71Cases() { return [ [ ' $a, "b" => $b] = $array; $c = [$d, $e] = $array[$a]; [[$a, $b], [$c, $d]] = $d; $array = []; $d = array(); ', [76, 84], ], ]; } /** * @dataProvider provideIsBinaryOperator71Cases * @requires PHP 7.1 */ public function testIsBinaryOperator71(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isBinary) { static::assertSame($isBinary, $tokensAnalyzer->isBinaryOperator($index)); if ($isBinary) { static::assertFalse($tokensAnalyzer->isUnarySuccessorOperator($index)); static::assertFalse($tokensAnalyzer->isUnaryPredecessorOperator($index)); } } } public function provideIsBinaryOperator71Cases() { return [ [ ' false], ], ]; } /** * @dataProvider provideIsBinaryOperator74Cases * @requires PHP 7.4 */ public function testIsBinaryOperator74(string $source, array $expected): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isBinary) { static::assertSame($isBinary, $tokensAnalyzer->isBinaryOperator($index)); if ($isBinary) { static::assertFalse($tokensAnalyzer->isUnarySuccessorOperator($index)); static::assertFalse($tokensAnalyzer->isUnaryPredecessorOperator($index)); } } } public function provideIsBinaryOperator74Cases() { return [ [ ' true], ], ]; } /** * @dataProvider provideArrayExceptionsCases */ public function testIsNotArray(string $source, int $tokenIndex): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); static::assertFalse($tokensAnalyzer->isArray($tokenIndex)); } /** * @dataProvider provideArrayExceptionsCases */ public function testIsMultiLineArrayException(string $source, int $tokenIndex): void { $this->expectException(\InvalidArgumentException::class); $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); $tokensAnalyzer->isArrayMultiLine($tokenIndex); } public function provideArrayExceptionsCases() { return [ ['expectException(\LogicException::class); $tokens = Tokens::fromCode('isBlockMultiline($tokens, 1); } /** * @param bool $isBlockMultiline * @param string $source * @param int $tokenIndex * * @dataProvider provideIsBlockMultilineCases */ public function testIsBlockMultiline($isBlockMultiline, $source, $tokenIndex): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); static::assertSame($isBlockMultiline, $tokensAnalyzer->isBlockMultiline($tokens, $tokenIndex)); } public static function provideIsBlockMultilineCases() { yield [ false, 'getMethodAttributes($index); static::assertSame($expected, $attributes); } public function provideGetFunctionPropertiesCases() { $defaultAttributes = [ 'visibility' => null, 'static' => false, 'abstract' => false, 'final' => false, ]; $template = ' false, 12 => false, 19 => false, 34 => false, 47 => false, 53 => false, 59 => false, 66 => false, 91 => false, 112 => true, 123 => true, 139 => true, 153 => false, 162 => true, ]; $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); foreach ($tokens as $index => $token) { if (!$token->isGivenKind(T_WHILE)) { continue; } static::assertSame( $expected[$index], $tokensAnalyzer->isWhilePartOfDoWhile($index), sprintf('Expected token at index "%d" to be detected as %sa "do-while"-loop.', $index, true === $expected[$index] ? '' : 'not ') ); } } /** * @dataProvider provideGetImportUseIndexesCases */ public function testGetImportUseIndexes(array $expected, string $input, bool $perNamespace = false): void { $tokens = Tokens::fromCode($input); $tokensAnalyzer = new TokensAnalyzer($tokens); static::assertSame($expected, $tokensAnalyzer->getImportUseIndexes($perNamespace)); } public function provideGetImportUseIndexesCases() { return [ [ [1, 8], '', true, ], [ [1, 8], '', ], [ [7, 22], 'getImportUseIndexes($perNamespace)); } public function provideGetImportUseIndexesPHP70Cases() { return [ [ [1, 22, 41], 'getImportUseIndexes($perNamespace)); } public function provideGetImportUseIndexesPHP72Cases() { return [ [ [1, 23, 43], 'getClassyElements(); static::assertSame([ 13 => [ 'token' => $tokens[13], 'type' => 'method', // setUp 'classIndex' => 1, ], 46 => [ 'token' => $tokens[46], 'type' => 'method', // testSomethingWithMoney 'classIndex' => 1, ], 100 => [ 'token' => $tokens[100], // const A 'type' => 'const', 'classIndex' => 87, ], 115 => [ 'token' => $tokens[115], // const B 'type' => 'const', 'classIndex' => 65, ], 124 => [ 'token' => $tokens[124], 'type' => 'method', // foo 'classIndex' => 65, // $a ], 143 => [ 'token' => $tokens[143], // const AA 'type' => 'const', 'classIndex' => 138, ], 161 => [ 'token' => $tokens[161], // const AB 'type' => 'const', 'classIndex' => 158, ], ], $elements); } /** * @dataProvider provideIsSuperGlobalCases */ public function testIsSuperGlobal(bool $expected, string $source, int $index): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); static::assertSame($expected, $tokensAnalyzer->isSuperGlobal($index)); } public function provideIsSuperGlobalCases() { $superNames = [ '$_COOKIE', '$_ENV', '$_FILES', '$_GET', '$_POST', '$_REQUEST', '$_SERVER', '$_SESSION', '$GLOBALS', ]; $cases = []; foreach ($superNames as $superName) { $cases[] = [ true, sprintf('$b(); }; // $_SERVER', ' countTokenKind(T_STRING), $expected, 'All T_STRING tokens must be tested' ); $tokensAnalyzer = new TokensAnalyzer($tokens); foreach ($expected as $index => $expectedValue) { static::assertSame( $expectedValue, $tokensAnalyzer->isConstantInvocation($index), sprintf('Token at index '.$index.' should match the expected value (%s).', $expectedValue ? 'true' : 'false') ); } } }