<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * 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 <dariusz.ruminski@gmail.com>
 * @author Max Voloshin <voloshin.dp@gmail.com>
 * @author Gregor Harlan <gharlan@web.de>
 *
 * @internal
 *
 * @covers \PhpCsFixer\Tokenizer\TokensAnalyzer
 */
final class TokensAnalyzerTest extends TestCase
{
    /**
     * @dataProvider provideGetClassyElementsCases
     */
    public function testGetClassyElements(array $expectedElements, string $source): void
    {
        $tokens = Tokens::fromCode($source);

        array_walk(
            $expectedElements,
            static function (array &$element, $index) use ($tokens): void {
                $element['token'] = $tokens[$index];
                ksort($element);
            }
        );

        $tokensAnalyzer = new TokensAnalyzer($tokens);

        static::assertSame(
            $expectedElements,
            $tokensAnalyzer->getClassyElements()
        );
    }

    public function provideGetClassyElementsCases(): \Generator
    {
        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',
                ],
            ],
            '<?php
            /**  */
            class Foo
            {
                use A\B;
                //
                use Foo;

                const A = 1;

                public function foo()
                {
                    $a = new class()
                    {
                        use Z; // nested trait import

                        public function bar()
                        {
                            echo 123;
                        }
                    };

                    $a->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'
<?php
class Foo
{
    public $prop0;
    protected $prop1;
    private $prop2 = 1;
    var $prop3 = array(1,2,3);
    const CONSTANT = 'constant value';

    public function bar4()
    {
        $a = 5;

        return " ({$a})";
    }
    public function bar5($data)
    {
        $message = $data;
        $example = function ($arg) use ($message) {
            echo $arg . ' ' . $message;
        };
        $example('hello');
    }function A(){}
}

function test(){}

class Foo2
{
    const CONSTANT = 'constant value';

    use Foo\Bar; // expected in the return value
}

PHP
            ,
        ];
    }

    /**
     * @requires PHP 7.4
     */
    public function testGetClassyElementsWithNullableProperties(): void
    {
        $source = <<<'PHP'
<?php
class Foo
{
    public int $prop0;
    protected ?array $prop1;
    private string $prop2 = 1;
    var ? Foo\Bar $prop3 = array(1,2,3);
}

PHP;
        $tokens = Tokens::fromCode($source);
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $elements = $tokensAnalyzer->getClassyElements();

        static::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'
<?php
class A {
    public $A;

    private function B()
    {
        return new class(){
            protected $level1;
            private function XYZ() {
                return new class(){private $level2 = 1;};
            }
        };
    }

    private function C() {
    }
}

function B() {} // do not count this
PHP;
        $tokens = Tokens::fromCode($source);
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $elements = $tokensAnalyzer->getClassyElements();

        static::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'
<?php class A0
{
    public function AA0()
    {
        return new class
        {
            public function BB0()
            {
            }
        };
    }

    public function otherFunction0()
    {
    }
}

class A1
{
    public function AA1()
    {
        return new class
        {
            public function BB1()
            {
                return new class
                {
                    public function CC1()
                    {
                        return new class
                        {
                            public function DD1()
                            {
                                return new class{};
                            }

                            public function DD2()
                            {
                                return new class{};
                            }
                        };
                    }
                };
            }

            public function BB2()
            {
                return new class
                {
                    public function CC2()
                    {
                        return new class
                        {
                            public function DD2()
                            {
                                return new class{};
                            }
                        };
                    }
                };
            }
        };
    }

    public function otherFunction1()
    {
    }
}
PHP;
        $tokens = Tokens::fromCode($source);
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $elements = $tokensAnalyzer->getClassyElements();

        static::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
        );
    }

    /**
     * @requires PHP 7.4
     */
    public function testGetClassyElements74(): void
    {
        $source = <<<'PHP'
<?php
class Foo
{
    public int $bar = 3;

    protected ?string $baz;

    private ?string $bazNull = null;

    public static iterable $staticProp;

    public float $x, $y;

    var bool $flag1;

    var ?bool $flag2;
}

PHP;
        $tokens = Tokens::fromCode($source);
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $elements = $tokensAnalyzer->getClassyElements();
        $expected = [];

        foreach ([11, 23, 31, 44, 51, 54, 61, 69] as $index) {
            $expected[$index] = [
                'classIndex' => 1,
                'token' => $tokens[$index],
                'type' => 'property',
            ];
        }

        static::assertSame($expected, $elements);
    }

    /**
     * @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, $index) use ($tokens): void {
                $element['token'] = $tokens[$index];
                ksort($element);
            }
        );

        static::assertSame($expected, $elements);
    }

    public function provideGetClassyElements81Cases(): \Generator
    {
        yield [
            [
                11 => [
                    'classIndex' => 1,
                    'type' => 'property', // $prop1
                ],
                20 => [
                    'classIndex' => 1,
                    'type' => 'property', // $prop2
                ],
                29 => [
                    'classIndex' => 1,
                    'type' => 'property', // $prop13
                ],
            ],
            '<?php
class Foo
{
    readonly string $prop1;
    readonly public string $prop2;
    public readonly string $prop3;
}
            ',
        ];

        yield 'final const' => [
            [
                11 => [
                    'classIndex' => 1,
                    'type' => 'const', // A
                ],
                24 => [
                    'classIndex' => 1,
                    'type' => 'const', // B
                ],
            ],
            '<?php
class Foo
{
    final public const A = "1";
    public final const B = "2";
}
            ',
        ];
    }

    /**
     * @dataProvider provideIsAnonymousClassCases
     */
    public function testIsAnonymousClass(array $expected, string $source): void
    {
        $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source));

        foreach ($expected as $index => $expectedValue) {
            static::assertSame($expectedValue, $tokensAnalyzer->isAnonymousClass($index));
        }
    }

    public function provideIsAnonymousClassCases(): \Generator
    {
        yield [
            [1 => false],
            '<?php class foo {}',
        ];

        yield [
            [7 => true],
            '<?php $foo = new class() {};',
        ];

        yield [
            [7 => true],
            '<?php $foo = new class() extends Foo implements Bar, Baz {};',
        ];

        yield [
            [1 => false, 19 => true],
            '<?php class Foo { function bar() { return new class() {}; } }',
        ];

        yield [
            [7 => true, 11 => true],
            '<?php $a = new class(new class($d->a) implements B{}) extends C{};',
        ];

        yield [
            [1 => false],
            '<?php interface foo {}',
        ];

        if (\PHP_VERSION_ID >= 80000) {
            yield [
                [11 => true],
                '<?php $object = new #[ExampleAttribute] class(){};',
            ];

            yield [
                [27 => true],
                '<?php $object = new #[A] #[B] #[C]#[D]/* */ /** */#[E]class(){};',
            ];
        }
    }

    /**
     * @dataProvider provideIsLambdaCases
     */
    public function testIsLambda(array $expected, string $source): void
    {
        $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source));

        foreach ($expected as $index => $isLambda) {
            static::assertSame($isLambda, $tokensAnalyzer->isLambda($index));
        }
    }

    public function provideIsLambdaCases(): array
    {
        return [
            [
                [1 => false],
                '<?php function foo () {};',
            ],
            [
                [1 => false],
                '<?php function /** foo */ foo () {};',
            ],
            [
                [5 => true],
                '<?php $foo = function () {};',
            ],
            [
                [5 => true],
                '<?php $foo = function /** foo */ () {};',
            ],
            [
                [7 => true],
                '<?php
preg_replace_callback(
    "/(^|[a-z])/",
    function (array $matches) {
        return "a";
    },
    $string
);',
            ],
            [
                [5 => true],
                '<?php $foo = function &() {};',
            ],
            [
                [6 => true],
                '<?php
                    $a = function (): array {
                        return [];
                    };',
            ],
            [
                [2 => false],
                '<?php
                    function foo (): array {
                        return [];
                    };',
            ],
            [
                [6 => true],
                '<?php
                    $a = function (): void {
                        return [];
                    };',
            ],
            [
                [2 => false],
                '<?php
                    function foo (): void {
                        return [];
                    };',
            ],
            [
                [6 => true],
                '<?php
                    $a = function (): ?int {
                        return [];
                    };',
            ],
            [
                [6 => true],
                '<?php
                    $a = function (): int {
                        return [];
                    };',
            ],
            [
                [2 => false],
                '<?php
                    function foo (): ?int {
                        return [];
                    };',
            ],
        ];
    }

    /**
     * @dataProvider provideIsLambda74Cases
     * @requires PHP 7.4
     */
    public function testIsLambda74(array $expected, string $source): void
    {
        $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source));

        foreach ($expected as $index => $expectedValue) {
            static::assertSame($expectedValue, $tokensAnalyzer->isLambda($index));
        }
    }

    public function provideIsLambda74Cases(): \Generator
    {
        yield [
            [5 => true],
            '<?php $fn = fn() => [];',
        ];

        yield [
            [5 => true],
            '<?php $fn = fn () => [];',
        ];
    }

    /**
     * @dataProvider provideIsLambda80Cases
     * @requires PHP 8.0
     */
    public function testIsLambda80(array $expected, string $source): void
    {
        $tokensAnalyzer = new TokensAnalyzer(Tokens::fromCode($source));

        foreach ($expected as $index => $expectedValue) {
            static::assertSame($expectedValue, $tokensAnalyzer->isLambda($index));
        }
    }

    public function provideIsLambda80Cases(): array
    {
        return [
            [
                [6 => true],
                '<?php
                    $a = function (): ?static {
                        return [];
                    };',
            ],
            [
                [6 => true],
                '<?php
                    $a = function (): static {
                        return [];
                    };',
            ],
            [
                [14 => true],
                '<?php
$c = 4; //
$a = function(
    $a,
    $b,
) use (
    $c,
) {
    echo $a + $b + $c;
};


$a(1,2);',
            ],
        ];
    }

    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('<?php '));
        $tokensAnalyzer->isLambda(0);
    }

    /**
     * @dataProvider provideIsConstantInvocationCases
     */
    public function testIsConstantInvocation(array $expected, string $source): void
    {
        $this->doIsConstantInvocationTest($expected, $source);
    }

    public function provideIsConstantInvocationCases(): array
    {
        return [
            [
                [3 => true],
                '<?php echo FOO;',
            ],
            [
                [4 => true],
                '<?php echo \FOO;',
            ],
            [
                [3 => false, 5 => false, 7 => true],
                '<?php echo Foo\Bar\BAR;',
            ],
            [
                [3 => true, 7 => true, 11 => true],
                '<?php echo FOO ? BAR : BAZ;',
            ],
            'Bitwise & and bitwise |' => [
                [3 => true, 7 => true, 11 => true],
                '<?php echo FOO & BAR | BAZ;',
            ],
            'Bitwise &' => [
                [3 => true],
                '<?php echo FOO & $bar;',
            ],
            [
                [5 => true],
                '<?php echo $foo[BAR];',
            ],
            [
                [3 => true, 5 => true],
                '<?php echo FOO[BAR];',
            ],
            [
                [1 => false, 3 => true, 6 => false,  8 => true],
                '<?php func(FOO, Bar\BAZ);',
            ],
            [
                [4 => true, 8 => true],
                '<?php if (FOO && BAR) {}',
            ],
            [
                [3 => true, 7 => false, 9 => false,  11 => true],
                '<?php return FOO * X\Y\BAR;',
            ],
            [
                [3 => false, 11 => true, 16 => true, 20 => true],
                '<?php function x() { yield FOO; yield FOO => BAR; }',
            ],
            [
                [11 => true],
                '<?php switch ($a) { case FOO: break; }',
            ],
            [
                [3 => false],
                '<?php namespace FOO;',
            ],
            [
                [3 => false],
                '<?php use FOO;',
            ],
            [
                [5 => false, 7 => false, 9 => false],
                '<?php use function FOO\BAR\BAZ;',
            ],
            [
                [3 => false, 8 => false],
                '<?php namespace X; const FOO = 1;',
            ],
            [
                [3 => false],
                '<?php class FOO {}',
            ],
            [
                [3 => false],
                '<?php interface FOO {}',
            ],
            [
                [3 => false],
                '<?php trait FOO {}',
            ],
            [
                [3 => false, 7 => false],
                '<?php class x extends FOO {}',
            ],
            [
                [3 => false, 7 => false],
                '<?php class x implements FOO {}',
            ],
            [
                [3 => false, 7 => false, 10 => false, 13 => false],
                '<?php class x implements FOO, BAR, BAZ {}',
            ],
            [
                [3 => false, 9 => false],
                '<?php class x { const FOO = 1; }',
            ],
            [
                [3 => false, 9 => false],
                '<?php class x { use FOO; }',
            ],
            [
                [3 => false, 9 => false, 12 => false, 16 => false, 18 => false, 22 => false],
                '<?php class x { use FOO, BAR { FOO::BAZ insteadof BAR; } }',
            ],
            [
                [3 => false, 6 => false, 11 => false, 17 => false],
                '<?php function x (FOO $foo, BAR &$bar, BAZ ...$baz) {}',
            ],
            [
                [1 => false],
                '<?php FOO();',
            ],
            [
                [1 => false, 3 => false],
                '<?php FOO::x();',
            ],
            [
                [1 => false, 3 => false],
                '<?php x::FOO();',
            ],
            [
                [5 => false],
                '<?php $foo instanceof FOO;',
            ],
            [
                [9 => false],
                '<?php try {} catch (FOO $e) {}',
            ],
            [
                [4 => false],
                '<?php "$foo[BAR]";',
            ],
            [
                [5 => true],
                '<?php "{$foo[BAR]}";',
            ],
            [
                [1 => false, 6 => false],
                '<?php FOO: goto FOO;',
            ],
            [
                [1 => false, 3 => true, 7 => true],
                '<?php foo(E_USER_DEPRECATED | E_DEPRECATED);',
            ],
            [
                [3 => false, 7 => false, 10 => false, 13 => false],
                '<?php interface Foo extends Bar, Baz, Qux {}',
            ],
            [
                [3 => false, 5 => false, 8 => false, 10 => false, 13 => false, 15 => false],
                '<?php use Foo\Bar, Foo\Baz, Foo\Qux;',
            ],
            [
                [3 => false, 8 => false],
                '<?php function x(): FOO {}',
            ],
            [
                [3 => false, 5 => false, 8 => false, 11 => false, 15 => false, 18 => false],
                '<?php use X\Y\{FOO, BAR as BAR2, BAZ};',
            ],
            [
                [6 => false, 16 => false, 21 => false],
                '<?php

abstract class Baz
{
    abstract public function test(): Foo;
}
',
            ],
            [
                [3 => false, 6 => false],
                '<?php function x(?FOO $foo) {}',
            ],
            [
                [3 => false, 9 => false],
                '<?php function x(): ?FOO {}',
            ],
            [
                [9 => false, 11 => false, 13 => false],
                '<?php try {} catch (FOO|BAR|BAZ $e) {}',
            ],
            [
                [3 => false, 11 => false, 16 => false],
                '<?php interface Foo { public function bar(): Baz; }',
            ],
            [
                [3 => false, 11 => false, 17 => false],
                '<?php interface Foo { public function bar(): \Baz; }',
            ],
            [
                [3 => false, 11 => false, 17 => false],
                '<?php interface Foo { public function bar(): ?Baz; }',
            ],
            [
                [3 => false, 11 => false, 18 => false],
                '<?php interface Foo { public function bar(): ?\Baz; }',
            ],
        ];
    }

    /**
     * @dataProvider provideIsConstantInvocationPhp80Cases
     * @requires PHP 8.0
     */
    public function testIsConstantInvocationPhp80(array $expected, string $source): void
    {
        $this->doIsConstantInvocationTest($expected, $source);
    }

    public function provideIsConstantInvocationPhp80Cases(): \Generator
    {
        yield 'abstract method return alternation' => [
            [6 => false, 16 => false, 21 => false, 23 => false],
            '<?php

abstract class Baz
{
    abstract public function test(): Foo|Bar;
}
',
        ];

        yield 'function return alternation' => [
            [3 => false, 8 => false, 10 => false],
            '<?php function test(): Foo|Bar {}',
        ];

        yield 'nullsafe operator' => [
            [3 => false, 5 => false],
            '<?php $a?->b?->c;',
        ];

        yield 'non-capturing catch' => [
            [9 => false],
            '<?php try {} catch (Exception) {}',
        ];

        yield 'non-capturing catch 2' => [
            [10 => false],
            '<?php try {} catch (\Exception) {}',
        ];

        yield 'non-capturing multiple catch' => [
            [9 => false, 13 => false],
            '<?php try {} catch (Foo | Bar) {}',
        ];

        yield 'attribute 1' => [
            [2 => false, 5 => false, 10 => false],
            '<?php #[Foo, Bar] function foo() {}',
        ];

        yield 'attribute 2' => [
            [2 => false, 7 => false, 14 => false],
            '<?php #[Foo(), Bar()] function foo() {}',
        ];

        yield [
            [2 => false, 9 => false],
            '<?php #[Foo()] function foo() {}',
        ];

        yield [
            [3 => false, 10 => false],
            '<?php #[\Foo()] function foo() {}',
        ];

        yield [
            [2 => false, 4 => false, 11 => false],
            '<?php #[A\Foo()] function foo() {}',
        ];

        yield [
            [3 => false, 5 => false, 12 => false],
            '<?php #[\A\Foo()] function foo() {}',
        ];
    }

    /**
     * @dataProvider provideIsConstantInvocationPhp81Cases
     * @requires PHP 8.1
     */
    public function testIsConstantInvocationPhp81(array $expected, string $source): void
    {
        $this->doIsConstantInvocationTest($expected, $source);
    }

    public function provideIsConstantInvocationPhp81Cases(): \Generator
    {
        yield [
            [5 => false, 15 => false],
            '<?php
abstract class Baz
{
    final public const Y = "i";
}
',
        ];

        yield [
            [4 => false, 12 => false, 23 => false],
            '<?php

class Test {
    public function __construct(
        public $prop = new Foo,
    ) {}
}
',
        ];

        yield 'intersection' => [
            [3 => false, 9 => false, 11 => false],
            '<?php function foo(): \Foo&Bar {}',
        ];

        yield 'abstract method return intersection' => [
            [6 => false, 16 => false, 21 => false, 23 => false, 25 => false, 27 => false, 29 => false],
            '<?php

abstract class Baz
{
    abstract public function foo(): Foo&Bar1&Bar2&Bar3&Bar4;
}
',
        ];
    }

    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('<?php '));
        $tokensAnalyzer->isConstantInvocation(0);
    }

    /**
     * @requires PHP 8.0
     */
    public function testIsConstantInvocationForNullSafeObjectOperator(): void
    {
        $tokens = Tokens::fromCode('<?php $a?->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(array $expected, string $source): 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(): array
    {
        return [
            [
                [2 => true],
                '<?php $a++;',
            ],
            [
                [2 => true],
                '<?php $a--;',
            ],
            [
                [3 => true],
                '<?php $a ++;',
            ],
            [
                [2 => true, 4 => false],
                '<?php $a++ + 1;',
            ],
            [
                [5 => true],
                '<?php ${"a"}++;',
            ],
            [
                [4 => true],
                '<?php $foo->bar++;',
            ],
            [
                [6 => true],
                '<?php $foo->{"bar"}++;',
            ],
            'array access' => [
                [5 => true],
                '<?php $a["foo"]++;',
            ],
            'array curly access' => [
                [5 => true],
                '<?php $a{"foo"}++;',
            ],
        ];
    }

    /**
     * @dataProvider provideIsUnaryPredecessorOperatorCases
     */
    public function testIsUnaryPredecessorOperator(array $expected, string $source): 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(): array
    {
        return [
            [
                [1 => true],
                '<?php ++$a;',
            ],
            [
                [1 => true],
                '<?php --$a;',
            ],
            [
                [1 => true],
                '<?php -- $a;',
            ],
            [
                [3 => false, 5 => true],
                '<?php $a + ++$b;',
            ],
            [
                [1 => true, 2 => true],
                '<?php !!$a;',
            ],
            [
                [5 => true],
                '<?php $a = &$b;',
            ],
            [
                [3 => true],
                '<?php function &foo() {}',
            ],
            [
                [1 => true],
                '<?php @foo();',
            ],
            [
                [3 => true, 8 => true],
                '<?php foo(+ $a, -$b);',
            ],
            [
                [5 => true, 11 => true, 17 => true],
                '<?php function foo(&$a, array &$b, Bar &$c) {}',
            ],
            [
                [8 => true],
                '<?php function foo($a, ...$b) {}',
            ],
            [
                [5 => true, 6 => true],
                '<?php function foo(&...$b) {}',
            ],
            [
                [7 => true],
                '<?php function foo(array ...$b) {}',
            ],
            [
                [7 => true],
                '<?php $foo = function(...$a) {};',
            ],
            [
                [10 => true],
                '<?php $foo = function($a, ...$b) {};',
            ],
        ];
    }

    /**
     * @dataProvider provideIsBinaryOperatorCases
     */
    public function testIsBinaryOperator(array $expected, string $source): 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(): \Generator
    {
        yield from [
            [
                [8 => true],
                '<?php echo $a[1] + 1;',
            ],
            [
                [8 => true],
                '<?php echo $a{1} + 1;',
            ],
            [
                [3 => true],
                '<?php $a .= $b; ?>',
            ],
            [
                [3 => true],
                '<?php $a . \'a\' ?>',
            ],
            [
                [3 => true],
                '<?php $a &+ $b;',
            ],
            [
                [3 => true],
                '<?php $a && $b;',
            ],
            [
                [3 => true],
                '<?php $a & $b;',
            ],
            [
                [4 => true],
                '<?php [] + [];',
            ],
            [
                [3 => true],
                '<?php $a + $b;',
            ],
            [
                [3 => true],
                '<?php 1 + $b;',
            ],
            [
                [3 => true],
                '<?php 0.2 + $b;',
            ],
            [
                [6 => true],
                '<?php $a[1] + $b;',
            ],
            [
                [3 => true],
                '<?php FOO + $b;',
            ],
            [
                [5 => true],
                '<?php foo() + $b;',
            ],
            [
                [6 => true],
                '<?php ${"foo"} + $b;',
            ],
            [
                [2 => true],
                '<?php $a+$b;',
            ],
            [
                [5 => true],
                '<?php $a /* foo */  +  /* bar */  $b;',
            ],
            [
                [3 => true],
                '<?php $a =
$b;',
            ],

            [
                [3 => true],
                '<?php $a
= $b;',
            ],
            [
                [3 => true, 9 => true, 12 => false],
                '<?php $a = array("b" => "c", );',
            ],
            [
                [3 => true, 5 => false],
                '<?php $a * -$b;',
            ],
            [
                [3 => true, 5 => false, 8 => true, 10 => false],
                '<?php $a = -2 / +5;',
            ],
            [
                [3 => true, 5 => false],
                '<?php $a = &$b;',
            ],
            [
                [2 => false, 4 => true],
                '<?php $a++ + $b;',
            ],
            [
                [7 => true],
                '<?php $a = FOO & $bar;',
            ],
            [
                [3 => true],
                '<?php __LINE__ - 1;',
            ],
            [
                [5 => true],
                '<?php `echo 1` + 1;',
            ],
            [
                [3 => true],
                '<?php $a ** $b;',
            ],
            [
                [3 => true],
                '<?php $a **= $b;',
            ],
            [
                [9 => false],
                '<?php $a = "{$value}-{$theSwitch}";',
            ],
        ];

        $operators = [
            '+', '-', '*', '/', '%', '<', '>', '|', '^', '&=', '&&', '||', '.=', '/=', '==', '>=', '===', '!=',
            '<>', '!==', '<=', 'and', 'or', 'xor', '-=', '%=', '*=', '|=', '+=', '<<', '<<=', '>>', '>>=', '^',
        ];

        foreach ($operators as $operator) {
            yield [
                [3 => true],
                '<?php $a '.$operator.' $b;',
            ];
        }

        yield [
            [3 => true],
            '<?php $a <=> $b;',
        ];

        yield [
            [3 => true],
            '<?php $a ?? $b;',
        ];
    }

    /**
     * @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(): array
    {
        return [
            [
                '<?php
                    array("a" => 1);
                ',
                2,
            ],
            [
                '<?php
                    ["a" => 2];
                ',
                2, false,
            ],
            [
                '<?php
                    array(
                        "a" => 3
                    );
                ',
                2, true,
            ],
            [
                '<?php
                    [
                        "a" => 4
                    ];
                ',
                2, true,
            ],
            [
                '<?php
                    array(
                        "a" => array(5, 6, 7),
8 => new \Exception(\'Hello\')
                    );
                ',
                2, true,
            ],
            [
                // mix short array syntax
                '<?php
                    array(
                        "a" => [9, 10, 11],
12 => new \Exception(\'Hello\')
                    );
                ',
                2, true,
            ],
            // Windows/Max EOL testing
            [
                "<?php\r\narray('a' => 13);\r\n",
                1,
            ],
            [
                "<?php\r\n   array(\r\n       'a' => 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(): array
    {
        return [
            [
                '<?php
                    [$a] = $z;
                    ["a" => $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(array $expected, string $source): 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(): \Generator
    {
        yield [
            [11 => false],
            '<?php try {} catch (A | B $e) {}',
        ];
    }

    /**
     * @dataProvider provideIsBinaryOperator74Cases
     * @requires PHP 7.4
     */
    public function testIsBinaryOperator74(array $expected, string $source): 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(): \Generator
    {
        yield [
            [3 => true],
            '<?php $a ??= $b;',
        ];
    }

    /**
     * @dataProvider provideIsBinaryOperator80Cases
     * @requires PHP 8.0
     */
    public function testIsBinaryOperator80(array $expected, string $source): 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 static function provideIsBinaryOperator80Cases(): iterable
    {
        yield [
            [6 => false],
            '<?php function foo(array|string $x) {}',
        ];

        yield [
            [6 => false],
            '<?php function foo(string|array $x) {}',
        ];

        yield [
            [6 => false],
            '<?php function foo(int|callable $x) {}',
        ];

        yield [
            [6 => false],
            '<?php function foo(callable|int $x) {}',
        ];
    }

    /**
     * @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(): array
    {
        return [
            ['<?php $a;', 1],
            ["<?php\n \$a = (0+1); // [0,1]", 4],
            ['<?php $text = "foo $bbb[0] bar";', 8],
            ['<?php $text = "foo ${aaa[123]} bar";', 9],
        ];
    }

    public function testIsBlockMultilineException(): void
    {
        $this->expectException(\LogicException::class);

        $tokens = Tokens::fromCode('<?php foo(1, 2, 3);');
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $tokensAnalyzer->isBlockMultiline($tokens, 1);
    }

    /**
     * @dataProvider provideIsBlockMultilineCases
     */
    public function testIsBlockMultiline(bool $isBlockMultiline, string $source, int $tokenIndex): void
    {
        $tokens = Tokens::fromCode($source);
        $tokensAnalyzer = new TokensAnalyzer($tokens);

        static::assertSame($isBlockMultiline, $tokensAnalyzer->isBlockMultiline($tokens, $tokenIndex));
    }

    public static function provideIsBlockMultilineCases(): \Generator
    {
        yield [
            false,
            '<?php foo(1, 2, 3);',
            2,
        ];

        yield [
            true,
            '<?php foo(1,
                2,
                3
            );',
            2,
        ];

        yield [
            false,
            '<?php foo(1, "Multi
                string", 2, 3);',
            2,
        ];

        yield [
            false,
            '<?php foo(1, havingNestedBlockThatIsMultilineDoesNotMakeTheMainBlockMultiline(
                    "a",
                    "b"
                ), 2, 3);',
            2,
        ];
    }

    /**
     * @dataProvider provideGetFunctionPropertiesCases
     */
    public function testGetFunctionProperties(string $source, int $index, array $expected): void
    {
        $tokens = Tokens::fromCode($source);
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $attributes = $tokensAnalyzer->getMethodAttributes($index);

        static::assertSame($expected, $attributes);
    }

    public function provideGetFunctionPropertiesCases(): array
    {
        $defaultAttributes = [
            'visibility' => null,
            'static' => false,
            'abstract' => false,
            'final' => false,
        ];

        $template = '
<?php
class TestClass {
    %s function a() {
        //
    }
}
';
        $cases = [];

        $attributes = $defaultAttributes;
        $attributes['visibility'] = T_PRIVATE;
        $cases[] = [sprintf($template, 'private'), 10, $attributes];

        $attributes = $defaultAttributes;
        $attributes['visibility'] = T_PUBLIC;
        $cases[] = [sprintf($template, 'public'), 10, $attributes];

        $attributes = $defaultAttributes;
        $attributes['visibility'] = T_PROTECTED;
        $cases[] = [sprintf($template, 'protected'), 10, $attributes];

        $attributes = $defaultAttributes;
        $attributes['visibility'] = null;
        $attributes['static'] = true;
        $cases[] = [sprintf($template, 'static'), 10, $attributes];

        $attributes = $defaultAttributes;
        $attributes['visibility'] = T_PUBLIC;
        $attributes['static'] = true;
        $attributes['final'] = true;
        $cases[] = [sprintf($template, 'final public static'), 14, $attributes];

        $attributes = $defaultAttributes;
        $attributes['visibility'] = null;
        $attributes['abstract'] = true;
        $cases[] = [sprintf($template, 'abstract'), 10, $attributes];

        $attributes = $defaultAttributes;
        $attributes['visibility'] = T_PUBLIC;
        $attributes['abstract'] = true;
        $cases[] = [sprintf($template, 'abstract public'), 12, $attributes];

        $attributes = $defaultAttributes;
        $cases[] = [sprintf($template, ''), 8, $attributes];

        return $cases;
    }

    public function testIsWhilePartOfDoWhile(): void
    {
        $source =
<<<'SRC'
<?php
// `not do`
while(false) {
}
while (false);
while (false)?>
<?php

if(false){
}while(false);

if(false){
}while(false)?><?php
while(false){}while(false){}

while ($i <= 10):
    echo $i;
    $i++;
endwhile;

?>
<?php while(false): ?>

<?php endwhile ?>

<?php
// `do`
do{
} while(false);

do{
} while(false)?>
<?php
if (false){}do{}while(false);

// `not do`, `do`
if(false){}while(false){}do{}while(false);
SRC;

        $expected = [
            3 => 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(): array
    {
        return [
            [
                [1, 8],
                '<?php use E\F?><?php use A\B;',
            ],
            [
                [[1], [14], [29]],
                '<?php
use T\A;
namespace A { use D\C; }
namespace b { use D\C; }
',
                true,
            ],
            [
                [[1, 8]],
                '<?php use D\B; use A\C?>',
                true,
            ],
            [
                [1, 8],
                '<?php use D\B; use A\C?>',
            ],
            [
                [7, 22],
                '<?php
namespace A { use D\C; }
namespace b { use D\C; }
',
            ],
            [
                [3, 10, 34, 45, 54, 59, 77, 95],
                <<<'EOF'
use Zoo\Bar;
use Foo\Bar;
use Foo\Zar\Baz;

<?php

use Foo\Bar;
use Foo\Bar\Foo as Fooo, Foo\Bar\FooBar as FooBaz;
 use Foo\Bir as FBB;
use Foo\Zar\Baz;
use SomeClass;
   use Symfony\Annotation\Template, Symfony\Doctrine\Entities\Entity;
use Zoo\Bar;

$a = new someclass();

use Zoo\Tar;

class AnnotatedClass
{
}
EOF
                ,
            ],
            [
                [1, 22, 41],
                '<?php
use some\a\{ClassA, ClassB, ClassC as C};
use function some\a\{fn_a, fn_b, fn_c};
use const some\a\{ConstA, ConstB, ConstC};
                ',
            ],
            [
                [[1, 22, 41]],
                '<?php
use some\a\{ClassA, ClassB, ClassC as C};
use function some\a\{fn_a, fn_b, fn_c};
use const some\a\{ConstA, ConstB, ConstC};
                ',
                true,
            ],
            [
                [1, 23, 43],
                '<?php
use some\a\{ClassA, ClassB, ClassC as C,};
use function some\a\{fn_a, fn_b, fn_c,};
use const some\a\{ConstA, ConstB, ConstC,};
                ',
            ],
            [
                [[1, 23, 43]],
                '<?php
use some\a\{ClassA, ClassB, ClassC as C,};
use function some\a\{fn_a, fn_b, fn_c,};
use const some\a\{ConstA, ConstB, ConstC,};
                ',
                true,
            ],
        ];
    }

    public function testGetClassyElementsWithMultipleNestedAnonymousClass(): void
    {
        $source = '<?php
class MyTestWithAnonymousClass extends TestCase
{
    public function setUp()
    {
        $provider = new class(function () {}) {};
    }

    public function testSomethingWithMoney(
        Money $amount
    ) {
        $a = new class(function () {
    new class(function () {
        new class(function () {})
        {
            const A=1;
        };
    })
    {
        const B=1;

        public function foo() {
            $c = new class() {const AA=3;};
            $d = new class {const AB=3;};
        }
    };
})
{
    const C=1;
};
    }
}';
        $tokens = Tokens::fromCode($source);
        $tokensAnalyzer = new TokensAnalyzer($tokens);
        $elements = $tokensAnalyzer->getClassyElements();

        static::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);

        static::assertSame($expected, $tokensAnalyzer->isSuperGlobal($index));
    }

    public function provideIsSuperGlobalCases(): array
    {
        $superNames = [
            '$_COOKIE',
            '$_ENV',
            '$_FILES',
            '$_GET',
            '$_POST',
            '$_REQUEST',
            '$_SERVER',
            '$_SESSION',
            '$GLOBALS',
        ];

        $cases = [];

        foreach ($superNames as $superName) {
            $cases[] = [
                true,
                sprintf('<?php echo %s[0];', $superName),
                3,
            ];
        }

        $notGlobalCodeCases = [
            '<?php echo 1; $a = static function($b) use ($a) { $a->$b(); }; // $_SERVER',
            '<?php class Foo{}?> <?php $_A = 1; /* $_SESSION */',
        ];

        foreach ($notGlobalCodeCases as $notGlobalCodeCase) {
            $tokensCount = \count(Tokens::fromCode($notGlobalCodeCase));

            for ($i = 0; $i < $tokensCount; ++$i) {
                $cases[] = [
                    false,
                    $notGlobalCodeCase,
                    $i,
                ];
            }
        }

        return $cases;
    }

    private function doIsConstantInvocationTest(array $expected, string $source): void
    {
        $tokens = Tokens::fromCode($source);

        static::assertCount(
            $tokens->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')
            );
        }
    }
}