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\Properties\DirectReadWritePropertiesExtensionProvider;
24: use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
25: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
26: use PHPStan\Rules\Registry as RuleRegistry;
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 RuleRegistry([
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: $files = array_map([$this->getFileHelper(), 'normalizePath'], $files);
128: $analyserResult = $this->getAnalyser()->analyse(
129: $files,
130: null,
131: null,
132: true,
133: );
134: if (count($analyserResult->getInternalErrors()) > 0) {
135: $this->fail(implode("\n", $analyserResult->getInternalErrors()));
136: }
137:
138: $actualErrors = $analyserResult->getUnorderedErrors();
139: $ruleErrorTransformer = new RuleErrorTransformer();
140: if (count($analyserResult->getCollectedData()) > 0) {
141: $ruleRegistry = new RuleRegistry([
142: $this->getRule(),
143: ]);
144:
145: $nodeType = CollectedDataNode::class;
146: $node = new CollectedDataNode($analyserResult->getCollectedData());
147: $scopeFactory = $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier());
148: $scope = $scopeFactory->create(ScopeContext::create('irrelevant'));
149: foreach ($ruleRegistry->getRules($nodeType) as $rule) {
150: $ruleErrors = $rule->processNode($node, $scope);
151: foreach ($ruleErrors as $ruleError) {
152: $actualErrors[] = $ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getLine());
153: }
154: }
155: }
156:
157: $strictlyTypedSprintf = static function (int $line, string $message, ?string $tip): string {
158: $message = sprintf('%02d: %s', $line, $message);
159: if ($tip !== null) {
160: $message .= "\n 💡 " . $tip;
161: }
162:
163: return $message;
164: };
165:
166: $expectedErrors = array_map(
167: static fn (array $error): string => $strictlyTypedSprintf($error[1], $error[0], $error[2] ?? null),
168: $expectedErrors,
169: );
170:
171: $actualErrors = array_map(
172: static function (Error $error) use ($strictlyTypedSprintf): string {
173: $line = $error->getLine();
174: if ($line === null) {
175: return $strictlyTypedSprintf(-1, $error->getMessage(), $error->getTip());
176: }
177: return $strictlyTypedSprintf($line, $error->getMessage(), $error->getTip());
178: },
179: $actualErrors,
180: );
181:
182: $this->assertSame(implode("\n", $expectedErrors) . "\n", implode("\n", $actualErrors) . "\n");
183: }
184:
185: protected function shouldPolluteScopeWithLoopInitialAssignments(): bool
186: {
187: return false;
188: }
189:
190: protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool
191: {
192: return true;
193: }
194:
195: public static function getAdditionalConfigFiles(): array
196: {
197: return [
198: __DIR__ . '/../../conf/bleedingEdge.neon',
199: ];
200: }
201:
202: }
203: