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