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