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