1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Testing;
4:
5: use PhpParser\Node;
6: use PHPStan\Analyser\Analyser;
7: use PHPStan\Analyser\AnalyserResultFinalizer;
8: use PHPStan\Analyser\Error;
9: use PHPStan\Analyser\FileAnalyser;
10: use PHPStan\Analyser\LocalIgnoresProcessor;
11: use PHPStan\Analyser\NodeScopeResolver;
12: use PHPStan\Analyser\RuleErrorTransformer;
13: use PHPStan\Analyser\TypeSpecifier;
14: use PHPStan\Collectors\Collector;
15: use PHPStan\Collectors\Registry as CollectorRegistry;
16: use PHPStan\Dependency\DependencyResolver;
17: use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
18: use PHPStan\File\FileHelper;
19: use PHPStan\Php\PhpVersion;
20: use PHPStan\PhpDoc\PhpDocInheritanceResolver;
21: use PHPStan\PhpDoc\StubPhpDocProvider;
22: use PHPStan\Reflection\InitializerExprTypeResolver;
23: use PHPStan\Reflection\SignatureMap\SignatureMapProvider;
24: use PHPStan\Rules\DirectRegistry as DirectRuleRegistry;
25: use PHPStan\Rules\Properties\DirectReadWritePropertiesExtensionProvider;
26: use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
27: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
28: use PHPStan\Rules\Rule;
29: use PHPStan\Type\FileTypeMapper;
30: use function array_map;
31: use function count;
32: use function implode;
33: use function sprintf;
34:
35: /**
36: * @api
37: * @template TRule of Rule
38: */
39: abstract class RuleTestCase extends PHPStanTestCase
40: {
41:
42: private ?Analyser $analyser = null;
43:
44: /**
45: * @phpstan-return TRule
46: */
47: abstract protected function getRule(): Rule;
48:
49: /**
50: * @return array<Collector<Node, mixed>>
51: */
52: protected function getCollectors(): array
53: {
54: return [];
55: }
56:
57: /**
58: * @return ReadWritePropertiesExtension[]
59: */
60: protected function getReadWritePropertiesExtensions(): array
61: {
62: return [];
63: }
64:
65: protected function getTypeSpecifier(): TypeSpecifier
66: {
67: return self::getContainer()->getService('typeSpecifier');
68: }
69:
70: private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser
71: {
72: if ($this->analyser === null) {
73: $collectorRegistry = new CollectorRegistry($this->getCollectors());
74:
75: $reflectionProvider = $this->createReflectionProvider();
76: $typeSpecifier = $this->getTypeSpecifier();
77:
78: $readWritePropertiesExtensions = $this->getReadWritePropertiesExtensions();
79: $nodeScopeResolver = new NodeScopeResolver(
80: $reflectionProvider,
81: self::getContainer()->getByType(InitializerExprTypeResolver::class),
82: self::getReflector(),
83: self::getClassReflectionExtensionRegistryProvider(),
84: $this->getParser(),
85: self::getContainer()->getByType(FileTypeMapper::class),
86: self::getContainer()->getByType(StubPhpDocProvider::class),
87: self::getContainer()->getByType(PhpVersion::class),
88: self::getContainer()->getByType(SignatureMapProvider::class),
89: self::getContainer()->getByType(PhpDocInheritanceResolver::class),
90: self::getContainer()->getByType(FileHelper::class),
91: $typeSpecifier,
92: self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
93: $readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
94: self::createScopeFactory($reflectionProvider, $typeSpecifier),
95: $this->shouldPolluteScopeWithLoopInitialAssignments(),
96: $this->shouldPolluteScopeWithAlwaysIterableForeach(),
97: [],
98: [],
99: self::getContainer()->getParameter('universalObjectCratesClasses'),
100: self::getContainer()->getParameter('exceptions')['implicitThrows'],
101: $this->shouldTreatPhpDocTypesAsCertain(),
102: self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'],
103: self::getContainer()->getParameter('featureToggles')['paramOutType'],
104: );
105: $fileAnalyser = new FileAnalyser(
106: $this->createScopeFactory($reflectionProvider, $typeSpecifier),
107: $nodeScopeResolver,
108: $this->getParser(),
109: self::getContainer()->getByType(DependencyResolver::class),
110: new RuleErrorTransformer(),
111: new LocalIgnoresProcessor(),
112: );
113: $this->analyser = new Analyser(
114: $fileAnalyser,
115: $ruleRegistry,
116: $collectorRegistry,
117: $nodeScopeResolver,
118: 50,
119: );
120: }
121:
122: return $this->analyser;
123: }
124:
125: /**
126: * @param string[] $files
127: * @param list<array{0: string, 1: int, 2?: string|null}> $expectedErrors
128: */
129: public function analyse(array $files, array $expectedErrors): void
130: {
131: $actualErrors = $this->gatherAnalyserErrors($files);
132: $strictlyTypedSprintf = static function (int $line, string $message, ?string $tip): string {
133: $message = sprintf('%02d: %s', $line, $message);
134: if ($tip !== null) {
135: $message .= "\n 💡 " . $tip;
136: }
137:
138: return $message;
139: };
140:
141: $expectedErrors = array_map(
142: static fn (array $error): string => $strictlyTypedSprintf($error[1], $error[0], $error[2] ?? null),
143: $expectedErrors,
144: );
145:
146: $actualErrors = array_map(
147: static function (Error $error) use ($strictlyTypedSprintf): string {
148: $line = $error->getLine();
149: if ($line === null) {
150: return $strictlyTypedSprintf(-1, $error->getMessage(), $error->getTip());
151: }
152: return $strictlyTypedSprintf($line, $error->getMessage(), $error->getTip());
153: },
154: $actualErrors,
155: );
156:
157: $this->assertSame(implode("\n", $expectedErrors) . "\n", implode("\n", $actualErrors) . "\n");
158: }
159:
160: /**
161: * @param string[] $files
162: * @return list<Error>
163: */
164: public function gatherAnalyserErrors(array $files): array
165: {
166: $ruleRegistry = new DirectRuleRegistry([
167: $this->getRule(),
168: ]);
169: $files = array_map([$this->getFileHelper(), 'normalizePath'], $files);
170: $analyserResult = $this->getAnalyser($ruleRegistry)->analyse(
171: $files,
172: null,
173: null,
174: true,
175: );
176: if (count($analyserResult->getInternalErrors()) > 0) {
177: $this->fail(implode("\n", $analyserResult->getInternalErrors()));
178: }
179:
180: $finalizer = new AnalyserResultFinalizer(
181: $ruleRegistry,
182: new RuleErrorTransformer(),
183: $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()),
184: new LocalIgnoresProcessor(),
185: true,
186: );
187:
188: return $finalizer->finalize($analyserResult, false)->getAnalyserResult()->getUnorderedErrors();
189: }
190:
191: protected function shouldPolluteScopeWithLoopInitialAssignments(): bool
192: {
193: return false;
194: }
195:
196: protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool
197: {
198: return true;
199: }
200:
201: public static function getAdditionalConfigFiles(): array
202: {
203: return [
204: __DIR__ . '/../../conf/bleedingEdge.neon',
205: ];
206: }
207:
208: }
209: