1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Testing;
4:
5: use PhpParser\Node;
6: use PhpParser\Node\Expr\StaticCall;
7: use PhpParser\Node\Name;
8: use PHPStan\Analyser\NodeScopeResolver;
9: use PHPStan\Analyser\Scope;
10: use PHPStan\Analyser\ScopeContext;
11: use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
12: use PHPStan\File\FileHelper;
13: use PHPStan\Php\PhpVersion;
14: use PHPStan\PhpDoc\PhpDocInheritanceResolver;
15: use PHPStan\PhpDoc\StubPhpDocProvider;
16: use PHPStan\Reflection\InitializerExprTypeResolver;
17: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
18: use PHPStan\TrinaryLogic;
19: use PHPStan\Type\ConstantScalarType;
20: use PHPStan\Type\FileTypeMapper;
21: use PHPStan\Type\VerbosityLevel;
22: use function array_map;
23: use function array_merge;
24: use function count;
25: use function in_array;
26: use function is_string;
27: use function sprintf;
28: use function stripos;
29: use function strtolower;
30:
31: /** @api */
32: abstract class TypeInferenceTestCase extends PHPStanTestCase
33: {
34:
35: /**
36: * @param callable(Node , Scope ): void $callback
37: * @param string[] $dynamicConstantNames
38: */
39: public static function processFile(
40: string $file,
41: callable $callback,
42: array $dynamicConstantNames = [],
43: ): void
44: {
45: $reflectionProvider = self::createReflectionProvider();
46: $typeSpecifier = self::getContainer()->getService('typeSpecifier');
47: $fileHelper = self::getContainer()->getByType(FileHelper::class);
48: $resolver = new NodeScopeResolver(
49: $reflectionProvider,
50: self::getContainer()->getByType(InitializerExprTypeResolver::class),
51: self::getReflector(),
52: self::getClassReflectionExtensionRegistryProvider(),
53: self::getParser(),
54: self::getContainer()->getByType(FileTypeMapper::class),
55: self::getContainer()->getByType(StubPhpDocProvider::class),
56: self::getContainer()->getByType(PhpVersion::class),
57: self::getContainer()->getByType(PhpDocInheritanceResolver::class),
58: self::getContainer()->getByType(FileHelper::class),
59: $typeSpecifier,
60: self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
61: self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
62: true,
63: true,
64: static::getEarlyTerminatingMethodCalls(),
65: static::getEarlyTerminatingFunctionCalls(),
66: true,
67: self::getContainer()->getParameter('treatPhpDocTypesAsCertain'),
68: );
69: $resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], static::getAdditionalAnalysedFiles())));
70:
71: $scopeFactory = self::createScopeFactory($reflectionProvider, $typeSpecifier, $dynamicConstantNames);
72: $scope = $scopeFactory->create(ScopeContext::create($file));
73:
74: $resolver->processNodes(
75: self::getParser()->parseFile($file),
76: $scope,
77: $callback,
78: );
79: }
80:
81: /**
82: * @api
83: * @param mixed ...$args
84: */
85: public function assertFileAsserts(
86: string $assertType,
87: string $file,
88: ...$args,
89: ): void
90: {
91: if ($assertType === 'type') {
92: $expectedType = $args[0];
93: $this->assertInstanceOf(ConstantScalarType::class, $expectedType);
94: $expected = $expectedType->getValue();
95: $actualType = $args[1];
96: $actual = $actualType->describe(VerbosityLevel::precise());
97: $this->assertSame(
98: $expected,
99: $actual,
100: sprintf('Expected type %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[2]),
101: );
102: } elseif ($assertType === 'variableCertainty') {
103: $expectedCertainty = $args[0];
104: $actualCertainty = $args[1];
105: $variableName = $args[2];
106: $this->assertTrue(
107: $expectedCertainty->equals($actualCertainty),
108: sprintf('Expected %s, actual certainty of variable $%s is %s in %s on line %d.', $expectedCertainty->describe(), $variableName, $actualCertainty->describe(), $file, $args[3]),
109: );
110: }
111: }
112:
113: /**
114: * @api
115: * @return array<string, mixed[]>
116: */
117: public static function gatherAssertTypes(string $file): array
118: {
119: $asserts = [];
120: self::processFile($file, static function (Node $node, Scope $scope) use (&$asserts, $file): void {
121: if (!$node instanceof Node\Expr\FuncCall) {
122: return;
123: }
124:
125: $nameNode = $node->name;
126: if (!$nameNode instanceof Name) {
127: return;
128: }
129:
130: $functionName = $nameNode->toString();
131: if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertvariablecertainty'], true)) {
132: self::fail(sprintf(
133: 'Missing use statement for %s() on line %d.',
134: $functionName,
135: $node->getLine(),
136: ));
137: } elseif ($functionName === 'PHPStan\\Testing\\assertType') {
138: $expectedType = $scope->getType($node->getArgs()[0]->value);
139: $actualType = $scope->getType($node->getArgs()[1]->value);
140: $assert = ['type', $file, $expectedType, $actualType, $node->getLine()];
141: } elseif ($functionName === 'PHPStan\\Testing\\assertNativeType') {
142: $expectedType = $scope->getType($node->getArgs()[0]->value);
143: $actualType = $scope->getNativeType($node->getArgs()[1]->value);
144: $assert = ['type', $file, $expectedType, $actualType, $node->getLine()];
145: } elseif ($functionName === 'PHPStan\\Testing\\assertVariableCertainty') {
146: $certainty = $node->getArgs()[0]->value;
147: if (!$certainty instanceof StaticCall) {
148: self::fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName));
149: }
150: if (!$certainty->class instanceof Node\Name) {
151: self::fail(sprintf('ERROR: Invalid TrinaryLogic call.'));
152: }
153:
154: if ($certainty->class->toString() !== 'PHPStan\\TrinaryLogic') {
155: self::fail(sprintf('ERROR: Invalid TrinaryLogic call.'));
156: }
157:
158: if (!$certainty->name instanceof Node\Identifier) {
159: self::fail(sprintf('ERROR: Invalid TrinaryLogic call.'));
160: }
161:
162: // @phpstan-ignore-next-line
163: $expectedertaintyValue = TrinaryLogic::{$certainty->name->toString()}();
164: $variable = $node->getArgs()[1]->value;
165: if (!$variable instanceof Node\Expr\Variable) {
166: self::fail(sprintf('ERROR: Invalid assertVariableCertainty call.'));
167: }
168: if (!is_string($variable->name)) {
169: self::fail(sprintf('ERROR: Invalid assertVariableCertainty call.'));
170: }
171:
172: $actualCertaintyValue = $scope->hasVariableType($variable->name);
173: $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variable->name, $node->getLine()];
174: } else {
175: $correctFunction = null;
176:
177: $assertFunctions = [
178: 'assertType' => 'PHPStan\\Testing\\assertType',
179: 'assertNativeType' => 'PHPStan\\Testing\\assertNativeType',
180: 'assertVariableCertainty' => 'PHPStan\\Testing\\assertVariableCertainty',
181: ];
182: foreach ($assertFunctions as $assertFn => $fqFunctionName) {
183: if (stripos($functionName, $assertFn) === false) {
184: continue;
185: }
186:
187: $correctFunction = $fqFunctionName;
188: }
189:
190: if ($correctFunction === null) {
191: return;
192: }
193:
194: self::fail(sprintf(
195: 'Function %s imported with wrong namespace %s called on line %d.',
196: $correctFunction,
197: $functionName,
198: $node->getLine(),
199: ));
200: }
201:
202: if (count($node->getArgs()) !== 2) {
203: self::fail(sprintf(
204: 'ERROR: Wrong %s() call on line %d.',
205: $functionName,
206: $node->getLine(),
207: ));
208: }
209:
210: $asserts[$file . ':' . $node->getLine()] = $assert;
211: });
212:
213: if (count($asserts) === 0) {
214: self::fail(sprintf('File %s does not contain any asserts', $file));
215: }
216:
217: return $asserts;
218: }
219:
220: /** @return string[] */
221: protected static function getAdditionalAnalysedFiles(): array
222: {
223: return [];
224: }
225:
226: /** @return string[][] */
227: protected static function getEarlyTerminatingMethodCalls(): array
228: {
229: return [];
230: }
231:
232: /** @return string[] */
233: protected static function getEarlyTerminatingFunctionCalls(): array
234: {
235: return [];
236: }
237:
238: }
239: