packages/deriver/tests/Unit/Deriver/QueryTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver;
6
7use Deriver\Deriver\Query;
8use Deriver\Exception\InvalidInputException;
9use PHPUnit\Framework\Attributes\CoversClass;
10use PHPUnit\Framework\Attributes\DataProvider;
11use PHPUnit\Framework\TestCase;
12
13#[CoversClass(Query::class)]
14final class QueryTest extends TestCase
15{
16    public function testValidatePathRejectsAnUnanchoredEllipsis(): void
17    {
18        $this->expectException(InvalidInputException::class);
19        Query::validatePath(['...']);
20    }
21
22    public function testParseKeepsTheCallerPathAsSyntax(): void
23    {
24        self::assertSame(['entry', '...', 'bar'], Query::parse('entry -> ... -> bar -> foo#return')->path);
25        self::assertSame([], Query::parse('foo#return')->path);
26    }
27
28    public function testParseExtractsCallableSelectors(): void
29    {
30        $query = Query::parse('App\\Repository::find#parameter(id)');
31        self::assertSame('App\\Repository::find', $query->target);
32        self::assertNull($query->line);
33        self::assertNull($query->column);
34        self::assertSame('parameter', $query->selector);
35        self::assertSame(['id'], $query->arguments);
36    }
37
38    public function testParseExtractsUnquotedPathsAndCoordinatesWithoutReadingFiles(): void
39    {
40        $query = Query::parse('not present/source #1.php:25:5#argument(0)');
41        self::assertSame('not present/source #1.php', $query->target);
42        self::assertSame(25, $query->line);
43        self::assertSame(5, $query->column);
44        self::assertSame('argument', $query->selector);
45        self::assertSame([0], $query->arguments);
46    }
47
48    public function testParsePreservesSelectorSyntaxWithoutResolvingItsMeaning(): void
49    {
50        $query = Query::parse('App\\missing#selection(name, 2)');
51        self::assertSame('selection', $query->selector);
52        self::assertSame(['name', 2], $query->arguments);
53    }
54
55    #[DataProvider('providerRoundTrips')]
56    public function testToStringRoundTripsTheParsedFields(string $input, string $canonical): void
57    {
58        $query = Query::parse($input);
59        self::assertSame($canonical, $query->toString());
60        self::assertEquals($query, Query::parse($query->toString()));
61    }
62
63    /**
64     * @return iterable<string, array{string, string}>
65     */
66    public static function providerRoundTrips(): iterable
67    {
68        yield 'direct path' => ['bar->foo#return', 'bar -> foo#return'];
69        yield 'ancestor path' => ['entry -> ... -> bar -> foo#parameter(x)', 'entry -> ... -> bar -> foo#parameter(x)'];
70        yield 'call site path' => ['bar -> src/a.php:1:2#argument(0)', 'bar -> src/a.php:1:2#argument(0)'];
71        yield 'return' => ['App\\answer#return', 'App\\answer#return'];
72        yield 'parameter' => ['App\\Repository::find#parameter(id)', 'App\\Repository::find#parameter(id)'];
73        yield 'position' => ['src/Repository.php:25:5#argument(0)', 'src/Repository.php:25:5#argument(0)'];
74        yield 'named argument' => ['src/Repository.php:25:5#argument(sql)', 'src/Repository.php:25:5#argument(sql)'];
75        yield 'windows path' => ['C:\\source files\\Run.php:2:1#argument(1)', 'C:\\source files\\Run.php:2:1#argument(1)'];
76        yield 'leading namespace' => ['\\App\\answer#return', '\\App\\answer#return'];
77        yield 'whitespace' => [" \n App\\answer #return() \n", 'App\\answer#return'];
78        yield 'argument whitespace' => ['target#selection( name, 2 )', 'target#selection(name,2)'];
79        yield 'integer boundary' => ['target#selection(' . PHP_INT_MAX . ')', 'target#selection(' . PHP_INT_MAX . ')'];
80    }
81
82    #[DataProvider('providerInvalidSelectors')]
83    public function testParseRejectsMalformedSyntax(string $input): void
84    {
85        $this->expectException(InvalidInputException::class);
86        Query::parse($input);
87    }
88
89    /**
90     * @return iterable<string, array{string}>
91     */
92    public static function providerInvalidSelectors(): iterable
93    {
94        foreach (['-> foo#return', 'bar -> -> foo#return', 'bar -> #return', 'bar -> ...#return', '... -> foo#return', 'bar -> ... -> ... -> foo#return', 'bar#return -> foo#return', '', '#return', ' #return', 'target', 'target#', 'target#return(', 'target#return)', 'target#return()junk', 'target#parameter($id)', 'target#argument(-1)', 'target#argument(1.5)', 'target#argument(01)', 'target#selection(a,)', 'target#selection(a,,b)', 'target#selection(a b)', 'target#selection(nested(0))', 'file.php:0:1#argument(0)', 'file.php:1:0#argument(0)', 'file.php:01:1#argument(0)', '"file.php":1:1#argument(0)', "'file.php':1:1#argument(0)", ':1:1#argument(0)', "file\0.php:1:1#argument(0)", "file\n.php:1:1#argument(0)", 'target#argument(' . PHP_INT_MAX . '0)', 'file.php:' . PHP_INT_MAX . '0:1#argument(0)'] as $index => $input) {
95            yield 'invalid ' . $index => [$input];
96        }
97    }
98}
99