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