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