packages/deriver/tests/Integration/SelectorQueryTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Integration;
6
7use Deriver\Deriver;
8use Deriver\Deriver\Query;
9use Deriver\Deriver\Sources\Path;
10use Deriver\Exception\InvalidInputException;
11use org\bovigo\vfs\vfsStream;
12use Override;
13use phpmock\Mock;
14use phpmock\MockBuilder;
15use PHPUnit\Framework\Attributes\CoversNothing;
16use PHPUnit\Framework\Attributes\PreserveGlobalState;
17use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
18use PHPUnit\Framework\TestCase;
19
20#[CoversNothing]
21#[RunTestsInSeparateProcesses]
22#[PreserveGlobalState(false)]
23final class SelectorQueryTest extends TestCase
24{
25    /**
26     * @throws \phpmock\MockEnabledException
27     */
28    public function testPreparedRelativeSourcesAndSelectorsKeepTheirOriginalDirectory(): void
29    {
30        $workingDirectory = '/project';
31        $filesystem = vfsStream::setup('project', null, ['src' => []]);
32        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
33            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
34        })->build()->enable();
35        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
36            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
37        })->build()->enable();
38        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
39            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
40        })->build()->enable();
41        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
42            return $workingDirectory;
43        })->build()->enable();
44        $source = $filesystem;
45        $name = 'src/a.php';
46        $contents = "<?php\nunknown('captured');\n";
47        $url = $source->url() . '/' . $name;
48        file_put_contents($url, $contents);
49        $path = '/' . $source->path() . '/' . $name;
50        $other = vfsStream::newDirectory('other/src');
51        $other->at($source);
52        $name1 = 'src/a.php';
53        $contents1 = "<?php\nunknown('different');\n";
54        $url1 = $other->url() . '/' . $name1;
55        file_put_contents($url1, $contents1);
56        $cwd = $workingDirectory;
57        self::assertNotFalse($cwd);
58        try {
59            $workingDirectory = '/' . $source->path();
60            $deriver = Deriver::prepare(['src/a.php']);
61            $path1 = $path;
62            $path1 = str_starts_with($path1, '/') ? $path1 : $workingDirectory . '/' . $path1;
63            self::assertTrue(unlink('vfs:/' . Path::normalize($path1)));
64            $workingDirectory = '/' . $other->path();
65            self::assertSame('captured', $deriver->derive('src/a.php:2:1#argument(0)')[0]->result);
66            self::assertSame('captured', $deriver->derive($path . ':2:1#argument(0)')[0]->result);
67        } finally {
68            $workingDirectory = $cwd;
69        }
70    }
71    /**
72     * @throws \phpmock\MockEnabledException
73     */
74    public function testDeriveSelectsNamespacedReturnsAndFormalParameterOrigins(): void
75    {
76        $workingDirectory = '/project';
77        $filesystem = vfsStream::setup('project');
78        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
79            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
80        })->build()->enable();
81        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
82            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
83        })->build()->enable();
84        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
85            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
86        })->build()->enable();
87        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
88            return $workingDirectory;
89        })->build()->enable();
90        $source = $filesystem;
91        $name = 'app.php';
92        $contents = '<?php namespace App; class Repository { public static function find($id){return $id * 2;} } Repository::find(3); Repository::find(5);';
93        $url = $source->url() . '/' . $name;
94        file_put_contents($url, $contents);
95        $path = '/' . $source->path() . '/' . $name;
96        $deriver = Deriver::prepare([$path]);
97        $parameters = $deriver->derive('App\Repository::find#parameter(id)');
98        $returns = $deriver->derive('App\Repository::find#return');
99        self::assertSame([3, 5], array_column(iterator_to_array($parameters, false), 'result'));
100        self::assertSame([6, 10], array_column(iterator_to_array($returns, false), 'result'));
101    }
102    /**
103     * @throws \phpmock\MockEnabledException
104     */
105    public function testDeriveSelectsArgumentExpressionsWithoutExecutingTheCall(): void
106    {
107        $workingDirectory = '/project';
108        $filesystem = vfsStream::setup('project');
109        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
110            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
111        })->build()->enable();
112        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
113            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
114        })->build()->enable();
115        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
116            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
117        })->build()->enable();
118        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
119            return $workingDirectory;
120        })->build()->enable();
121        $source = $filesystem;
122        $name = 'source #1.php';
123        $contents = "<?php\n\$x = 3;\nunknown(\$x++, \$x);\n";
124        $url = $source->url() . '/' . $name;
125        file_put_contents($url, $contents);
126        $path = '/' . $source->path() . '/' . $name;
127        $files = [$path];
128        $deriver = Deriver::prepare($files);
129        self::assertSame(3, $deriver->derive($path . ':3:1#argument(0)')[0]->result);
130        self::assertSame(4, $deriver->derive($path . ':3:1#argument(1)')[0]->result);
131    }
132    /**
133     * @throws \phpmock\MockEnabledException
134     */
135    public function testDeriveResolvesRelativePathsAgainstTheWorkingDirectory(): void
136    {
137        $workingDirectory = '/project';
138        $filesystem = vfsStream::setup('project', null, ['src' => []]);
139        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
140            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
141        })->build()->enable();
142        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
143            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
144        })->build()->enable();
145        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
146            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
147        })->build()->enable();
148        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
149            return $workingDirectory;
150        })->build()->enable();
151        $source = $filesystem;
152        $name = 'src/a.php';
153        $contents = "<?php\nunknown(sql: 'SELECT 1');\n";
154        $url = $source->url() . '/' . $name;
155        file_put_contents($url, $contents);
156        $path = '/' . $source->path() . '/' . $name;
157        $cwd = $workingDirectory;
158        self::assertNotFalse($cwd);
159        $workingDirectory = '/' . $source->path();
160        try {
161            $query = Query::parse('src/./a.php:2:1#argument(sql)');
162            self::assertSame('SELECT 1', Deriver::prepare([$path])->derive($query->toString())[0]->result);
163            self::assertSame('SELECT 1', Deriver::prepare(['src/a.php'])->derive($path . ':2:1#argument(0)')[0]->result);
164        } finally {
165            $workingDirectory = $cwd;
166        }
167    }
168    /**
169     * @throws \phpmock\MockEnabledException
170     */
171    public function testDeriveFindsArgumentsInsideClosures(): void
172    {
173        $workingDirectory = '/project';
174        $filesystem = vfsStream::setup('project');
175        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
176            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
177        })->build()->enable();
178        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
179            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
180        })->build()->enable();
181        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
182            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
183        })->build()->enable();
184        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
185            return $workingDirectory;
186        })->build()->enable();
187        $source = $filesystem;
188        $name = 'a.php';
189        $contents = "<?php\n\$run = function () {\n    unknown('inside');\n};\n";
190        $url = $source->url() . '/' . $name;
191        file_put_contents($url, $contents);
192        $path = '/' . $source->path() . '/' . $name;
193        self::assertSame('inside', Deriver::prepare([$path])->derive($path . ':3:5#argument(0)')[0]->result);
194    }
195    /**
196     * @throws \phpmock\MockEnabledException
197     */
198    public function testDeriveUsesByteColumnsWithUtf8AndCrLfSource(): void
199    {
200        $workingDirectory = '/project';
201        $filesystem = vfsStream::setup('project');
202        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
203            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
204        })->build()->enable();
205        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
206            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
207        })->build()->enable();
208        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
209            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
210        })->build()->enable();
211        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
212            return $workingDirectory;
213        })->build()->enable();
214        $source = $filesystem;
215        $prefix = '$label = "日本語"; ';
216        $name = 'a.php';
217        $contents = "<?php\r\n" . $prefix . "unknown('sql');\r\n";
218        $url = $source->url() . '/' . $name;
219        file_put_contents($url, $contents);
220        $path = '/' . $source->path() . '/' . $name;
221        $query = $path . ':2:' . (strlen($prefix) + 1) . '#argument(0)';
222        self::assertSame('sql', Deriver::prepare([$path])->derive($query)[0]->result);
223    }
224    /**
225     * @throws \phpmock\MockEnabledException
226     */
227    public function testDeriveRejectsCallsSharingTheSameStartPosition(): void
228    {
229        $workingDirectory = '/project';
230        $filesystem = vfsStream::setup('project');
231        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
232            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
233        })->build()->enable();
234        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
235            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
236        })->build()->enable();
237        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
238            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
239        })->build()->enable();
240        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
241            return $workingDirectory;
242        })->build()->enable();
243        $source = $filesystem;
244        $name = 'a.php';
245        $contents = "<?php\nfactory()->send('sql');\n";
246        $url = $source->url() . '/' . $name;
247        file_put_contents($url, $contents);
248        $path = '/' . $source->path() . '/' . $name;
249        $this->expectException(InvalidInputException::class);
250        $this->expectExceptionMessage('exactly one captured call');
251        Deriver::prepare([$path])->derive($path . ':2:1#argument(0)');
252    }
253    /**
254     * @throws \phpmock\MockEnabledException
255     */
256    public function testDeriveDoesNotAddAFileNamedOnlyByTheSelector(): void
257    {
258        $workingDirectory = '/project';
259        $filesystem = vfsStream::setup('project');
260        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
261            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
262        })->build()->enable();
263        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
264            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
265        })->build()->enable();
266        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
267            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
268        })->build()->enable();
269        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
270            return $workingDirectory;
271        })->build()->enable();
272        $source = $filesystem;
273        $name = 'selected.php';
274        $contents = '<?php unknown(1);';
275        $url = $source->url() . '/' . $name;
276        file_put_contents($url, $contents);
277        $selected = '/' . $source->path() . '/' . $name;
278        $name1 = 'excluded.php';
279        $contents1 = '<?php unknown(2);';
280        $url1 = $source->url() . '/' . $name1;
281        file_put_contents($url1, $contents1);
282        $excluded = '/' . $source->path() . '/' . $name1;
283        $this->expectException(InvalidInputException::class);
284        Deriver::prepare([$selected])->derive($excluded . ':1:7#argument(0)');
285    }
286    /**
287     * @throws \phpmock\MockEnabledException
288     */
289    public function testDeriveRejectsAnUnknownFormalParameter(): void
290    {
291        $workingDirectory = '/project';
292        $filesystem = vfsStream::setup('project');
293        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
294            return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
295        })->build()->enable();
296        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
297            return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
298        })->build()->enable();
299        (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
300            return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
301        })->build()->enable();
302        (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
303            return $workingDirectory;
304        })->build()->enable();
305        $source = $filesystem;
306        $name = 'a.php';
307        $contents = '<?php function find($id){return $id;}';
308        $url = $source->url() . '/' . $name;
309        file_put_contents($url, $contents);
310        $path = '/' . $source->path() . '/' . $name;
311        $this->expectException(InvalidInputException::class);
312        Deriver::prepare([$path])->derive('find#parameter(missing)');
313    }
314    #[Override]
315    protected function tearDown(): void
316    {
317        Mock::disableAll();
318        parent::tearDown();
319    }
320}
321