packages/sql-catalog/tests/Unit/Core/Php/DeclaredGlobalsTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Php;
6
7use PhpParser\Node\Stmt\Global_;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\DataProvider;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use SqlCatalog\Core\Php\DeclaredGlobals;
13use SqlCatalog\Core\Php\ParsedFile;
14use SqlCatalog\Core\Php\SourceParser;
15
16#[CoversClass(DeclaredGlobals::class)]
17#[UsesClass(ParsedFile::class)]
18#[UsesClass(SourceParser::class)]
19final class DeclaredGlobalsTest extends TestCase
20{
21    public function testClassOfNamesAGlobalAnExtensionDeclares(): void
22    {
23        $file = (new SourceParser())->parse('a.php', '<?php function f() { global $wpdb; }');
24        $statement = $file->statements[0];
25        self::assertInstanceOf(\PhpParser\Node\Stmt\Function_::class, $statement);
26        $declaration = $statement->stmts[0];
27        self::assertInstanceOf(Global_::class, $declaration);
28
29        self::assertSame('wpdb', (new DeclaredGlobals(['wpdb' => 'wpdb']))->classOf($declaration, 'wpdb'));
30    }
31
32    public function testClassOfIsSilentWhenNothingSaysWhatTheGlobalHolds(): void
33    {
34        $file = (new SourceParser())->parse('a.php', '<?php function f() { global $other; }');
35        $statement = $file->statements[0];
36        self::assertInstanceOf(\PhpParser\Node\Stmt\Function_::class, $statement);
37        $declaration = $statement->stmts[0];
38        self::assertInstanceOf(Global_::class, $declaration);
39
40        self::assertNull((new DeclaredGlobals())->classOf($declaration, 'other'));
41    }
42
43    public function testDeclaredNamesTheClassAnExtensionGivesAGlobal(): void
44    {
45        $globals = new DeclaredGlobals(['wpdb' => 'App\\Db', 'cache' => 'App\\Cache']);
46
47        self::assertSame('App\\Db', $globals->declared('wpdb'));
48        self::assertSame('App\\Cache', $globals->declared('cache'));
49    }
50
51    public function testDeclaredIsSilentForAGlobalNoExtensionDeclares(): void
52    {
53        self::assertNull((new DeclaredGlobals(['wpdb' => 'App\\Db']))->declared('other'));
54        self::assertNull((new DeclaredGlobals(['wpdb' => 'App\\Db']))->declared('$wpdb'));
55        self::assertNull((new DeclaredGlobals())->declared('wpdb'));
56    }
57
58    public function testDocumentedReadsTheTagOnTheEnclosingDeclaration(): void
59    {
60        $file = (new SourceParser())->parse(
61            'a.php',
62            '<?php' . "\n" . '/** @global \\App\\Db $db */' . "\n" . 'function f() { global $db; }',
63        );
64        $statement = $file->statements[0];
65        self::assertInstanceOf(\PhpParser\Node\Stmt\Function_::class, $statement);
66        $declaration = $statement->stmts[0];
67        self::assertInstanceOf(Global_::class, $declaration);
68
69        self::assertSame('App\\Db', (new DeclaredGlobals())->documented($declaration, 'db'));
70    }
71
72    public function testDocumentedIsSilentWhenNoTagNamesTheVariable(): void
73    {
74        $file = (new SourceParser())->parse(
75            'a.php',
76            '<?php' . "\n" . '/** @global wpdb $wpdb */' . "\n" . 'function f() { global $db; }',
77        );
78        $statement = $file->statements[0];
79        self::assertInstanceOf(\PhpParser\Node\Stmt\Function_::class, $statement);
80        $declaration = $statement->stmts[0];
81        self::assertInstanceOf(Global_::class, $declaration);
82
83        self::assertNull((new DeclaredGlobals())->documented($declaration, 'db'));
84    }
85
86    public function testCommentsClimbToTheDeclarationTheGlobalIsWrittenIn(): void
87    {
88        $file = (new SourceParser())->parse(
89            'a.php',
90            '<?php' . "\n" . '/** @global wpdb $wpdb */' . "\n" . 'function f() { /** @var wpdb $wpdb */ global $wpdb; }',
91        );
92        $statement = $file->statements[0];
93        self::assertInstanceOf(\PhpParser\Node\Stmt\Function_::class, $statement);
94        $declaration = $statement->stmts[0];
95        self::assertInstanceOf(Global_::class, $declaration);
96
97        self::assertSame(
98            ['/** @var wpdb $wpdb */', '/** @global wpdb $wpdb */'],
99            (new DeclaredGlobals())->comments($declaration),
100        );
101    }
102
103    /**
104     * @return list<array{string, string, string|null}>
105     */
106    public static function providerTagged(): array
107    {
108        return [
109            ['/** @global wpdb $wpdb */', 'wpdb', 'wpdb'],
110            ['/** @var \\App\\Db $db */', 'db', 'App\\Db'],
111            ['/** @global wpdb|null $wpdb */', 'wpdb', 'wpdb'],
112            ['/** @global wpdb $wpdb */', 'other', null],
113            ['/** nothing */', 'wpdb', null],
114            ['/** @global wpdb $wpdbx */', 'wpdb', null],
115        ];
116    }
117
118    #[DataProvider('providerTagged')]
119    public function testTaggedReadsTheClassOutOfADocComment(string $comment, string $name, ?string $expected): void
120    {
121        self::assertSame($expected, (new DeclaredGlobals())->tagged($comment, $name));
122    }
123}
124