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