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