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