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