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