* 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 * * @internal * * @covers \PhpCsFixer\Tokenizer\TokensAnalyzer */ final class TokensAnalyzerTest extends TestCase { /** * @param array $expectedElements * * @dataProvider provideGetClassyElementsCases */ public function testGetClassyElements(array $expectedElements, string $source): void { $tokens = Tokens::fromCode($source); array_walk( $expectedElements, static function (array &$element, int $index) use ($tokens): void { $element['token'] = $tokens[$index]; ksort($element); } ); $tokensAnalyzer = new TokensAnalyzer($tokens); self::assertSame( $expectedElements, $tokensAnalyzer->getClassyElements() ); } public static function provideGetClassyElementsCases(): iterable { yield 'trait import' => [ [ 10 => [ 'classIndex' => 4, 'type' => 'trait_import', ], 19 => [ 'classIndex' => 4, 'type' => 'trait_import', ], 24 => [ 'classIndex' => 4, 'type' => 'const', ], 35 => [ 'classIndex' => 4, 'type' => 'method', ], 55 => [ 'classIndex' => 49, 'type' => 'trait_import', ], 64 => [ 'classIndex' => 49, 'type' => 'method', ], ], 'bar(); } }', ]; yield [ [ 9 => [ 'classIndex' => 1, 'type' => 'property', ], 14 => [ 'classIndex' => 1, 'type' => 'property', ], 19 => [ 'classIndex' => 1, 'type' => 'property', ], 28 => [ 'classIndex' => 1, 'type' => 'property', ], 42 => [ 'classIndex' => 1, 'type' => 'const', ], 53 => [ 'classIndex' => 1, 'type' => 'method', ], 83 => [ 'classIndex' => 1, 'type' => 'method', ], 140 => [ 'classIndex' => 1, 'type' => 'method', ], 164 => [ 'classIndex' => 158, 'type' => 'const', ], 173 => [ 'classIndex' => 158, 'type' => 'trait_import', ], ], <<<'PHP' getClassyElements(); self::assertSame( [ 11 => [ 'classIndex' => 1, 'token' => $tokens[11], 'type' => 'property', ], 19 => [ 'classIndex' => 1, 'token' => $tokens[19], 'type' => 'property', ], 26 => [ 'classIndex' => 1, 'token' => $tokens[26], 'type' => 'property', ], 41 => [ 'classIndex' => 1, 'token' => $tokens[41], 'type' => 'property', ], ], $elements ); } public function testGetClassyElementsWithAnonymousClass(): void { $source = <<<'PHP' getClassyElements(); self::assertSame( [ 9 => [ 'classIndex' => 1, 'token' => $tokens[9], 'type' => 'property', // $A ], 14 => [ 'classIndex' => 1, 'token' => $tokens[14], 'type' => 'method', // B ], 33 => [ 'classIndex' => 26, 'token' => $tokens[33], 'type' => 'property', // $level1 ], 38 => [ 'classIndex' => 26, 'token' => $tokens[38], 'type' => 'method', // XYZ ], 56 => [ 'classIndex' => 50, 'token' => $tokens[56], 'type' => 'property', // $level2 ], 74 => [ 'classIndex' => 1, 'token' => $tokens[74], 'type' => 'method', // C ], ], $elements ); } public function testGetClassyElementsWithMultipleAnonymousClass(): void { $source = <<<'PHP' getClassyElements(); self::assertSame( [ 9 => [ 'classIndex' => 1, 'token' => $tokens[9], 'type' => 'method', ], 27 => [ 'classIndex' => 21, 'token' => $tokens[27], 'type' => 'method', ], 44 => [ 'classIndex' => 1, 'token' => $tokens[44], 'type' => 'method', ], 64 => [ 'classIndex' => 56, 'token' => $tokens[64], 'type' => 'method', ], 82 => [ 'classIndex' => 76, 'token' => $tokens[82], 'type' => 'method', ], 100 => [ 'classIndex' => 94, 'token' => $tokens[100], 'type' => 'method', ], 118 => [ 'classIndex' => 112, 'token' => $tokens[118], 'type' => 'method', ], 139 => [ 'classIndex' => 112, 'token' => $tokens[139], 'type' => 'method', ], 170 => [ 'classIndex' => 76, 'token' => $tokens[170], 'type' => 'method', ], 188 => [ 'classIndex' => 182, 'token' => $tokens[188], 'type' => 'method', ], 206 => [ 'classIndex' => 200, 'token' => $tokens[206], 'type' => 'method', ], 242 => [ 'classIndex' => 56, 'token' => $tokens[242], 'type' => 'method', ], ], $elements ); } public function testGetClassyElements74(): void { $source = <<<'PHP' getClassyElements(); $expected = []; foreach ([11, 23, 31, 44, 51, 54, 61, 69] as $index) { $expected[$index] = [ 'classIndex' => 1, 'token' => $tokens[$index], 'type' => 'property', ]; } self::assertSame($expected, $elements); } /** * @param array $expected * * @dataProvider provideGetClassyElements81Cases * * @requires PHP 8.1 */ public function testGetClassyElements81(array $expected, string $source): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); $elements = $tokensAnalyzer->getClassyElements(); array_walk( $expected, static function (array &$element, int $index) use ($tokens): void { $element['token'] = $tokens[$index]; ksort($element); } ); self::assertSame($expected, $elements); } public static function provideGetClassyElements81Cases(): iterable { yield [ [ 11 => [ 'classIndex' => 1, 'type' => 'property', // $prop1 ], 20 => [ 'classIndex' => 1, 'type' => 'property', // $prop2 ], 29 => [ 'classIndex' => 1, 'type' => 'property', // $prop13 ], ], ' [ [ 11 => [ 'classIndex' => 1, 'type' => 'const', // A ], 24 => [ 'classIndex' => 1, 'type' => 'const', // B ], ], ' [ [ 11 => [ 'classIndex' => 1, 'type' => 'const', // A ], 24 => [ 'classIndex' => 1, 'type' => 'const', // B ], ], ' [ [ 12 => [ 'classIndex' => 1, 'type' => 'const', // Spades ], 21 => [ 'classIndex' => 1, 'type' => 'case', // Hearts ], 32 => [ 'classIndex' => 1, 'type' => 'method', // function tests ], 81 => [ 'classIndex' => 75, 'type' => 'method', // function bar123 ], 135 => [ 'classIndex' => 127, 'type' => 'method', // function bar7 ], ], ' [ [ 10 => [ 'classIndex' => 1, 'type' => 'case', ], 19 => [ 'classIndex' => 1, 'type' => 'case', ], 28 => [ 'classIndex' => 1, 'type' => 'method', ], ], ' $expected * * @dataProvider provideGetClassyElements82Cases * * @requires PHP 8.2 */ public function testGetClassyElements82(array $expected, string $source): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); $elements = $tokensAnalyzer->getClassyElements(); array_walk( $expected, static function (array &$element, int $index) use ($tokens): void { $element['token'] = $tokens[$index]; ksort($element); }, ); self::assertSame($expected, $elements); } public static function provideGetClassyElements82Cases(): iterable { yield 'constant in trait' => [ [ 7 => [ 'classIndex' => 1, 'type' => 'const', ], 18 => [ 'classIndex' => 1, 'type' => 'const', ], ], <<<'PHP' $expected * * @dataProvider provideIsAnonymousClassCases */ public function testIsAnonymousClass(array $expected, string $source): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $expectedValue) { self::assertSame($expectedValue, $tokensAnalyzer->isAnonymousClass($index)); } } /** * @return iterable, string}> */ public static function provideIsAnonymousClassCases(): iterable { yield [ [1 => false], ' true], ' true], ' false, 19 => true], ' true, 11 => true], 'a) implements B{}) extends C{};', ]; yield [ [1 => false], ' $expected * * @dataProvider provideIsAnonymousClass80Cases * * @requires PHP 8.0 */ public function testIsAnonymousClass80(array $expected, string $source): void { $this->testIsAnonymousClass($expected, $source); } /** * @return iterable, string}> */ public static function provideIsAnonymousClass80Cases(): iterable { yield [ [11 => true], ' true], ' $expected * * @dataProvider provideIsAnonymousClass81Cases * * @requires PHP 8.1 */ public function testIsAnonymousClass81(array $expected, string $source): void { $this->testIsAnonymousClass($expected, $source); } /** * @return iterable, string}> */ public static function provideIsAnonymousClass81Cases(): iterable { yield [ [1 => false], ' $expected * * @dataProvider provideIsAnonymousClass83Cases * * @requires PHP 8.3 */ public function testIsAnonymousClass83(array $expected, string $source): void { $this->testIsAnonymousClass($expected, $source); } /** * @return iterable, string}> */ public static function provideIsAnonymousClass83Cases(): iterable { yield 'simple readonly anonymous class' => [ [9 => true], ' [ [13 => true], ' [ [17 => true], ' $expected * * @dataProvider provideIsLambdaCases */ public function testIsLambda(array $expected, string $source): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isLambda) { self::assertSame($isLambda, $tokensAnalyzer->isLambda($index)); } } public static function provideIsLambdaCases(): iterable { yield [ [1 => false], ' false], ' true], ' true], ' true], ' true], ' true], ' false], ' true], ' false], ' true], ' true], ' false], ' true], ' [];', ]; yield [ [5 => true], ' [];', ]; } /** * @param array $expected * * @dataProvider provideIsLambda80Cases * * @requires PHP 8.0 */ public function testIsLambda80(array $expected, string $source): void { $this->testIsLambda($expected, $source); } public static function provideIsLambda80Cases(): iterable { yield [ [6 => true], ' true], ' true], '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); } /** * @param array $expected * * @dataProvider provideIsConstantInvocationCases */ public function testIsConstantInvocation(array $expected, string $source): void { $this->doIsConstantInvocationTest($expected, $source); } public static function provideIsConstantInvocationCases(): iterable { yield [ [3 => true], ' true], ' false, 5 => false, 7 => true], ' true, 7 => true, 11 => true], ' [ [3 => true, 7 => true, 11 => true], ' [ [3 => true], ' true], ' true, 5 => true], ' false, 3 => true, 6 => false, 8 => true], ' true, 8 => true], ' true, 7 => false, 9 => false, 11 => true], ' false, 11 => true, 16 => true, 20 => true], ' BAR; }', ]; yield [ [11 => 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], ' false, 8 => false], ' false, 5 => false, 8 => false, 11 => false, 15 => false, 18 => false], ' false, 16 => false, 21 => false], ' 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], ' true], ' false, 5 => false, 9 => false, ], ' $expected * * @dataProvider provideIsConstantInvocationPhp80Cases * * @requires PHP 8.0 */ public function testIsConstantInvocationPhp80(array $expected, string $source): void { $this->doIsConstantInvocationTest($expected, $source); } public static function provideIsConstantInvocationPhp80Cases(): iterable { yield 'abstract method return alternation' => [ [6 => false, 16 => false, 21 => false, 23 => false], ' [ [3 => false, 8 => false, 10 => false], ' [ [3 => false, 5 => false], 'b?->c;', ]; yield 'non-capturing catch' => [ [9 => false], ' [ [10 => false], ' [ [9 => false, 13 => false], ' [ [2 => false, 5 => false, 10 => false], ' [ [2 => false, 7 => false, 14 => false], ' false, 9 => false], ' false, 10 => false], ' false, 4 => false, 11 => false], ' false, 5 => false, 12 => false], ' [ [5 => false, 15 => false, 18 => false], ' [ [5 => false, 15 => false, 18 => false], ' [ [5 => false, 15 => false, 17 => false, 19 => false, 21 => false, 24 => false, 27 => false], ' [ [5 => false, 14 => false, 16 => false, 19 => false, 22 => false], ' [ [4 => false, 6 => false, 8 => false, 13 => false, 17 => false, 23 => false, 25 => false], ' $expected * * @dataProvider provideIsConstantInvocationPhp81Cases * * @requires PHP 8.1 */ public function testIsConstantInvocationPhp81(array $expected, string $source): void { $this->doIsConstantInvocationTest($expected, $source); } public static function provideIsConstantInvocationPhp81Cases(): iterable { yield [ [5 => false, 15 => false], ' false, 12 => false, 23 => false], ' [ [3 => false, 9 => false, 11 => false], ' [ [6 => false, 16 => false, 21 => false, 23 => false, 25 => false, 27 => false, 29 => false], ' $expected * * @dataProvider provideIsConstantInvocationPhp82Cases * * @requires PHP 8.2 */ public function testIsConstantInvocationPhp82(array $expected, string $source): void { $this->doIsConstantInvocationTest($expected, $source); } /** * @return iterable, string}> */ public static function provideIsConstantInvocationPhp82Cases(): iterable { yield [ [3 => false, 11 => false, 13 => false, 17 => false, 20 => false, 23 => false, 25 => false, 28 => false, 31 => false, 33 => false, 35 => false, 39 => false, 42 => false, 44 => false], ' false, 8 => false, 10 => false, 14 => false, 17 => false, 20 => false, 22 => false, 25 => false, 28 => false, 30 => false, 32 => false, 36 => false, 39 => false, 41 => false], ' false, 6 => false, 12 => false], ' $expected * * @dataProvider provideIsConstantInvocationPhp83Cases * * @requires PHP 8.3 */ public function testIsConstantInvocationPhp83(array $expected, string $source): void { $this->doIsConstantInvocationTest($expected, $source); } /** * @return iterable, string}> */ public static function provideIsConstantInvocationPhp83Cases(): iterable { yield [ [3 => false, 11 => false, 13 => false, 17 => true], ' false, 11 => false, 13 => false, 17 => true], ' false, 11 => false, 13 => false, 17 => true], ' false, 13 => false, 18 => true], ' false, 11 => false, 13 => false, 17 => true], ' false, 11 => false, 13 => false, 15 => false, 19 => true], ' false, 11 => false, 13 => false, 15 => false, 19 => true], ' false, 12 => false, 14 => false, 17 => false, 19 => false, 23 => true], ' false, 12 => false, 14 => false, 18 => false, 20 => false, 23 => false, 27 => true], ' false, 12 => false, 15 => false, 17 => false, 21 => false, 23 => false, 25 => false, 28 => false, 32 => true], '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; } self::assertFalse($tokensAnalyzer->isConstantInvocation($index)); } } /** * @param array $expected * * @dataProvider provideIsUnarySuccessorOperatorCases */ public function testIsUnarySuccessorOperator(array $expected, string $source): void { $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($expected as $index => $isUnary) { self::assertSame($isUnary, $tokensAnalyzer->isUnarySuccessorOperator($index)); if ($isUnary) { self::assertFalse($tokensAnalyzer->isUnaryPredecessorOperator($index)); self::assertFalse($tokensAnalyzer->isBinaryOperator($index)); } } } public static function provideIsUnarySuccessorOperatorCases(): iterable { yield [ [2 => true], ' true], ' true], ' true, 4 => false], ' true], ' true], 'bar++;', ]; yield [ [6 => true], '{"bar"}++;', ]; yield 'array access' => [ [5 => true], ' $expected * * @dataProvider provideIsUnarySuccessorOperatorPre84Cases * * @requires PHP <8.4 */ public function testIsUnarySuccessorOperatorPre84(array $expected, string $source): void { $this->testIsUnarySuccessorOperator($expected, $source); } /** * @return iterable, string}> */ public static function provideIsUnarySuccessorOperatorPre84Cases(): iterable { yield 'array curly access' => [ [5 => true], ' $expected * * @dataProvider provideIsUnaryPredecessorOperatorCases */ public function testIsUnaryPredecessorOperator(array $expected, string $source): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($tokens as $index => $token) { $expect = \in_array($index, $expected, true); self::assertSame( $expect, $tokensAnalyzer->isUnaryPredecessorOperator($index), \sprintf('Expected %sunary predecessor operator, got @ %d "%s".', $expect ? '' : 'no ', $index, var_export($token, true)) ); if ($expect) { self::assertFalse( $tokensAnalyzer->isUnarySuccessorOperator($index), \sprintf('Expected no unary successor operator, got @ %d "%s".', $index, var_export($token, true)) ); self::assertFalse( $tokensAnalyzer->isBinaryOperator($index), \sprintf('Expected no binary operator, got @ %d "%s".', $index, var_export($token, true)) ); } } } public static function provideIsUnaryPredecessorOperatorCases(): iterable { yield [ [1], ' null;', ]; yield [ [], ' A_CONSTANT & $object->property;', ]; } /** * @param list $expected * * @dataProvider provideIsBinaryOperatorCases */ public function testIsBinaryOperator(array $expected, string $source): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source)); foreach ($tokens as $index => $token) { $expect = \in_array($index, $expected, true); self::assertSame( $expect, $tokensAnalyzer->isBinaryOperator($index), \sprintf('Expected %sbinary operator, got @ %d "%s".', $expect ? '' : 'no ', $index, var_export($token, true)) ); if ($expect) { self::assertFalse( $tokensAnalyzer->isUnarySuccessorOperator($index), \sprintf('Expected no unary successor operator, got @ %d "%s".', $index, var_export($token, true)) ); self::assertFalse( $tokensAnalyzer->isUnaryPredecessorOperator($index), \sprintf('Expected no unary predecessor operator, got @ %d "%s".', $index, var_export($token, true)) ); } } } public static function provideIsBinaryOperatorCases(): iterable { yield [ [8], '', ]; yield [ [3], '', ]; yield [ [3], ' "c", );', ]; yield [ [3], ' false], '-', ]; $operators = [ '+', '-', '*', '/', '%', '<', '>', '|', '^', '&=', '&&', '||', '.=', '/=', '==', '>=', '===', '!=', '<>', '!==', '<=', 'and', 'or', 'xor', '-=', '%=', '*=', '|=', '+=', '<<', '<<=', '>>', '>>=', ]; foreach ($operators as $operator) { yield [ [3], ' $b;', ]; yield [ [3], ' $object->property & A_CONSTANT;', ]; } /** * @dataProvider provideIsArrayCases */ public function testIsArray(string $source, int $tokenIndex, bool $isMultiLineArray = false): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); self::assertTrue($tokensAnalyzer->isArray($tokenIndex), 'Expected to be an array.'); self::assertSame($isMultiLineArray, $tokensAnalyzer->isArrayMultiLine($tokenIndex), \sprintf('Expected %sto be a multiline array', $isMultiLineArray ? '' : 'not ')); } /** * @return iterable */ public static function provideIsArrayCases(): iterable { yield [ ' 1); ', 2, ]; yield [ ' 2]; ', 2, false, ]; yield [ ' 3 ); ', 2, true, ]; yield [ ' 4 ]; ', 2, true, ]; yield [ ' array(5, 6, 7), 8 => new \Exception(\'Hello\') ); ', 2, true, ]; yield [ // mix short array syntax ' [9, 10, 11], 12 => new \Exception(\'Hello\') ); ', 2, true, ]; // Windows/Max EOL testing yield [ " 13);\r\n", 1, ]; yield [ " 14,\r\n 'b' => 15\r\n );\r\n", 2, true, ]; } /** * @param list $tokenIndexes * * @dataProvider provideIsArray71Cases */ 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); self::assertSame( $expect, $tokensAnalyzer->isArray($index), \sprintf('Expected %sarray, got @ %d "%s".', $expect ? '' : 'no ', $index, var_export($token, true)) ); } } public static function provideIsArray71Cases(): iterable { yield [ ' $a, "b" => $b] = $array; $c = [$d, $e] = $array[$a]; [[$a, $b], [$c, $d]] = $d; $array = []; $d = array(); ', [76, 84], ]; } /** * @param list $expected * * @dataProvider provideIsBinaryOperator80Cases * * @requires PHP 8.0 */ public function testIsBinaryOperator80(array $expected, string $source): void { $this->testIsBinaryOperator($expected, $source); } public static function provideIsBinaryOperator80Cases(): iterable { yield [ [], ' $expected * * @dataProvider provideIsBinaryOperator81Cases * * @requires PHP 8.1 */ public function testIsBinaryOperator81(array $expected, string $source): void { $this->testIsBinaryOperator($expected, $source); } public static function provideIsBinaryOperator81Cases(): iterable { yield 'type intersection' => [ [], ' $expected * * @dataProvider provideIsBinaryOperator82Cases * * @requires PHP 8.2 */ public function testIsBinaryOperator82(array $expected, string $source): void { $this->testIsBinaryOperator($expected, $source); } public static function provideIsBinaryOperator82Cases(): iterable { yield [ [], ' [ [], ' [ [12, 15], ' $expected * * @dataProvider provideIsBinaryOperatorPre84Cases * * @requires PHP <8.4 */ public function testIsBinaryOperatorPre84(array $expected, string $source): void { $this->testIsBinaryOperator($expected, $source); } /** * @return iterable, string}> */ public static function provideIsBinaryOperatorPre84Cases(): iterable { yield [ [8], '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); } /** * @return iterable */ public static function provideArrayExceptionsCases(): iterable { yield ['expectException(\LogicException::class); $tokens = Tokens::fromCode('isBlockMultiline($tokens, 1); } /** * @dataProvider provideIsBlockMultilineCases */ public function testIsBlockMultiline(bool $isBlockMultiline, string $source, int $tokenIndex): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); self::assertSame($isBlockMultiline, $tokensAnalyzer->isBlockMultiline($tokens, $tokenIndex)); } /** * @return iterable */ public static function provideIsBlockMultilineCases(): iterable { yield [ false, 'getMethodAttributes($index); self::assertSame($expected, $attributes); } public static function provideGetFunctionPropertiesCases(): iterable { $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; } $isExpected = $expected[$index] ?? null; self::assertSame( $isExpected, $tokensAnalyzer->isWhilePartOfDoWhile($index), \sprintf('Expected token at index "%d" to be detected as %sa "do-while"-loop.', $index, true === $isExpected ? '' : 'not ') ); } } /** * @dataProvider provideIsEnumCaseCases * * @param array $expected * * @requires PHP 8.1 */ public function testIsEnumCase(string $source, array $expected): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); foreach ($tokens as $index => $token) { if (!$token->isGivenKind(T_CASE)) { try { $tokensAnalyzer->isEnumCase($index); self::fail('TokensAnalyzer::isEnumCase() did not throw LogicException.'); } catch (\Throwable $e) { self::assertInstanceOf(\LogicException::class, $e); self::assertMatchesRegularExpression('/^No T_CASE given at index \d+, got \S+ instead\.$/', $e->getMessage()); } continue; } self::assertSame($expected[$index], $tokensAnalyzer->isEnumCase($index)); } } public static function provideIsEnumCaseCases(): iterable { yield 'switch only' => [ ' false, 33 => false, ], ]; yield 'pure enum' => [ ' true, 12 => true, ], ]; yield 'pure enum with switch' => [ 'name) { case \'One\': case \'Two\': return strtolower($instance->name); } } } ', [ 7 => true, 12 => true, 45 => false, 50 => false, ], ]; yield 'backed enum' => [ ' true, 19 => true, 28 => true, 37 => true, ], ]; yield 'backed enum with switch' => [ 'value) { case \'hearts\': case \'spades\': return strtoupper($instance->value); default: return $instance->value; } } } ', [ 10 => true, 19 => true, 28 => true, 37 => true, 74 => false, 79 => false, ], ]; } /** * @param array>|list $expected * * @dataProvider provideGetImportUseIndexesCases */ public function testGetImportUseIndexes(array $expected, string $input, bool $perNamespace = false): void { $tokens = Tokens::fromCode($input); $tokensAnalyzer = new TokensAnalyzer($tokens); self::assertSame($expected, $tokensAnalyzer->getImportUseIndexes($perNamespace)); } public static function provideGetImportUseIndexesCases(): iterable { yield [ [1, 8], '', true, ]; yield [ [1, 8], '', ]; yield [ [7, 22], 'getClassyElements(); self::assertSame([ 13 => [ 'classIndex' => 1, 'token' => $tokens[13], 'type' => 'method', // setUp ], 46 => [ 'classIndex' => 1, 'token' => $tokens[46], 'type' => 'method', // testSomethingWithMoney ], 100 => [ 'classIndex' => 87, 'token' => $tokens[100], // const A 'type' => 'const', ], 115 => [ 'classIndex' => 65, 'token' => $tokens[115], // const B 'type' => 'const', ], 124 => [ 'classIndex' => 65, // $a 'token' => $tokens[124], 'type' => 'method', // foo ], 143 => [ 'classIndex' => 138, 'token' => $tokens[143], // const AA 'type' => 'const', ], 161 => [ 'classIndex' => 158, 'token' => $tokens[161], // const AB 'type' => 'const', ], ], $elements); } /** * @dataProvider provideIsSuperGlobalCases */ public function testIsSuperGlobal(bool $expected, string $source, int $index): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); self::assertSame($expected, $tokensAnalyzer->isSuperGlobal($index)); } /** * @return iterable */ public static function provideIsSuperGlobalCases(): iterable { $superNames = [ '$_COOKIE', '$_ENV', '$_FILES', '$_GET', '$_POST', '$_REQUEST', '$_SERVER', '$_SESSION', '$GLOBALS', ]; $cases = []; foreach ($superNames as $superName) { $cases[] = [ true, \sprintf('$b(); }; // $_SERVER', ' $expectedModifiers */ public function testGetClassyModifiers(array $expectedModifiers, int $index, string $source): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); self::assertSame($expectedModifiers, $tokensAnalyzer->getClassyModifiers($index)); } public static function provideGetClassyModifiersCases(): iterable { yield 'final' => [ ['final' => 1, 'abstract' => null, 'readonly' => null], 3, ' [ ['final' => null, 'abstract' => 3, 'readonly' => null], 5, ' $expectedModifiers */ public function testGetClassyModifiersOnPhp82(array $expectedModifiers, int $index, string $source): void { $this->testGetClassyModifiers($expectedModifiers, $index, $source); } public static function provideGetClassyModifiersOnPhp82Cases(): iterable { yield 'readonly' => [ ['final' => null, 'abstract' => null, 'readonly' => 1], 3, ' [ ['final' => 3, 'abstract' => null, 'readonly' => 1], 5, ' [ ['final' => 1, 'abstract' => null, 'readonly' => 3], 5, ' [ ['final' => 1, 'abstract' => null, 'readonly' => 5], 7, ' [ ['final' => null, 'abstract' => 3, 'readonly' => 1], 5, ' [ ['final' => null, 'abstract' => 1, 'readonly' => 3], 5, ' [ ['final' => null, 'abstract' => 1, 'readonly' => 5], 7, 'expectException(\InvalidArgumentException::class); $tokensAnalyzer->getClassyModifiers(1); } /** * @dataProvider provideGetLastTokenIndexOfArrowFunctionCases * * @param array $expectations */ public function testGetLastTokenIndexOfArrowFunction(array $expectations, string $source): void { $tokens = Tokens::fromCode($source); $tokensAnalyzer = new TokensAnalyzer($tokens); $indices = []; foreach ($expectations as $index => $expectedEndIndex) { $indices[$index] = $tokensAnalyzer->getLastTokenIndexOfArrowFunction($index); } self::assertSame($expectations, $indices); } public static function provideGetLastTokenIndexOfArrowFunctionCases(): iterable { yield 'simple cases' => [ [ 2 => 11, 16 => 25, 28 => 39, 46 => 61, ], ' $x; static fn(): int => $x; fn($x = 42) => $x; $eq = fn ($x, $y) => $x == $y; ', ]; yield 'references, splat and arrow cases' => [ [ 2 => 10, 13 => 21, 24 => 35, 42 => 51, 65 => 77, ], ' $x; fn&($x) => $x; fn($x, ...$rest) => $rest; $fn = fn(&$x) => $x++; $y = &$fn($x); fn($x, &...$rest) => 1; ', ]; yield 'different endings' => [ [ 9 => 21, 31 => 43, ], ' $item * 2, $list ); return fn ($y) => $x * $y ?> ', ]; yield 'nested arrow function' => [ [ 1 => 26, 14 => 25, ], ' (fn(int $z):bool => $z);', ]; yield 'arrow function as argument' => [ [ 5 => 14, ], ' $x);', ]; yield 'arrow function as collection item' => [ [ 9 => 18, 26 => 35, 46 => 55, 62 => 69, ], ' $x1, 1], [fn(array $x) => $x2, 1], [1, fn(array $x) => $x3], ([(fn($x4) => $x5)]), ];', ]; yield 'nested inside anonymous class' => [ [ 1 => 46, 33 => 41, ], ' $a = new class($x) { public function foo() { return fn(&$x) => $x; } };', ]; yield 'array destructuring' => [ [ 4 => 13, ], ' $x1] = $x;', ]; yield 'array_map() callback with different token blocks' => [ [ 9 => 28, ], ' $item[\'callback\']($item[\'value\']), [/* items */] ); ', ]; yield 'arrow function returning array' => [ [ 5 => 21, ], ' [0, 1, $a];', ]; } public function testCannotGetLastTokenIndexOfArrowFunctionForNonFnToken(): void { $tokens = Tokens::fromCode('expectException(\InvalidArgumentException::class); $tokensAnalyzer->getLastTokenIndexOfArrowFunction(1); } /** * @param array $expected */ private function doIsConstantInvocationTest(array $expected, string $source): void { $tokens = Tokens::fromCode($source); self::assertCount( $tokens->countTokenKind(T_STRING), $expected, 'All T_STRING tokens must be tested' ); $tokensAnalyzer = new TokensAnalyzer($tokens); foreach ($expected as $index => $expectedValue) { self::assertSame( $expectedValue, $tokensAnalyzer->isConstantInvocation($index), \sprintf('Token at index '.$index.' should match the expected value (%s).', $expectedValue ? 'true' : 'false') ); } } }