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