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