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