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