1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Testing;
4:
5: use Override;
6: use PHPStan\Analyser\ConstantResolver;
7: use PHPStan\Analyser\DirectInternalScopeFactoryFactory;
8: use PHPStan\Analyser\Error;
9: use PHPStan\Analyser\ScopeFactory;
10: use PHPStan\Analyser\TypeSpecifier;
11: use PHPStan\BetterReflection\Reflector\Reflector;
12: use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
13: use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider;
14: use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
15: use PHPStan\DependencyInjection\Type\UnaryOperatorTypeSpecifyingExtensionRegistryProvider;
16: use PHPStan\Node\Printer\ExprPrinter;
17: use PHPStan\Parser\Parser;
18: use PHPStan\Php\ComposerPhpVersionFactory;
19: use PHPStan\Php\ConfiguredPhpVersionRangeHelper;
20: use PHPStan\Php\PhpVersion;
21: use PHPStan\PhpDoc\TypeNodeResolver;
22: use PHPStan\PhpDoc\TypeStringResolver;
23: use PHPStan\Reflection\AttributeReflectionFactory;
24: use PHPStan\Reflection\InitializerExprTypeResolver;
25: use PHPStan\Reflection\ReflectionProvider;
26: use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider;
27: use PHPStan\Rules\Properties\PropertyReflectionFinder;
28: use PHPStan\Type\Constant\OversizedArrayBuilder;
29: use PHPStan\Type\TypeAliasResolver;
30: use PHPStan\Type\UsefulTypeAliasResolver;
31: use PHPUnit\Framework\ExpectationFailedException;
32: use PHPUnit\Framework\TestCase;
33: use function count;
34: use function implode;
35: use function rtrim;
36: use function sprintf;
37: use const DIRECTORY_SEPARATOR;
38:
39: /** @api */
40: abstract class PHPStanTestCase extends TestCase
41: {
42:
43: use PHPStanTestCaseTrait;
44:
45: /**
46: * Re-register the runtime container as the global static reflection provider before
47: * every test. Enable this in tests that construct Type objects directly and assert
48: * PHP-version-dependent reflection results, so a foreign PhpVersion leaked by another
49: * test can't flake them. See https://github.com/phpstan/phpstan/issues/14860
50: */
51: protected bool $reinitializeContainerBeforeEachTest = false;
52:
53: #[Override]
54: protected function setUp(): void
55: {
56: if (!$this->reinitializeContainerBeforeEachTest) {
57: return;
58: }
59:
60: self::getContainer();
61: }
62:
63: public static function getParser(): Parser
64: {
65: /** @var Parser $parser */
66: $parser = self::getContainer()->getService('defaultAnalysisParser');
67: return $parser;
68: }
69:
70: /** @api */
71: public static function createReflectionProvider(): ReflectionProvider
72: {
73: return self::getContainer()->getByType(ReflectionProvider::class);
74: }
75:
76: public static function getReflector(): Reflector
77: {
78: return self::getContainer()->getService('betterReflectionReflector');
79: }
80:
81: public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider
82: {
83: return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class);
84: }
85:
86: /**
87: * @param string[] $dynamicConstantNames
88: */
89: public static function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): ScopeFactory
90: {
91: $container = self::getContainer();
92:
93: if (count($dynamicConstantNames) === 0) {
94: $dynamicConstantNames = $container->getParameter('dynamicConstantNames');
95: }
96:
97: $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider);
98: $composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class);
99: $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, new ConfiguredPhpVersionRangeHelper(null, $composerPhpVersionFactory), container: $container);
100:
101: $initializerExprTypeResolver = new InitializerExprTypeResolver(
102: $constantResolver,
103: $reflectionProviderProvider,
104: $container->getByType(PhpVersion::class),
105: $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class),
106: $container->getByType(UnaryOperatorTypeSpecifyingExtensionRegistryProvider::class),
107: new OversizedArrayBuilder(),
108: $container->getParameter('usePathConstantsAsConstantString'),
109: );
110:
111: return new ScopeFactory(
112: new DirectInternalScopeFactoryFactory(
113: $container,
114: $reflectionProvider,
115: $initializerExprTypeResolver,
116: $container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class),
117: $container->getByType(ExprPrinter::class),
118: $typeSpecifier,
119: new PropertyReflectionFinder(),
120: self::getParser(),
121: $container->getByType(PhpVersion::class),
122: $container->getByType(AttributeReflectionFactory::class),
123: $container->getParameter('phpVersion'),
124: $constantResolver,
125: ),
126: );
127: }
128:
129: /**
130: * @param array<string, string> $globalTypeAliases
131: */
132: public static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver
133: {
134: $container = self::getContainer();
135:
136: return new UsefulTypeAliasResolver(
137: $globalTypeAliases,
138: $container->getByType(TypeStringResolver::class),
139: $container->getByType(TypeNodeResolver::class),
140: $reflectionProvider,
141: 0,
142: );
143: }
144:
145: protected function shouldTreatPhpDocTypesAsCertain(): bool
146: {
147: return true;
148: }
149:
150: /**
151: * Provides a DIRECTORY_SEPARATOR agnostic assertion helper, to compare file paths.
152: *
153: */
154: protected function assertSamePaths(string $expected, string $actual, string $message = ''): void
155: {
156: $expected = $this->getFileHelper()->normalizePath($expected);
157: $actual = $this->getFileHelper()->normalizePath($actual);
158:
159: $this->assertSame($expected, $actual, $message);
160: }
161:
162: /**
163: * @param Error[]|string[] $errors
164: */
165: protected function assertNoErrors(array $errors): void
166: {
167: try {
168: $this->assertCount(0, $errors);
169: } catch (ExpectationFailedException $e) {
170: $messages = [];
171: foreach ($errors as $error) {
172: if ($error instanceof Error) {
173: $messages[] = sprintf("- %s\n in %s on line %d%s\n", rtrim($error->getMessage(), '.'), $error->getFile(), $error->getLine() ?? 0, $error->getTip() !== null ? sprintf("\n💡 %s", $error->getTip()) : '');
174: } else {
175: $messages[] = $error;
176: }
177: }
178:
179: $this->fail($e->getMessage() . "\n\nEmitted errors:\n" . implode("\n", $messages));
180: }
181: }
182:
183: protected function skipIfNotOnWindows(): void
184: {
185: if (DIRECTORY_SEPARATOR === '\\') {
186: return;
187: }
188:
189: self::markTestSkipped();
190: }
191:
192: protected function skipIfNotOnUnix(): void
193: {
194: if (DIRECTORY_SEPARATOR === '/') {
195: return;
196: }
197:
198: self::markTestSkipped();
199: }
200:
201: }
202: