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