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