|
2 | 2 |
|
3 | 3 | namespace PHPStan\Rules\PHPUnit;
|
4 | 4 |
|
| 5 | +use PhpParser\Node\Attribute; |
| 6 | +use PhpParser\Node\Expr\ClassConstFetch; |
| 7 | +use PhpParser\Node\Name; |
| 8 | +use PhpParser\Node\Scalar\String_; |
| 9 | +use PhpParser\Node\Stmt\ClassMethod; |
5 | 10 | use PHPStan\Analyser\Scope;
|
6 | 11 | use PHPStan\PhpDoc\ResolvedPhpDocBlock;
|
7 | 12 | use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
|
|
10 | 15 | use PHPStan\Reflection\ReflectionProvider;
|
11 | 16 | use PHPStan\Rules\RuleError;
|
12 | 17 | use PHPStan\Rules\RuleErrorBuilder;
|
| 18 | +use PHPStan\Type\FileTypeMapper; |
13 | 19 | use function array_merge;
|
14 | 20 | use function count;
|
15 | 21 | use function explode;
|
@@ -26,19 +32,84 @@ class DataProviderHelper
|
26 | 32 | */
|
27 | 33 | private $reflectionProvider;
|
28 | 34 |
|
| 35 | +/** |
| 36 | +* The file type mapper. |
| 37 | +* |
| 38 | +* @var FileTypeMapper |
| 39 | +*/ |
| 40 | +private $fileTypeMapper; |
| 41 | + |
29 | 42 | /** @var bool */
|
30 | 43 | private $phpunit10OrNewer;
|
31 | 44 |
|
32 |
| -public function __construct(ReflectionProvider $reflectionProvider, bool $phpunit10OrNewer) |
| 45 | +public function __construct( |
| 46 | +ReflectionProvider $reflectionProvider, |
| 47 | +FileTypeMapper $fileTypeMapper, |
| 48 | +bool $phpunit10OrNewer |
| 49 | +) |
33 | 50 | {
|
34 | 51 | $this->reflectionProvider = $reflectionProvider;
|
| 52 | +$this->fileTypeMapper = $fileTypeMapper; |
35 | 53 | $this->phpunit10OrNewer = $phpunit10OrNewer;
|
36 | 54 | }
|
37 | 55 |
|
| 56 | +/** |
| 57 | +* @return iterable<array{ClassReflection|null, string, int}> |
| 58 | +*/ |
| 59 | +public function getDataProviderMethods( |
| 60 | +Scope $scope, |
| 61 | +ClassMethod $node, |
| 62 | +ClassReflection $classReflection |
| 63 | +): iterable |
| 64 | +{ |
| 65 | +$docComment = $node->getDocComment(); |
| 66 | +if ($docComment !== null) { |
| 67 | +$methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( |
| 68 | +$scope->getFile(), |
| 69 | +$classReflection->getName(), |
| 70 | +$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, |
| 71 | +$node->name->toString(), |
| 72 | +$docComment->getText() |
| 73 | +); |
| 74 | +foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) { |
| 75 | +$dataProviderValue = $this->getDataProviderAnnotationValue($annotation); |
| 76 | +if ($dataProviderValue === null) { |
| 77 | +// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule |
| 78 | +continue; |
| 79 | +} |
| 80 | + |
| 81 | +$dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue); |
| 82 | +$dataProviderMethod[] = $node->getLine(); |
| 83 | + |
| 84 | +yield $dataProviderValue => $dataProviderMethod; |
| 85 | +} |
| 86 | +} |
| 87 | + |
| 88 | +if (!$this->phpunit10OrNewer) { |
| 89 | +return; |
| 90 | +} |
| 91 | + |
| 92 | +foreach ($node->attrGroups as $attrGroup) { |
| 93 | +foreach ($attrGroup->attrs as $attr) { |
| 94 | +$dataProviderMethod = null; |
| 95 | +if ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataprovider') { |
| 96 | +$dataProviderMethod = $this->parseDataProviderAttribute($attr, $classReflection); |
| 97 | +} elseif ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataproviderexternal') { |
| 98 | +$dataProviderMethod = $this->parseDataProviderExternalAttribute($attr); |
| 99 | +} |
| 100 | +if ($dataProviderMethod === null) { |
| 101 | +continue; |
| 102 | +} |
| 103 | + |
| 104 | +yield from $dataProviderMethod; |
| 105 | +} |
| 106 | +} |
| 107 | +} |
| 108 | + |
38 | 109 | /**
|
39 | 110 | * @return array<PhpDocTagNode>
|
40 | 111 | */
|
41 |
| -public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array |
| 112 | +private function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array |
42 | 113 | {
|
43 | 114 | if ($phpDoc === null) {
|
44 | 115 | return [];
|
@@ -62,67 +133,62 @@ public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
|
62 | 133 | * @return RuleError[] errors
|
63 | 134 | */
|
64 | 135 | public function processDataProvider(
|
65 |
| -Scope $scope, |
66 |
| -PhpDocTagNode $phpDocTag, |
| 136 | +string $dataProviderValue, |
| 137 | +?ClassReflection $classReflection, |
| 138 | +string $methodName, |
| 139 | +int $lineNumber, |
67 | 140 | bool $checkFunctionNameCase,
|
68 | 141 | bool $deprecationRulesInstalled
|
69 | 142 | ): array
|
70 | 143 | {
|
71 |
| -$dataProviderValue = $this->getDataProviderValue($phpDocTag); |
72 |
| -if ($dataProviderValue === null) { |
73 |
| -// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule |
74 |
| -return []; |
75 |
| -} |
76 |
| - |
77 |
| -[$classReflection, $method] = $this->parseDataProviderValue($scope, $dataProviderValue); |
78 | 144 | if ($classReflection === null) {
|
79 | 145 | $error = RuleErrorBuilder::message(sprintf(
|
80 | 146 | '@dataProvider %s related class not found.',
|
81 | 147 | $dataProviderValue
|
82 |
| -))->build(); |
| 148 | +))->line($lineNumber)->build(); |
83 | 149 |
|
84 | 150 | return [$error];
|
85 | 151 | }
|
86 | 152 |
|
87 | 153 | try {
|
88 |
| -$dataProviderMethodReflection = $classReflection->getNativeMethod($method); |
| 154 | +$dataProviderMethodReflection = $classReflection->getNativeMethod($methodName); |
89 | 155 | } catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) {
|
90 | 156 | $error = RuleErrorBuilder::message(sprintf(
|
91 | 157 | '@dataProvider %s related method not found.',
|
92 | 158 | $dataProviderValue
|
93 |
| -))->build(); |
| 159 | +))->line($lineNumber)->build(); |
94 | 160 |
|
95 | 161 | return [$error];
|
96 | 162 | }
|
97 | 163 |
|
98 | 164 | $errors = [];
|
99 | 165 |
|
100 |
| -if ($checkFunctionNameCase && $method !== $dataProviderMethodReflection->getName()) { |
| 166 | +if ($checkFunctionNameCase && $methodName !== $dataProviderMethodReflection->getName()) { |
101 | 167 | $errors[] = RuleErrorBuilder::message(sprintf(
|
102 | 168 | '@dataProvider %s related method is used with incorrect case: %s.',
|
103 | 169 | $dataProviderValue,
|
104 | 170 | $dataProviderMethodReflection->getName()
|
105 |
| -))->build(); |
| 171 | +))->line($lineNumber)->build(); |
106 | 172 | }
|
107 | 173 |
|
108 | 174 | if (!$dataProviderMethodReflection->isPublic()) {
|
109 | 175 | $errors[] = RuleErrorBuilder::message(sprintf(
|
110 | 176 | '@dataProvider %s related method must be public.',
|
111 | 177 | $dataProviderValue
|
112 |
| -))->build(); |
| 178 | +))->line($lineNumber)->build(); |
113 | 179 | }
|
114 | 180 |
|
115 | 181 | if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) {
|
116 | 182 | $errors[] = RuleErrorBuilder::message(sprintf(
|
117 | 183 | '@dataProvider %s related method must be static in PHPUnit 10 and newer.',
|
118 | 184 | $dataProviderValue
|
119 |
| -))->build(); |
| 185 | +))->line($lineNumber)->build(); |
120 | 186 | }
|
121 | 187 |
|
122 | 188 | return $errors;
|
123 | 189 | }
|
124 | 190 |
|
125 |
| -private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string |
| 191 | +private function getDataProviderAnnotationValue(PhpDocTagNode $phpDocTag): ?string |
126 | 192 | {
|
127 | 193 | if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) {
|
128 | 194 | return null;
|
@@ -134,7 +200,7 @@ private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string
|
134 | 200 | /**
|
135 | 201 | * @return array{ClassReflection|null, string}
|
136 | 202 | */
|
137 |
| -private function parseDataProviderValue(Scope $scope, string $dataProviderValue): array |
| 203 | +private function parseDataProviderAnnotationValue(Scope $scope, string $dataProviderValue): array |
138 | 204 | {
|
139 | 205 | $parts = explode('::', $dataProviderValue, 2);
|
140 | 206 | if (count($parts) <= 1) {
|
@@ -148,4 +214,62 @@ private function parseDataProviderValue(Scope $scope, string $dataProviderValue)
|
148 | 214 | return [null, $dataProviderValue];
|
149 | 215 | }
|
150 | 216 |
|
| 217 | +/** |
| 218 | +* @return array<string, array{(ClassReflection|null), string, int}>|null |
| 219 | +*/ |
| 220 | +private function parseDataProviderExternalAttribute(Attribute $attribute): ?array |
| 221 | +{ |
| 222 | +if (count($attribute->args) !== 2) { |
| 223 | +return null; |
| 224 | +} |
| 225 | +$methodNameArg = $attribute->args[1]->value; |
| 226 | +if (!$methodNameArg instanceof String_) { |
| 227 | +return null; |
| 228 | +} |
| 229 | +$classNameArg = $attribute->args[0]->value; |
| 230 | +if ($classNameArg instanceof ClassConstFetch && $classNameArg->class instanceof Name) { |
| 231 | +$className = $classNameArg->class->toString(); |
| 232 | +} elseif ($classNameArg instanceof String_) { |
| 233 | +$className = $classNameArg->value; |
| 234 | +} else { |
| 235 | +return null; |
| 236 | +} |
| 237 | + |
| 238 | +$dataProviderClassReflection = null; |
| 239 | +if ($this->reflectionProvider->hasClass($className)) { |
| 240 | +$dataProviderClassReflection = $this->reflectionProvider->getClass($className); |
| 241 | +$className = $dataProviderClassReflection->getName(); |
| 242 | +} |
| 243 | + |
| 244 | +return [ |
| 245 | +sprintf('%s::%s', $className, $methodNameArg->value) => [ |
| 246 | +$dataProviderClassReflection, |
| 247 | +$methodNameArg->value, |
| 248 | +$attribute->getLine(), |
| 249 | +], |
| 250 | +]; |
| 251 | +} |
| 252 | + |
| 253 | +/** |
| 254 | +* @return array<string, array{(ClassReflection|null), string, int}>|null |
| 255 | +*/ |
| 256 | +private function parseDataProviderAttribute(Attribute $attribute, ClassReflection $classReflection): ?array |
| 257 | +{ |
| 258 | +if (count($attribute->args) !== 1) { |
| 259 | +return null; |
| 260 | +} |
| 261 | +$methodNameArg = $attribute->args[0]->value; |
| 262 | +if (!$methodNameArg instanceof String_) { |
| 263 | +return null; |
| 264 | +} |
| 265 | + |
| 266 | +return [ |
| 267 | +$methodNameArg->value => [ |
| 268 | +$classReflection, |
| 269 | +$methodNameArg->value, |
| 270 | +$attribute->getLine(), |
| 271 | +], |
| 272 | +]; |
| 273 | +} |
| 274 | + |
151 | 275 | }
|
0 commit comments