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: * @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: self::getContainer()->getParameter('polluteScopeWithBlock'),
103: [],
104: [],
105: self::getContainer()->getParameter('universalObjectCratesClasses'),
106: self::getContainer()->getParameter('exceptions')['implicitThrows'],
107: $this->shouldTreatPhpDocTypesAsCertain(),
108: );
109: $fileAnalyser = new FileAnalyser(
110: $this->createScopeFactory($reflectionProvider, $typeSpecifier),
111: $nodeScopeResolver,
112: $this->getParser(),
113: self::getContainer()->getByType(DependencyResolver::class),
114: new RuleErrorTransformer(),
115: new LocalIgnoresProcessor(),
116: );
117: $this->analyser = new Analyser(
118: $fileAnalyser,
119: $ruleRegistry,
120: $collectorRegistry,
121: $nodeScopeResolver,
122: 50,
123: );
124: }
125:
126: return $this->analyser;
127: }
128:
129: /**
130: * @param string[] $files
131: * @param list<array{0: string, 1: int, 2?: string|null}> $expectedErrors
132: */
133: public function analyse(array $files, array $expectedErrors): void
134: {
135: $actualErrors = $this->gatherAnalyserErrors($files);
136: $strictlyTypedSprintf = static function (int $line, string $message, ?string $tip): string {
137: $message = sprintf('%02d: %s', $line, $message);
138: if ($tip !== null) {
139: $message .= "\n 💡 " . $tip;
140: }
141:
142: return $message;
143: };
144:
145: $expectedErrors = array_map(
146: static fn (array $error): string => $strictlyTypedSprintf($error[1], $error[0], $error[2] ?? null),
147: $expectedErrors,
148: );
149:
150: $actualErrors = array_map(
151: static function (Error $error) use ($strictlyTypedSprintf): string {
152: $line = $error->getLine();
153: if ($line === null) {
154: return $strictlyTypedSprintf(-1, $error->getMessage(), $error->getTip());
155: }
156: return $strictlyTypedSprintf($line, $error->getMessage(), $error->getTip());
157: },
158: $actualErrors,
159: );
160:
161: $this->assertSame(implode("\n", $expectedErrors) . "\n", implode("\n", $actualErrors) . "\n");
162: }
163:
164: /**
165: * @param string[] $files
166: * @return list<Error>
167: */
168: public function gatherAnalyserErrors(array $files): array
169: {
170: $ruleRegistry = new DirectRuleRegistry([
171: $this->getRule(),
172: ]);
173: $files = array_map([$this->getFileHelper(), 'normalizePath'], $files);
174: $analyserResult = $this->getAnalyser($ruleRegistry)->analyse(
175: $files,
176: null,
177: null,
178: true,
179: );
180: if (count($analyserResult->getInternalErrors()) > 0) {
181: $this->fail(implode("\n", array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $analyserResult->getInternalErrors())));
182: }
183:
184: if ($this->shouldFailOnPhpErrors() && count($analyserResult->getAllPhpErrors()) > 0) {
185: $this->fail(implode("\n", array_map(
186: static fn (Error $error): string => sprintf('%s on %s:%d', $error->getMessage(), $error->getFile(), $error->getLine()),
187: $analyserResult->getAllPhpErrors(),
188: )));
189: }
190:
191: $finalizer = new AnalyserResultFinalizer(
192: $ruleRegistry,
193: new RuleErrorTransformer(),
194: $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()),
195: new LocalIgnoresProcessor(),
196: true,
197: );
198:
199: return $finalizer->finalize($analyserResult, false, true)->getAnalyserResult()->getUnorderedErrors();
200: }
201:
202: protected function shouldPolluteScopeWithLoopInitialAssignments(): bool
203: {
204: return false;
205: }
206:
207: protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool
208: {
209: return true;
210: }
211:
212: protected function shouldFailOnPhpErrors(): bool
213: {
214: return true;
215: }
216:
217: public static function getAdditionalConfigFiles(): array
218: {
219: return [
220: __DIR__ . '/../../conf/bleedingEdge.neon',
221: ];
222: }
223:
224: }
225: