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