packages/deriver/tests/Unit/Deriver/Resolver/ClassScopeTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Resolver;
6
7use Deriver\Deriver\Parser\Node\File;
8use Deriver\Deriver\Parser\Parser;
9use Deriver\Deriver\Parser\ParserConfig;
10use Deriver\Deriver\Parser\TargetSyntax;
11use Deriver\Deriver\Resolver\AssignmentPatterns;
12use Deriver\Deriver\Resolver\ClassScope;
13use Deriver\Deriver\Resolver\ExpressionValidation;
14use Deriver\Deriver\Resolver\MagicContext;
15use Deriver\Deriver\Resolver\Resolver;
16use Deriver\Deriver\Sources\Source;
17use Deriver\Exception\ResolutionException;
18use PhpParser\ErrorHandler\Collecting;
19use PhpParser\Node\Expr\Closure;
20use PhpParser\Node\Name;
21use PhpParser\Node\Stmt\Class_;
22use PhpParser\Node\Stmt\ClassMethod;
23use PhpParser\Node\Stmt\Function_;
24use PHPUnit\Framework\Attributes\CoversClass;
25use PHPUnit\Framework\Attributes\DataProvider;
26use PHPUnit\Framework\Attributes\Small;
27use PHPUnit\Framework\Attributes\UsesClass;
28use PHPUnit\Framework\TestCase;
29
30#[UsesClass(Resolver::class)]
31#[UsesClass(ExpressionValidation::class)]
32#[UsesClass(ResolutionException::class)]
33#[UsesClass(ParserConfig::class)]
34#[CoversClass(ClassScope::class)]
35#[UsesClass(Parser::class)]
36#[UsesClass(File::class)]
37#[UsesClass(MagicContext::class)]
38#[UsesClass(AssignmentPatterns::class)]
39#[UsesClass(TargetSyntax::class)]
40#[UsesClass(Source::class)]
41#[Small]
42final class ClassScopeTest extends TestCase
43{
44    #[DataProvider('providerProgramsClassNameProgramsinvalid')]
45    public function testEnterNodeRejectsUndefinedLexicalClassReferences(string $source, string $message): void
46    {
47        $this->expectException(ResolutionException::class);
48        $this->expectExceptionMessage($message);
49        (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', $source)));
50    }
51    public function testLeaveNodeRestoresTheEnclosingClassAfterClosuresAndNestedFunctions(): void
52    {
53        $errors = new Collecting();
54        $visitor = new ClassScope($errors);
55        $class = new Class_('Box', ['extends' => new Name('Base')]);
56        $method = new ClassMethod('value');
57        $closure = new Closure();
58        $function = new Function_('nested');
59        $visitor->enterNode($class);
60        $visitor->enterNode($method);
61        $methodScope = $visitor->scope;
62        $visitor->enterNode($closure);
63        $closureScope = $visitor->scope;
64        $visitor->leaveNode($closure);
65        $restored = $visitor->scope;
66        $visitor->enterNode($function);
67        $functionScope = $visitor->scope;
68        $visitor->leaveNode($function);
69        $visitor->leaveNode($method);
70        $visitor->leaveNode($class);
71        self::assertSame(['class' => true, 'parent' => true, 'trait' => false, 'function' => 'method'], $methodScope);
72        self::assertSame(['class' => true, 'parent' => true, 'trait' => false, 'function' => 'closure'], $closureScope);
73        self::assertSame($methodScope, $restored);
74        self::assertSame(['class' => false, 'parent' => false, 'trait' => false, 'function' => 'function'], $functionScope);
75        self::assertSame(['class' => false, 'parent' => false, 'trait' => false, 'function' => 'script'], $visitor->scope);
76        self::assertSame([], $visitor->stack);
77        self::assertSame([], $errors->getErrors());
78    }
79    #[DataProvider('providerRebindableScopes')]
80    public function testValidateDefersRebindableScopesAndOrdinaryClassNames(string $source): void
81    {
82        $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', $source)));
83        self::assertSame($source, $tree->source->contents);
84        self::assertNotEmpty($tree->stmts);
85    }
86    /**
87     * @return iterable<string,array{string}>
88     */
89    public static function providerRebindableScopes(): iterable
90    {
91        yield 'script' => ['<?php return self::class;'];
92        yield 'closure' => ['<?php function target(){return function(){return parent::class;};}'];
93        yield 'arrow' => ['<?php function target(){return fn()=>static::class;}'];
94        yield 'trait' => ['<?php trait T{function value(){return parent::class;}}'];
95        yield 'parent method' => ['<?php class Base{}class Box extends Base{function value(){return parent::class;}}'];
96        yield 'ordinary unknown name' => ['<?php function target(){return Missing::class;}'];
97        yield 'dynamic name' => ['<?php function target($x){return $x::class;}'];
98        yield 'anonymous class' => ['<?php function target(){return new class{function value(){return self::class;}};}'];
99    }
100    /**
101     * @return array<string,array{string,string}> Invalid source and required compiler diagnostic fragment
102     */
103    public static function providerProgramsClassNameProgramsinvalid(): array
104    {
105        return ['free self' => ['<?php function target(){return self::class;}', 'no class scope'], 'free parent' => ['<?php function target(){return parent::class;}', 'no class scope'], 'free static' => ['<?php function target(){return static::class;}', 'no class scope'], 'missing parent' => ['<?php class Box{function value(){return parent::class;}}function target(){return 1;}', 'has no parent'], 'nested named function' => ['<?php class Box{function value(){function nested(){return self::class;}}}function target(){return 1;}', 'no class scope'], 'free static call' => ['<?php function target(){return self::value();}', 'no class scope'], 'free allocation' => ['<?php function target(){return new self;}', 'no class scope'], 'free static property' => ['<?php function target(){return parent::$value;}', 'no class scope'], 'free type test' => ['<?php function target(){return new stdClass instanceof self;}', 'no class scope'], 'missing parent allocation' => ['<?php class Box{function value(){return new parent;}}function target(){return 1;}', 'has no parent'], 'missing parent initializer' => ['<?php class Box{const VALUE=parent::class;}function target(){return 1;}', 'has no parent'], 'free default initializer' => ['<?php function value($x=self::class){}function target(){return 1;}', 'no class scope']];
106    }
107}
108