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