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