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