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