packages/sql-catalog/tests/Unit/Core/Analysis/ConstantReaderTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Analysis;
6
7use PhpParser\Node\Expr\ClassConstFetch;
8use PhpParser\Node\Expr\ConstFetch;
9use PhpParser\Node\Expr\Variable;
10use PhpParser\Node\Name;
11use PhpParser\NodeFinder;
12use PHPUnit\Framework\Attributes\CoversClass;
13use PHPUnit\Framework\Attributes\UsesClass;
14use PHPUnit\Framework\TestCase;
15use SqlCatalog\Core\Analysis\ConstantReader;
16use SqlCatalog\Core\Analysis\FunctionScope;
17use SqlCatalog\Core\Analysis\Interpreter;
18use SqlCatalog\Core\Php\NodeText;
19use SqlCatalog\Core\Php\ProgramIndex;
20use SqlCatalog\Core\Php\ProgramIndexBuilder;
21use SqlCatalog\Core\Php\SourceParser;
22
23#[CoversClass(ConstantReader::class)]
24#[UsesClass(\SqlCatalog\Core\Analysis\CallEvaluator::class)]
25#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\CalleeReturns::class)]
26#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\CallerIndex::class)]
27#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Callers::class)]
28#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Deriver::class)]
29#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\EntryBinder::class)]
30#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\FreeNames::class)]
31#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\ModifiedNames::class)]
32#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\PropertyWrites::class)]
33#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\SliceExecutor::class)]
34#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\AssignmentSteps::class)]
35#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\BackwardSlicer::class)]
36#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\LoopPasses::class)]
37#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\SourceTree::class)]
38#[UsesClass(\SqlCatalog\Core\Analysis\EvaluationBudget::class)]
39#[UsesClass(\SqlCatalog\Core\Analysis\ExpressionEvaluator::class)]
40#[UsesClass(FunctionScope::class)]
41#[UsesClass(Interpreter::class)]
42#[UsesClass(\SqlCatalog\Core\Analysis\ReferenceEvaluator::class)]
43#[UsesClass(\SqlCatalog\Core\Analysis\SinkFinder::class)]
44#[UsesClass(\SqlCatalog\Core\Analysis\SinkMatcher::class)]
45#[UsesClass(\SqlCatalog\Core\Evaluation\Domain::class)]
46#[UsesClass(\SqlCatalog\Core\Evaluation\Environment::class)]
47#[UsesClass(\SqlCatalog\Core\Evaluation\LiteralTerm::class)]
48#[UsesClass(\SqlCatalog\Core\Evaluation\OpaqueTerm::class)]
49#[UsesClass(\SqlCatalog\Core\Evaluation\ObjectTerm::class)]
50#[UsesClass(\SqlCatalog\Core\Php\ClassShape::class)]
51#[UsesClass(\SqlCatalog\Core\Php\DeclaredGlobals::class)]
52#[UsesClass(NodeText::class)]
53#[UsesClass(\SqlCatalog\Core\Php\ParsedFile::class)]
54#[UsesClass(ProgramIndex::class)]
55#[UsesClass(ProgramIndexBuilder::class)]
56#[UsesClass(SourceParser::class)]
57#[UsesClass(\SqlCatalog\Core\Text\TextHole::class)]
58#[UsesClass(\SqlCatalog\Core\Text\TextPattern::class)]
59#[UsesClass(\SqlCatalog\Core\Type\TypeShape::class)]
60#[UsesClass(\SqlCatalog\Core\Analysis\FunctionModel\Registry::class)]
61#[UsesClass(\SqlCatalog\Core\Analysis\BuiltinCallModel::class)]
62#[UsesClass(\SqlCatalog\Core\Analysis\Effect\WriteEffects::class)]
63#[UsesClass(\SqlCatalog\Core\Analysis\Effect\ReferenceEffects::class)]
64final class ConstantReaderTest extends TestCase
65{
66    public function testReadRuntimeConstantOnlyTrustsThePhpConstants(): void
67    {
68        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
69        self::assertSame(PHP_INT_MAX, $evaluator->readRuntimeConstant('PHP_INT_MAX')->soleLiteral()?->value);
70        self::assertNull($evaluator->readRuntimeConstant('DIRECTORY_SEPARATOR')->soleLiteral());
71    }
72
73    public function testConstantNamePrefersTheOneTheNamespaceDeclares(): void
74    {
75        $file = (new SourceParser())->parse('t.php', '<?php namespace App; const T = "x"; $result = T;');
76        $index = (new ProgramIndexBuilder())->build([$file]);
77        $evaluator = new ConstantReader($index, new NodeText());
78        self::assertSame('T', $evaluator->constantName(new ConstFetch(new Name('T'))));
79    }
80
81    public function testReadConstantResolvesTheKeywordsAndTheDeclarations(): void
82    {
83        $file = (new SourceParser())->parse('t.php', '<?php const T = "users";');
84        $index = (new ProgramIndexBuilder())->build([$file]);
85        $evaluator = new ConstantReader($index, new NodeText());
86        $expressions = (new Interpreter($index, []))->evaluatorFor();
87        $scope = new FunctionScope('t.php');
88
89        self::assertTrue($evaluator->readConstant(new ConstFetch(new Name('true')), $scope, $expressions)->soleLiteral()?->value);
90        self::assertFalse($evaluator->readConstant(new ConstFetch(new Name('false')), $scope, $expressions)->soleLiteral()?->value);
91        self::assertNull($evaluator->readConstant(new ConstFetch(new Name('null')), $scope, $expressions)->soleLiteral()?->value);
92        self::assertSame('users', $evaluator->readConstant(new ConstFetch(new Name('T')), $scope, $expressions)->soleLiteral()?->value);
93    }
94
95    public function testReadClassConstantGivesUpWhenTheClassIsNotWritten(): void
96    {
97        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
98        $expressions = (new Interpreter(new ProgramIndex(), []))->evaluatorFor();
99        $node = new ClassConstFetch(new Variable('c'), 'T');
100        $read = $evaluator->readClassConstant($node, new FunctionScope('t.php'), $expressions);
101        self::assertSame('mixed', $read->type()->display());
102    }
103
104    public function testReadClassConstantGivesUpOnAConstantNothingDeclares(): void
105    {
106        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
107        $expressions = (new Interpreter(new ProgramIndex(), []))->evaluatorFor();
108        $node = new ClassConstFetch(new Name('C'), 'MISSING');
109        self::assertFalse($evaluator->readClassConstant($node, new FunctionScope('t.php'), $expressions)->isExact());
110    }
111
112    public function testReadConstantReadsTheKeywordsWhateverTheirCase(): void
113    {
114        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
115        $expressions = (new Interpreter(new ProgramIndex(), []))->evaluatorFor();
116        $scope = new FunctionScope('t.php');
117
118        self::assertTrue($evaluator->readConstant(new ConstFetch(new Name('TRUE')), $scope, $expressions)->soleLiteral()?->value);
119        self::assertFalse($evaluator->readConstant(new ConstFetch(new Name('False')), $scope, $expressions)->soleLiteral()?->value);
120        self::assertTrue($evaluator->readConstant(new ConstFetch(new Name('NULL')), $scope, $expressions)->isExact());
121        self::assertTrue($evaluator->readConstant(new ConstFetch(new Name('null')), $scope, $expressions)->isExact());
122    }
123
124    public function testConstantNameReadsTheNamespacedConstantOnlyWhenTheNamespaceDeclaresIt(): void
125    {
126        $file = (new SourceParser())->parse('t.php', '<?php namespace App; const T = "x"; $a = T; $b = PHP_EOL;');
127        $index = (new ProgramIndexBuilder())->build([$file]);
128        $fetches = (new NodeFinder())->findInstanceOf($file->statements, ConstFetch::class);
129
130        $names = array_map(
131            static fn (ConstFetch $fetch): string => (new ConstantReader($index, new NodeText()))->constantName($fetch),
132            $fetches,
133        );
134
135        self::assertSame(['App\\T', 'PHP_EOL'], $names);
136    }
137
138    public function testReadRuntimeConstantReadsAFullyQualifiedName(): void
139    {
140        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
141
142        self::assertSame(PHP_INT_MAX, $evaluator->readRuntimeConstant('\\PHP_INT_MAX')->soleLiteral()?->value);
143        self::assertSame('SQL_CATALOG_UNDEFINED', $evaluator->readRuntimeConstant('\\SQL_CATALOG_UNDEFINED')->patterns()[0]->holes()[0]->expression);
144    }
145
146    public function testReadClassConstantReadsWhatTheClassDeclares(): void
147    {
148        $file = (new SourceParser())->parse('t.php', '<?php class C { const T = "users"; }');
149        $index = (new ProgramIndexBuilder())->build([$file]);
150        $expressions = (new Interpreter($index, []))->evaluatorFor();
151        $node = new ClassConstFetch(new Name('C'), 'T');
152
153        self::assertSame('users', (new ConstantReader($index, new NodeText()))->readClassConstant($node, new FunctionScope('t.php'), $expressions)->soleLiteral()?->value);
154    }
155
156    public function testReadClassConstantReadsTheClassKeywordWhateverItsCase(): void
157    {
158        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
159        $expressions = (new Interpreter(new ProgramIndex(), []))->evaluatorFor();
160        $scope = new FunctionScope('t.php', 'C::m', 'C');
161
162        self::assertSame('App\\C', $evaluator->readClassConstant(new ClassConstFetch(new Name('App\\C'), 'class'), $scope, $expressions)->soleLiteral()?->value);
163        self::assertSame('C', $evaluator->readClassConstant(new ClassConstFetch(new Name('self'), 'CLASS'), $scope, $expressions)->soleLiteral()?->value);
164    }
165
166    public function testReadClassConstantTellsAnEnumCaseFromAConstantOfTheEnum(): void
167    {
168        $file = (new SourceParser())->parse('t.php', '<?php enum S: string { case A = "a"; const X = "x"; }');
169        $index = (new ProgramIndexBuilder())->build([$file]);
170        $evaluator = new ConstantReader($index, new NodeText());
171        $expressions = (new Interpreter($index, []))->evaluatorFor();
172        $scope = new FunctionScope('t.php');
173
174        $case = $evaluator->readClassConstant(new ClassConstFetch(new Name('S'), 'A'), $scope, $expressions)->soleObject();
175        $constant = $evaluator->readClassConstant(new ClassConstFetch(new Name('S'), 'X'), $scope, $expressions);
176
177        self::assertSame('S', $case?->className);
178        self::assertSame('A', $case->enumCase);
179        self::assertSame('x', $constant->soleLiteral()?->value);
180    }
181
182    public function testReadClassConstantQuotesWhatItCouldNotRead(): void
183    {
184        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
185        $expressions = (new Interpreter(new ProgramIndex(), []))->evaluatorFor();
186        $scope = new FunctionScope('t.php');
187
188        $missing = $evaluator->readClassConstant(new ClassConstFetch(new Name('C'), 'MISSING'), $scope, $expressions);
189        $unwritten = $evaluator->readClassConstant(new ClassConstFetch(new Variable('c'), 'T'), $scope, $expressions);
190
191        self::assertSame('C::MISSING', $missing->patterns()[0]->holes()[0]->expression);
192        self::assertSame('$c::T', $unwritten->patterns()[0]->holes()[0]->expression);
193    }
194
195    public function testResolveClassNameResolvesEveryKeywordForTheEnclosingClass(): void
196    {
197        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
198        $scope = new FunctionScope('t.php', 'C::m', 'C');
199
200        self::assertSame('C', $evaluator->resolveClassName(new Name('static'), $scope));
201        self::assertSame('C', $evaluator->resolveClassName(new Name('parent'), $scope));
202        self::assertSame('C', $evaluator->resolveClassName(new Name('SELF'), $scope));
203    }
204
205    public function testResolveClassNameResolvesTheSelfKeywords(): void
206    {
207        $evaluator = new ConstantReader(new ProgramIndex(), new NodeText());
208        $scope = new FunctionScope('t.php', 'C::m', 'C');
209        self::assertSame('C', $evaluator->resolveClassName(new Name('self'), $scope));
210        self::assertSame('Other', $evaluator->resolveClassName(new Name('Other'), $scope));
211        self::assertNull($evaluator->resolveClassName(new Variable('c'), $scope));
212    }
213}
214