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\Node\Printer\ExprPrinter;
14: use PHPStan\Parser\Parser;
15: use PHPStan\Php\ComposerPhpVersionFactory;
16: use PHPStan\Php\ConfiguredPhpVersionRangeHelper;
17: use PHPStan\Php\PhpVersion;
18: use PHPStan\PhpDoc\TypeNodeResolver;
19: use PHPStan\PhpDoc\TypeStringResolver;
20: use PHPStan\Reflection\AttributeReflectionFactory;
21: use PHPStan\Reflection\InitializerExprTypeResolver;
22: use PHPStan\Reflection\ReflectionProvider;
23: use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider;
24: use PHPStan\Rules\Properties\PropertyReflectionFinder;
25: use PHPStan\Testing\PHPUnit\ContainerInitializer;
26: use PHPStan\Type\Constant\OversizedArrayBuilder;
27: use PHPStan\Type\ExpressionTypeResolverExtension;
28: use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry;
29: use PHPStan\Type\TypeAliasResolver;
30: use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
31: use PHPStan\Type\UsefulTypeAliasResolver;
32: use PHPUnit\Framework\ExpectationFailedException;
33: use PHPUnit\Framework\TestCase;
34: use function count;
35: use function implode;
36: use function rtrim;
37: use function sprintf;
38: use const DIRECTORY_SEPARATOR;
39:
40: /** @api */
41: abstract class PHPStanTestCase extends TestCase
42: {
43:
44: use PHPStanTestCaseTrait;
45:
46: /**
47: * Re-register this test class's container as the owner of the global static state
48: * (the reflection provider, the PhpVersion, the bleeding edge toggle) before every
49: * test, so a container another test class installed can't flake tests that build
50: * Type objects directly. See https://github.com/phpstan/phpstan/issues/14860
51: *
52: * This is the only hook that does it, on every PHPUnit version. Data providers
53: * are covered separately - see PHPStanTestCaseTrait::restoreBaseContainer().
54: */
55: #[Override]
56: protected function setUp(): void
57: {
58: ContainerInitializer::initialize(static::class);
59: }
60:
61: #[Override]
62: public static function tearDownAfterClass(): void
63: {
64: parent::tearDownAfterClass();
65:
66: // The next test class's data providers run before any hook this test case has
67: // on PHPUnit 9, in the same ParaTest worker process - see restoreBaseContainer().
68: self::restoreBaseContainer();
69: }
70:
71: public static function getParser(): Parser
72: {
73: /** @var Parser $parser */
74: $parser = self::getContainer()->getService('defaultAnalysisParser');
75: return $parser;
76: }
77:
78: /** @api */
79: public static function createReflectionProvider(): ReflectionProvider
80: {
81: return self::getContainer()->getByType(ReflectionProvider::class);
82: }
83:
84: public static function getReflector(): Reflector
85: {
86: return self::getContainer()->getService('betterReflectionReflector');
87: }
88:
89: public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider
90: {
91: return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class);
92: }
93:
94: /**
95: * @param string[] $dynamicConstantNames
96: */
97: public static function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): ScopeFactory
98: {
99: $container = self::getContainer();
100:
101: if (count($dynamicConstantNames) === 0) {
102: $dynamicConstantNames = $container->getParameter('dynamicConstantNames');
103: }
104:
105: $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider);
106: $composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class);
107: $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, new ConfiguredPhpVersionRangeHelper(null, $composerPhpVersionFactory), container: $container);
108:
109: $initializerExprTypeResolver = new InitializerExprTypeResolver(
110: $constantResolver,
111: $reflectionProviderProvider,
112: $container->getByType(PhpVersion::class),
113: $container->getByType(OperatorTypeSpecifyingExtensionRegistry::class),
114: $container->getByType(UnaryOperatorTypeSpecifyingExtensionRegistry::class),
115: new OversizedArrayBuilder(),
116: $container->getParameter('usePathConstantsAsConstantString'),
117: );
118:
119: return new ScopeFactory(
120: new DirectInternalScopeFactoryFactory(
121: $container,
122: $reflectionProvider,
123: $initializerExprTypeResolver,
124: $container->getExtensionsCollection(ExpressionTypeResolverExtension::class),
125: $container->getByType(ExprPrinter::class),
126: $typeSpecifier,
127: new PropertyReflectionFinder(),
128: self::getParser(),
129: $container->getByType(PhpVersion::class),
130: $container->getByType(AttributeReflectionFactory::class),
131: $container->getByType(ConfiguredPhpVersionRangeHelper::class),
132: $constantResolver,
133: ),
134: );
135: }
136:
137: /**
138: * @param array<string, string> $globalTypeAliases
139: */
140: public static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver
141: {
142: $container = self::getContainer();
143:
144: return new UsefulTypeAliasResolver(
145: $globalTypeAliases,
146: $container->getByType(TypeStringResolver::class),
147: $container->getByType(TypeNodeResolver::class),
148: $reflectionProvider,
149: 0,
150: );
151: }
152:
153: protected function shouldTreatPhpDocTypesAsCertain(): bool
154: {
155: return true;
156: }
157:
158: /**
159: * Provides a DIRECTORY_SEPARATOR agnostic assertion helper, to compare file paths.
160: *
161: */
162: protected function assertSamePaths(string $expected, string $actual, string $message = ''): void
163: {
164: $expected = $this->getFileHelper()->normalizePath($expected);
165: $actual = $this->getFileHelper()->normalizePath($actual);
166:
167: $this->assertSame($expected, $actual, $message);
168: }
169:
170: /**
171: * @param Error[]|string[] $errors
172: */
173: protected function assertNoErrors(array $errors): void
174: {
175: try {
176: $this->assertCount(0, $errors);
177: } catch (ExpectationFailedException $e) {
178: $messages = [];
179: foreach ($errors as $error) {
180: if ($error instanceof Error) {
181: $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()) : '');
182: } else {
183: $messages[] = $error;
184: }
185: }
186:
187: $this->fail($e->getMessage() . "\n\nEmitted errors:\n" . implode("\n", $messages));
188: }
189: }
190:
191: protected function skipIfNotOnWindows(): void
192: {
193: if (DIRECTORY_SEPARATOR === '\\') {
194: return;
195: }
196:
197: self::markTestSkipped();
198: }
199:
200: protected function skipIfNotOnUnix(): void
201: {
202: if (DIRECTORY_SEPARATOR === '/') {
203: return;
204: }
205:
206: self::markTestSkipped();
207: }
208:
209: }
210: