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