packages/deriver/tests/Unit/Deriver/Lowerer/Declaration/CallSiteIndexTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Lowerer\Declaration;
6
7use Deriver\Deriver\Evaluator\TargetProfile;
8use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
9use Deriver\Deriver\Lowerer\Declaration\CallableSource;
10use Deriver\Deriver\Lowerer\Declaration\CallSiteIndex;
11use Deriver\Deriver\Lowerer\Declaration\Index;
12use Deriver\Deriver\Lowerer\Declaration\Indexer;
13use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
14use Deriver\Deriver\Lowerer\EffectInspection;
15use Deriver\Deriver\Parser\Node\File;
16use Deriver\Deriver\Parser\Parser;
17use Deriver\Deriver\Parser\ParserConfig;
18use Deriver\Deriver\Parser\TargetSyntax;
19use Deriver\Deriver\Resolver\AssignmentPatterns;
20use Deriver\Deriver\Resolver\ClassScope;
21use Deriver\Deriver\Resolver\ExpressionValidation;
22use Deriver\Deriver\Resolver\MagicContext;
23use Deriver\Deriver\Resolver\Resolver;
24use Deriver\Deriver\Sources\LineMap;
25use Deriver\Deriver\Sources\Path;
26use Deriver\Deriver\Sources\Source;
27use Deriver\Deriver\Sources\Sources;
28use Deriver\Exception\ParserException;
29use Deriver\Exception\ResolutionException;
30use PhpParser\Node\Expr\FuncCall;
31use PhpParser\Node\Name;
32use PHPUnit\Framework\Attributes\CoversClass;
33use PHPUnit\Framework\Attributes\DataProvider;
34use PHPUnit\Framework\Attributes\Small;
35use PHPUnit\Framework\Attributes\UsesClass;
36use PHPUnit\Framework\TestCase;
37
38/**
39 * @covers \Deriver\Deriver\Lowerer\Declaration\CallSiteIndex
40 */
41#[UsesClass(Resolver::class)]
42#[UsesClass(ExpressionValidation::class)]
43#[UsesClass(ResolutionException::class)]
44#[UsesClass(ParserConfig::class)]
45#[UsesClass(ParserException::class)]
46#[CoversClass(CallSiteIndex::class)]
47#[UsesClass(CallableIdentity::class)]
48#[UsesClass(Sources::class)]
49#[UsesClass(Path::class)]
50#[UsesClass(Source::class)]
51#[UsesClass(TargetProfile::class)]
52#[UsesClass(Parser::class)]
53#[UsesClass(File::class)]
54#[UsesClass(EffectInspection::class)]
55#[UsesClass(CallableSource::class)]
56#[UsesClass(Indexer::class)]
57#[UsesClass(Index::class)]
58#[UsesClass(Composition::class)]
59#[UsesClass(LineMap::class)]
60#[UsesClass(MagicContext::class)]
61#[UsesClass(AssignmentPatterns::class)]
62#[UsesClass(ClassScope::class)]
63#[UsesClass(TargetSyntax::class)]
64#[Small]
65final class CallSiteIndexTest extends TestCase
66{
67 public function testOwnersIndexesNestedClosuresAsSeparateOwners(): void
68 {
69 $source = '<?php function target(){return fn()=>sink(1);}';
70 $snapshotId = 'test';
71 $input = new Sources([new Source('fixture.php', $source)]);
72 $profile = new TargetProfile();
73 $parser = new Parser(new ParserConfig($profile->version));
74 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
75 $index = new Index($snapshotId, $trees);
76 $owners = (new CallSiteIndex($index))->owners('sink');
77 self::assertCount(1, $owners);
78 self::assertStringStartsWith('closure:fixture.php:', $owners[0]);
79 self::assertSame(0, $index->graphCount());
80 }
81 public function testContainsDoesNotAttributeANestedFunctionCallToItsParent(): void
82 {
83 $source1 = '<?php function target(){function nested(){sink(1);}}';
84 $snapshotId = 'test';
85 $input = new Sources([new Source('fixture.php', $source1)]);
86 $profile = new TargetProfile();
87 $parser = new Parser(new ParserConfig($profile->version));
88 $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input, false));
89 $index = new Index($snapshotId, $trees);
90 $source = $index->declarations['target'];
91 $pending = [];
92 self::assertFalse((new CallSiteIndex($index))->contains($source->node, 'sink', $source, $pending, true));
93 }
94 #[DataProvider('providerNamespacedCalls')]
95 public function testOwnersFindsResolvedNamespaceAndImportedNamesWithoutCompilingBodies(string $source, string $selector): void
96 {
97 $snapshotId = 'test';
98 $input = new Sources([new Source('fixture.php', $source)]);
99 $profile = new TargetProfile();
100 $parser = new Parser(new ParserConfig($profile->version));
101 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
102 $index = new Index($snapshotId, $trees);
103 self::assertSame(['N\target'], (new CallSiteIndex($index))->owners($selector));
104 self::assertSame(0, $index->graphCount());
105 }
106 /**
107 * @return iterable<string,array{string,string}>
108 */
109 public static function providerNamespacedCalls(): iterable
110 {
111 yield 'implicit namespace' => ['<?php namespace N;function target(){sink(1);}', 'N\sink'];
112 yield 'case-insensitive qualified selector' => ['<?php namespace N;function target(){sink(1);}', '\n\SINK'];
113 yield 'qualified syntax' => ['<?php namespace N;function target(){\N\sink(1);}', 'N\sink'];
114 yield 'function import' => ['<?php namespace N;use function M\sink as alias;function target(){alias(1);}', 'M\sink'];
115 yield 'namespace-relative syntax' => ['<?php namespace N;function target(){namespace\sink(1);}', 'N\sink'];
116 yield 'ordinary method name' => ['<?php namespace N;function target($x){$x->sink(1);}', 'SINK'];
117 }
118 public function testFallbackOnlyMatchesUnqualifiedFunctionNames(): void
119 {
120 $source = '<?php function target(){return 1;}';
121 $snapshotId = 'test';
122 $input = new Sources([new Source('fixture.php', $source)]);
123 $profile = new TargetProfile();
124 $parser = new Parser(new ParserConfig($profile->version));
125 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
126 $index = new Index($snapshotId, $trees);
127 $calls = new CallSiteIndex($index);
128 self::assertTrue($calls->fallback(new FuncCall(new Name('sink')), 'sink'));
129 self::assertFalse($calls->fallback(new FuncCall(new Name('Local\sink')), 'sink'));
130 }
131 #[DataProvider('providerCreations')]
132 public function testCreationMatchesConstructorSelectors(string $class, string $selector, bool $expected): void
133 {
134 $source = '<?php function target(){return 1;}';
135 $snapshotId = 'test';
136 $input = new Sources([new Source('fixture.php', $source)]);
137 $profile = new TargetProfile();
138 $parser = new Parser(new ParserConfig($profile->version));
139 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
140 self::assertSame($expected, (new CallSiteIndex(new Index($snapshotId, $trees)))->creation(new Name($class), $selector));
141 }
142 /**
143 * @return iterable<string,array{string,string,bool}>
144 */
145 public static function providerCreations(): iterable
146 {
147 yield 'wildcard' => ['N\Repo', '*', true];
148 yield 'constructor' => ['N\Repo', '\n\REPO::__Construct', true];
149 yield 'class name' => ['N\Repo', 'N\Repo', false];
150 yield 'other class' => ['N\Repo', 'N\Other::__construct', false];
151 yield 'lexical class' => ['self', 'N\Repo::__construct', true];
152 yield 'lexical class by function name' => ['self', 'sink', false];
153 }
154 public function testOwnersFindsCreationsWithoutCompilingBodies(): void
155 {
156 $source = '<?php namespace N;class Repo {} function target(){return new Repo;} function other($c){return new $c;}';
157 $snapshotId = 'test';
158 $input = new Sources([new Source('fixture.php', $source)]);
159 $profile = new TargetProfile();
160 $parser = new Parser(new ParserConfig($profile->version));
161 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
162 $index = new Index($snapshotId, $trees);
163 self::assertSame(['N\target'], (new CallSiteIndex($index))->owners('N\Repo::__construct'));
164 self::assertSame(['N\target'], (new CallSiteIndex($index))->owners('*'));
165 self::assertSame(0, $index->graphCount());
166 }
167}
168