packages/deriver/tests/Unit/Deriver/Parser/TargetSyntaxTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver\Parser;
6
7use Deriver\Deriver\Parser\Parser;
8use Deriver\Deriver\Parser\ParserConfig;
9use Deriver\Deriver\Parser\TargetSyntax;
10use Deriver\Deriver\Sources\Source;
11use PHPUnit\Framework\Attributes\CoversClass;
12use PHPUnit\Framework\Attributes\DataProvider;
13use PHPUnit\Framework\Attributes\Small;
14use PHPUnit\Framework\Attributes\UsesClass;
15use PHPUnit\Framework\TestCase;
16
17/**
18 * @covers \Deriver\Deriver\Parser\TargetSyntax
19 */
20#[UsesClass(ParserConfig::class)]
21#[UsesClass(\Deriver\Exception\ParserException::class)]
22#[CoversClass(TargetSyntax::class)]
23#[UsesClass(Source::class)]
24#[UsesClass(Parser::class)]
25#[UsesClass(\Deriver\Deriver\Parser\Node\File::class)]
26#[Small]
27final class TargetSyntaxTest extends TestCase
28{
29    public function testEnterNodeRejectsPartialApplicationBeforeCallLowering(): void
30    {
31        $errors = new \PhpParser\ErrorHandler\Collecting();
32        $validator = new TargetSyntax(new ParserConfig('8.3'), $errors);
33        $validator->enterNode(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('f'), [new \PhpParser\Node\ArgPlaceholder()]));
34        self::assertCount(1, $errors->getErrors());
35        self::assertStringContainsString('8.6', $errors->getErrors()[0]->getRawMessage());
36    }
37    public function testEnterNodeAcceptsOrdinaryFirstClassCallablesButRejectsObjectCreationPlaceholders(): void
38    {
39        $errors = new \PhpParser\ErrorHandler\Collecting();
40        $validator = new TargetSyntax(new ParserConfig('8.3'), $errors);
41        $validator->enterNode(new \PhpParser\Node\Expr\FuncCall(new \PhpParser\Node\Name('f'), [new \PhpParser\Node\VariadicPlaceholder()]));
42        self::assertSame([], $errors->getErrors());
43        $validator->enterNode(new \PhpParser\Node\Expr\New_(new \PhpParser\Node\Name('Box'), [new \PhpParser\Node\VariadicPlaceholder()]));
44        self::assertCount(1, $errors->getErrors());
45    }
46    public function testMinimumIdentifiesNewerOperatorsAndAsymmetricProperties(): void
47    {
48        $validator = new TargetSyntax(new ParserConfig('8.3'), new \PhpParser\ErrorHandler\Collecting());
49        self::assertSame('8.5', $validator->minimum(new \PhpParser\Node\Expr\Cast\Void_(new \PhpParser\Node\Scalar\Int_(1))));
50        self::assertSame('8.4', $validator->minimum(new \PhpParser\Node\Stmt\Property(\PhpParser\Modifiers::PUBLIC | \PhpParser\Modifiers::PRIVATE_SET, [new \PhpParser\Node\PropertyItem('x')])));
51        self::assertSame('', $validator->minimum(new \PhpParser\Node\Scalar\Int_(1)));
52    }
53    public function testLeaveNodeRestoresTheConstantContextForOrdinaryCalls(): void
54    {
55        $errors = new \PhpParser\ErrorHandler\Collecting();
56        $visitor = new TargetSyntax(new ParserConfig('8.3'), $errors);
57        $parameter = new \PhpParser\Node\Param(new \PhpParser\Node\Expr\Variable('x'));
58        $visitor->enterNode($parameter);
59        self::assertSame(1, $visitor->initializers);
60        $visitor->leaveNode($parameter);
61        self::assertSame(0, $visitor->initializers);
62    }
63    public function testInitializerRecognizesConstantDeclarationsButNotExecutableStaticVariables(): void
64    {
65        $visitor = new TargetSyntax(new ParserConfig('8.3'), new \PhpParser\ErrorHandler\Collecting());
66        self::assertTrue($visitor->initializer(new \PhpParser\Node\Const_('F', new \PhpParser\Node\Scalar\Int_(1))));
67        self::assertFalse($visitor->initializer(new \PhpParser\Node\StaticVar(new \PhpParser\Node\Expr\Variable('f'))));
68    }
69    #[DataProvider('providerUnsupportedSyntax')]
70    public function testUnparenthesizedNewAndOtherUnsupportedSyntaxRaiseParserExceptions(string $source): void
71    {
72        $this->expectException(\Deriver\Exception\ParserException::class);
73        (new Parser(new ParserConfig('8.3')))->parse(new Source('x.php', $source));
74    }
75
76    /**
77     * @return iterable<string, array{string}>
78     */
79    public static function providerUnsupportedSyntax(): iterable
80    {
81        yield 'direct new' => ['<?php function target(){return new X()->f();}'];
82        yield 'new in argument' => ['<?php function target(){return f(new X()->f());}'];
83        yield 'callable constant' => ['<?php const F=strlen(...);'];
84        yield 'callable default' => ['<?php function target($f=strlen(...)){}'];
85    }
86
87    #[DataProvider('providerSupportedSyntax')]
88    public function testEnterNodeAllowsSupportedSyntax(string $source): void
89    {
90        $node = (new Parser(new ParserConfig('8.3')))->parse(new Source('x.php', $source));
91        self::assertCount(1, $node->stmts);
92    }
93
94    /**
95     * @return iterable<string, array{string}>
96     */
97    public static function providerSupportedSyntax(): iterable
98    {
99        yield 'parenthesized new' => ['<?php function target(){return (new X())->f();}'];
100        yield 'runtime callable' => ['<?php function target(){return strlen(...);}'];
101        yield 'runtime unpack' => ['<?php function target(){$x=7;try{return [...$x];}catch(Error $e){return 1;}}'];
102    }
103}
104