1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Testing;
4:
5: use Override;
6: use PHPStan\Analyser\ConstantResolver;
7: use PHPStan\Analyser\DirectInternalScopeFactory;
8: use PHPStan\Analyser\Error;
9: use PHPStan\Analyser\NodeScopeResolver;
10: use PHPStan\Analyser\RicherScopeGetTypeHelper;
11: use PHPStan\Analyser\ScopeFactory;
12: use PHPStan\Analyser\TypeSpecifier;
13: use PHPStan\BetterReflection\Reflector\Reflector;
14: use PHPStan\DependencyInjection\Container;
15: use PHPStan\DependencyInjection\ContainerFactory;
16: use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
17: use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
18: use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider;
19: use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
20: use PHPStan\File\FileHelper;
21: use PHPStan\Internal\DirectoryCreator;
22: use PHPStan\Internal\DirectoryCreatorException;
23: use PHPStan\Node\Printer\ExprPrinter;
24: use PHPStan\Parser\Parser;
25: use PHPStan\Php\ComposerPhpVersionFactory;
26: use PHPStan\Php\PhpVersion;
27: use PHPStan\PhpDoc\TypeNodeResolver;
28: use PHPStan\PhpDoc\TypeStringResolver;
29: use PHPStan\Reflection\AttributeReflectionFactory;
30: use PHPStan\Reflection\InitializerExprTypeResolver;
31: use PHPStan\Reflection\ReflectionProvider;
32: use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider;
33: use PHPStan\Rules\Properties\PropertyReflectionFinder;
34: use PHPStan\Type\Constant\OversizedArrayBuilder;
35: use PHPStan\Type\TypeAliasResolver;
36: use PHPStan\Type\UsefulTypeAliasResolver;
37: use PHPUnit\Framework\ExpectationFailedException;
38: use PHPUnit\Framework\TestCase;
39: use function array_merge;
40: use function count;
41: use function implode;
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: /** @var array<string, Container> */
54: private static array $containers = [];
55:
56: /** @api */
57: public static function getContainer(): Container
58: {
59: $additionalConfigFiles = static::getAdditionalConfigFiles();
60: $additionalConfigFiles[] = __DIR__ . '/TestCase.neon';
61: $cacheKey = sha1(implode("\n", $additionalConfigFiles));
62:
63: if (!isset(self::$containers[$cacheKey])) {
64: $tmpDir = sys_get_temp_dir() . '/phpstan-tests';
65: try {
66: DirectoryCreator::ensureDirectoryExists($tmpDir, 0777);
67: } catch (DirectoryCreatorException $e) {
68: self::fail($e->getMessage());
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: } else {
94: ContainerFactory::postInitializeContainer(self::$containers[$cacheKey]);
95: }
96:
97: return self::$containers[$cacheKey];
98: }
99:
100: #[Override]
101: public static function setUpBeforeClass(): void
102: {
103: self::getContainer();
104: }
105:
106: /**
107: * @return string[]
108: */
109: public static function getAdditionalConfigFiles(): array
110: {
111: return [];
112: }
113:
114: public static function getParser(): Parser
115: {
116: /** @var Parser $parser */
117: $parser = self::getContainer()->getService('defaultAnalysisParser');
118: return $parser;
119: }
120:
121: /** @api */
122: public static function createReflectionProvider(): ReflectionProvider
123: {
124: return self::getContainer()->getByType(ReflectionProvider::class);
125: }
126:
127: public static function getReflector(): Reflector
128: {
129: return self::getContainer()->getService('betterReflectionReflector');
130: }
131:
132: public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider
133: {
134: return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class);
135: }
136:
137: /**
138: * @param string[] $dynamicConstantNames
139: */
140: public static function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): ScopeFactory
141: {
142: $container = self::getContainer();
143:
144: if (count($dynamicConstantNames) === 0) {
145: $dynamicConstantNames = $container->getParameter('dynamicConstantNames');
146: }
147:
148: $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider);
149: $composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class);
150: $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, $composerPhpVersionFactory, $container);
151:
152: $initializerExprTypeResolver = new InitializerExprTypeResolver(
153: $constantResolver,
154: $reflectionProviderProvider,
155: $container->getByType(PhpVersion::class),
156: $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class),
157: new OversizedArrayBuilder(),
158: $container->getParameter('usePathConstantsAsConstantString'),
159: );
160:
161: return new ScopeFactory(
162: new DirectInternalScopeFactory(
163: $reflectionProvider,
164: $initializerExprTypeResolver,
165: $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class),
166: $container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class),
167: $container->getByType(ExprPrinter::class),
168: $typeSpecifier,
169: new PropertyReflectionFinder(),
170: self::getParser(),
171: $container->getByType(NodeScopeResolver::class),
172: new RicherScopeGetTypeHelper($initializerExprTypeResolver, new PropertyReflectionFinder()),
173: $container->getByType(PhpVersion::class),
174: $container->getByType(AttributeReflectionFactory::class),
175: $container->getParameter('phpVersion'),
176: $constantResolver,
177: ),
178: );
179: }
180:
181: /**
182: * @param array<string, string> $globalTypeAliases
183: */
184: public static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver
185: {
186: $container = self::getContainer();
187:
188: return new UsefulTypeAliasResolver(
189: $globalTypeAliases,
190: $container->getByType(TypeStringResolver::class),
191: $container->getByType(TypeNodeResolver::class),
192: $reflectionProvider,
193: );
194: }
195:
196: protected function shouldTreatPhpDocTypesAsCertain(): bool
197: {
198: return true;
199: }
200:
201: public static function getFileHelper(): FileHelper
202: {
203: return self::getContainer()->getByType(FileHelper::class);
204: }
205:
206: /**
207: * Provides a DIRECTORY_SEPARATOR agnostic assertion helper, to compare file paths.
208: *
209: */
210: protected function assertSamePaths(string $expected, string $actual, string $message = ''): void
211: {
212: $expected = $this->getFileHelper()->normalizePath($expected);
213: $actual = $this->getFileHelper()->normalizePath($actual);
214:
215: $this->assertSame($expected, $actual, $message);
216: }
217:
218: /**
219: * @param Error[]|string[] $errors
220: */
221: protected function assertNoErrors(array $errors): void
222: {
223: try {
224: $this->assertCount(0, $errors);
225: } catch (ExpectationFailedException $e) {
226: $messages = [];
227: foreach ($errors as $error) {
228: if ($error instanceof Error) {
229: $messages[] = sprintf("- %s\n in %s on line %d\n", rtrim($error->getMessage(), '.'), $error->getFile(), $error->getLine());
230: } else {
231: $messages[] = $error;
232: }
233: }
234:
235: $this->fail($e->getMessage() . "\n\nEmitted errors:\n" . implode("\n", $messages));
236: }
237: }
238:
239: protected function skipIfNotOnWindows(): void
240: {
241: if (DIRECTORY_SEPARATOR === '\\') {
242: return;
243: }
244:
245: self::markTestSkipped();
246: }
247:
248: protected function skipIfNotOnUnix(): void
249: {
250: if (DIRECTORY_SEPARATOR === '/') {
251: return;
252: }
253:
254: self::markTestSkipped();
255: }
256:
257: }
258: