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