packages/deriver/tests/Unit/Deriver/Resolver/ResolverTest.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\Node;
19use PhpParser\NodeFinder;
20use PHPUnit\Framework\Attributes\CoversClass;
21use PHPUnit\Framework\Attributes\DataProvider;
22use PHPUnit\Framework\Attributes\Small;
23use PHPUnit\Framework\Attributes\UsesClass;
24use PHPUnit\Framework\TestCase;
25
26#[CoversClass(Resolver::class)]
27#[UsesClass(Parser::class)]
28#[UsesClass(ParserConfig::class)]
29#[UsesClass(TargetSyntax::class)]
30#[UsesClass(File::class)]
31#[UsesClass(Source::class)]
32#[UsesClass(ResolutionException::class)]
33#[UsesClass(AssignmentPatterns::class)]
34#[UsesClass(ClassScope::class)]
35#[UsesClass(ExpressionValidation::class)]
36#[UsesClass(MagicContext::class)]
37#[Small]
38final class ResolverTest extends TestCase
39{
40    public function testResolveBindsNamesAndMagicContextWithoutMutatingParsedSyntax(): void
41    {
42        $source = new Source('a.php', '<?php namespace App; use Vendor\Thing as Alias; #[Alias] class Box { #[Alias] function answer(#[Alias] ?Alias $arg): Alias|null { return [new Alias(), __METHOD__]; } } const FLAG = 1;');
43        $parsed = (new Parser(new ParserConfig('8.3')))->parse($source);
44        $before = serialize($parsed);
45        $resolved = (new Resolver())->resolve($parsed);
46        $finder = new NodeFinder();
47        $creation = $finder->findFirstInstanceOf([$resolved], Node\Expr\New_::class);
48        $magic = $finder->findFirstInstanceOf([$resolved], Node\Scalar\MagicConst\Method::class);
49        self::assertInstanceOf(Node\Expr\New_::class, $creation);
50        self::assertInstanceOf(Node\Name\FullyQualified::class, $creation->class);
51        self::assertSame('Vendor\Thing', $creation->class->toString());
52        self::assertInstanceOf(Node\Scalar\MagicConst\Method::class, $magic);
53        self::assertSame(['namespace' => 'App', 'class' => 'App\Box', 'function' => 'answer', 'method' => 'App\Box::answer', 'property' => ''], $magic->getAttribute('deriver-lexical'));
54        self::assertSame($source, $resolved->source);
55        self::assertSame($before, serialize($parsed));
56    }
57
58    #[DataProvider('providerInvalidSources')]
59    public function testResolveReportsSemanticErrorsWithTheirSourceLocation(string $contents): void
60    {
61        $parsed = (new Parser(new ParserConfig('8.3')))->parse(new Source('invalid.php', $contents));
62        $this->expectException(ResolutionException::class);
63        $this->expectExceptionMessage('invalid.php:2:');
64        (new Resolver())->resolve($parsed);
65    }
66
67    public function testResolveResetsScopeAndDiagnosticsAfterAFailure(): void
68    {
69        $parser = new Parser(new ParserConfig('8.3'));
70        $resolver = new Resolver();
71        $invalid = $parser->parse(new Source('bad.php', '<?php namespace Broken; use A as Alias; use B as Alias;'));
72        try {
73            $resolver->resolve($invalid);
74            self::fail('Duplicate aliases must fail during resolution.');
75        } catch (ResolutionException $error) {
76            self::assertSame('bad.php', $error->path);
77        }
78        $valid = $parser->parse(new Source('good.php', '<?php namespace Valid; function f() { return __FUNCTION__; }'));
79        self::assertSame(serialize((new Resolver())->resolve($valid)), serialize($resolver->resolve($valid)));
80    }
81
82    /**
83     * @return iterable<string, array{string}>
84     */
85    public static function providerInvalidSources(): iterable
86    {
87        yield 'duplicate alias' => ["<?php\nuse A as Alias; use B as Alias;"];
88        yield 'class scope' => ["<?php\nfunction f() { return self::class; }"];
89        yield 'literal unpack' => ["<?php\nreturn [...7];"];
90        yield 'assignment pattern' => ["<?php\n[&\$x] = [1];"];
91    }
92}
93