1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Testing;
4:
5: use PHPStan\Analyser\ConstantResolver;
6: use PHPStan\Analyser\DirectInternalScopeFactory;
7: use PHPStan\Analyser\Error;
8: use PHPStan\Analyser\MutatingScope;
9: use PHPStan\Analyser\NodeScopeResolver;
10: use PHPStan\Analyser\ScopeFactory;
11: use PHPStan\Analyser\TypeSpecifier;
12: use PHPStan\BetterReflection\Reflector\ClassReflector;
13: use PHPStan\BetterReflection\Reflector\ConstantReflector;
14: use PHPStan\BetterReflection\Reflector\FunctionReflector;
15: use PHPStan\BetterReflection\Reflector\Reflector;
16: use PHPStan\Broker\Broker;
17: use PHPStan\DependencyInjection\Container;
18: use PHPStan\DependencyInjection\ContainerFactory;
19: use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
20: use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
21: use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
22: use PHPStan\File\FileHelper;
23: use PHPStan\Node\Printer\ExprPrinter;
24: use PHPStan\Parser\Parser;
25: use PHPStan\Php\PhpVersion;
26: use PHPStan\PhpDoc\TypeNodeResolver;
27: use PHPStan\PhpDoc\TypeStringResolver;
28: use PHPStan\Reflection\InitializerExprTypeResolver;
29: use PHPStan\Reflection\ReflectionProvider;
30: use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider;
31: use PHPStan\Rules\Properties\PropertyReflectionFinder;
32: use PHPStan\Type\Constant\OversizedArrayBuilder;
33: use PHPStan\Type\TypeAliasResolver;
34: use PHPStan\Type\UsefulTypeAliasResolver;
35: use PHPUnit\Framework\ExpectationFailedException;
36: use PHPUnit\Framework\TestCase;
37: use function array_merge;
38: use function count;
39: use function implode;
40: use function is_dir;
41: use function mkdir;
42: use function rtrim;
43: use function sha1;
44: use function sprintf;
45: use function sys_get_temp_dir;
46: use const DIRECTORY_SEPARATOR;
47: use const PHP_VERSION_ID;
48:
49: /** @api */
50: abstract class PHPStanTestCase extends TestCase
51: {
52:
53: /** @deprecated */
54: public static bool $useStaticReflectionProvider = true;
55:
56: /** @var array<string, Container> */
57: private static array $containers = [];
58:
59: /** @api */
60: public static function getContainer(): Container
61: {
62: $additionalConfigFiles = static::getAdditionalConfigFiles();
63: $additionalConfigFiles[] = __DIR__ . '/TestCase.neon';
64: $cacheKey = sha1(implode("\n", $additionalConfigFiles));
65:
66: if (!isset(self::$containers[$cacheKey])) {
67: $tmpDir = sys_get_temp_dir() . '/phpstan-tests';
68: if (!@mkdir($tmpDir, 0777) && !is_dir($tmpDir)) {
69: self::fail(sprintf('Cannot create temp directory %s', $tmpDir));
70: }
71:
72: $rootDir = __DIR__ . '/../..';
73: $fileHelper = new FileHelper($rootDir);
74: $rootDir = $fileHelper->normalizePath($rootDir, '/');
75: $containerFactory = new ContainerFactory($rootDir);
76: $container = $containerFactory->create($tmpDir, array_merge([
77: $containerFactory->getConfigDirectory() . '/config.level8.neon',
78: ], $additionalConfigFiles), []);
79: self::$containers[$cacheKey] = $container;
80:
81: foreach ($container->getParameter('bootstrapFiles') as $bootstrapFile) {
82: (static function (string $file) use ($container): void {
83: require_once $file;
84: })($bootstrapFile);
85: }
86:
87: if (PHP_VERSION_ID >= 80000) {
88: require_once __DIR__ . '/../../stubs/runtime/Enum/UnitEnum.php';
89: require_once __DIR__ . '/../../stubs/runtime/Enum/BackedEnum.php';
90: require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnum.php';
91: require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumUnitCase.php';
92: require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumBackedCase.php';
93: }
94: } else {
95: ContainerFactory::postInitializeContainer(self::$containers[$cacheKey]);
96: }
97:
98: return self::$containers[$cacheKey];
99: }
100:
101: /**
102: * @return string[]
103: */
104: public static function getAdditionalConfigFiles(): array
105: {
106: return [];
107: }
108:
109: public static function getParser(): Parser
110: {
111: /** @var Parser $parser */
112: $parser = self::getContainer()->getService('defaultAnalysisParser');
113: return $parser;
114: }
115:
116: /**
117: * @api
118: * @deprecated Use createReflectionProvider() instead
119: */
120: public function createBroker(): Broker
121: {
122: return self::getContainer()->getByType(Broker::class);
123: }
124:
125: /** @api */
126: public static function createReflectionProvider(): ReflectionProvider
127: {
128: return self::getContainer()->getByType(ReflectionProvider::class);
129: }
130:
131: public static function getReflector(): Reflector
132: {
133: return self::getContainer()->getService('betterReflectionReflector');
134: }
135:
136: /**
137: * @deprecated Use getReflector() instead.
138: * @return array{ClassReflector, FunctionReflector, ConstantReflector}
139: */
140: public static function getReflectors(): array
141: {
142: return [
143: self::getContainer()->getService('betterReflectionClassReflector'),
144: self::getContainer()->getService('betterReflectionFunctionReflector'),
145: self::getContainer()->getService('betterReflectionConstantReflector'),
146: ];
147: }
148:
149: public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider
150: {
151: return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class);
152: }
153:
154: /**
155: * @param string[] $dynamicConstantNames
156: */
157: public static function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): ScopeFactory
158: {
159: $container = self::getContainer();
160:
161: if (count($dynamicConstantNames) === 0) {
162: $dynamicConstantNames = $container->getParameter('dynamicConstantNames');
163: }
164:
165: $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider);
166: $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames);
167:
168: return new ScopeFactory(
169: new DirectInternalScopeFactory(
170: MutatingScope::class,
171: $reflectionProvider,
172: new InitializerExprTypeResolver(
173: $constantResolver,
174: $reflectionProviderProvider,
175: $container->getByType(PhpVersion::class),
176: $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class),
177: new OversizedArrayBuilder(),
178: $container->getParameter('usePathConstantsAsConstantString'),
179: ),
180: $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class),
181: $container->getByType(ExprPrinter::class),
182: $typeSpecifier,
183: new PropertyReflectionFinder(),
184: self::getParser(),
185: $container->getByType(NodeScopeResolver::class),
186: $container->getByType(PhpVersion::class),
187: $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'],
188: $container->getParameter('featureToggles')['explicitMixedForGlobalVariables'],
189: $constantResolver,
190: ),
191: );
192: }
193:
194: /**
195: * @param array<string, string> $globalTypeAliases
196: */
197: public static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver
198: {
199: $container = self::getContainer();
200:
201: return new UsefulTypeAliasResolver(
202: $globalTypeAliases,
203: $container->getByType(TypeStringResolver::class),
204: $container->getByType(TypeNodeResolver::class),
205: $reflectionProvider,
206: );
207: }
208:
209: protected function shouldTreatPhpDocTypesAsCertain(): bool
210: {
211: return true;
212: }
213:
214: public static function getFileHelper(): FileHelper
215: {
216: return self::getContainer()->getByType(FileHelper::class);
217: }
218:
219: /**
220: * Provides a DIRECTORY_SEPARATOR agnostic assertion helper, to compare file paths.
221: *
222: */
223: protected function assertSamePaths(string $expected, string $actual, string $message = ''): void
224: {
225: $expected = $this->getFileHelper()->normalizePath($expected);
226: $actual = $this->getFileHelper()->normalizePath($actual);
227:
228: $this->assertSame($expected, $actual, $message);
229: }
230:
231: /**
232: * @param Error[]|string[] $errors
233: */
234: protected function assertNoErrors(array $errors): void
235: {
236: try {
237: $this->assertCount(0, $errors);
238: } catch (ExpectationFailedException $e) {
239: $messages = [];
240: foreach ($errors as $error) {
241: if ($error instanceof Error) {
242: $messages[] = sprintf("- %s\n in %s on line %d\n", rtrim($error->getMessage(), '.'), $error->getFile(), $error->getLine());
243: } else {
244: $messages[] = $error;
245: }
246: }
247:
248: $this->fail($e->getMessage() . "\n\nEmitted errors:\n" . implode("\n", $messages));
249: }
250: }
251:
252: protected function skipIfNotOnWindows(): void
253: {
254: if (DIRECTORY_SEPARATOR === '\\') {
255: return;
256: }
257:
258: self::markTestSkipped();
259: }
260:
261: protected function skipIfNotOnUnix(): void
262: {
263: if (DIRECTORY_SEPARATOR === '/') {
264: return;
265: }
266:
267: self::markTestSkipped();
268: }
269:
270: }
271: