22
33namespace PHPStan \Rules \PHPUnit ;
44
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 ;
510use PHPStan \Analyser \Scope ;
611use PHPStan \PhpDoc \ResolvedPhpDocBlock ;
712use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTagNode ;
1015use PHPStan \Reflection \ReflectionProvider ;
1116use PHPStan \Rules \RuleError ;
1217use PHPStan \Rules \RuleErrorBuilder ;
18+ use PHPStan \Type \FileTypeMapper ;
1319use function array_merge ;
1420use function count ;
1521use function explode ;
@@ -26,19 +32,84 @@ class DataProviderHelper
2632 */
2733 private $ reflectionProvider ;
2834
35+ /**
36+ * The file type mapper.
37+ *
38+ * @var FileTypeMapper
39+ */
40+ private $ fileTypeMapper ;
41+
2942 /** @var bool */
3043 private $ phpunit10OrNewer ;
3144
32- public function __construct (ReflectionProvider $ reflectionProvider , bool $ phpunit10OrNewer )
45+ public function __construct (
46+ ReflectionProvider $ reflectionProvider ,
47+ FileTypeMapper $ fileTypeMapper ,
48+ bool $ phpunit10OrNewer
49+ )
3350 {
3451 $ this ->reflectionProvider = $ reflectionProvider ;
52+ $ this ->fileTypeMapper = $ fileTypeMapper ;
3553 $ this ->phpunit10OrNewer = $ phpunit10OrNewer ;
3654 }
3755
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+
38109 /**
39110 * @return array<PhpDocTagNode>
40111 */
41- public function getDataProviderAnnotations (?ResolvedPhpDocBlock $ phpDoc ): array
112+ private function getDataProviderAnnotations (?ResolvedPhpDocBlock $ phpDoc ): array
42113 {
43114 if ($ phpDoc === null ) {
44115 return [];
@@ -62,67 +133,62 @@ public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
62133 * @return RuleError[] errors
63134 */
64135 public function processDataProvider (
65- Scope $ scope ,
66- PhpDocTagNode $ phpDocTag ,
136+ string $ dataProviderValue ,
137+ ?ClassReflection $ classReflection ,
138+ string $ methodName ,
139+ int $ lineNumber ,
67140 bool $ checkFunctionNameCase ,
68141 bool $ deprecationRulesInstalled
69142 ): array
70143 {
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 );
78144 if ($ classReflection === null ) {
79145 $ error = RuleErrorBuilder::message (sprintf (
80146 '@dataProvider %s related class not found. ' ,
81147 $ dataProviderValue
82- ))->build ();
148+ ))->line ( $ lineNumber )-> build ();
83149
84150 return [$ error ];
85151 }
86152
87153 try {
88- $ dataProviderMethodReflection = $ classReflection ->getNativeMethod ($ method );
154+ $ dataProviderMethodReflection = $ classReflection ->getNativeMethod ($ methodName );
89155 } catch (MissingMethodFromReflectionException $ missingMethodFromReflectionException ) {
90156 $ error = RuleErrorBuilder::message (sprintf (
91157 '@dataProvider %s related method not found. ' ,
92158 $ dataProviderValue
93- ))->build ();
159+ ))->line ( $ lineNumber )-> build ();
94160
95161 return [$ error ];
96162 }
97163
98164 $ errors = [];
99165
100- if ($ checkFunctionNameCase && $ method !== $ dataProviderMethodReflection ->getName ()) {
166+ if ($ checkFunctionNameCase && $ methodName !== $ dataProviderMethodReflection ->getName ()) {
101167 $ errors [] = RuleErrorBuilder::message (sprintf (
102168 '@dataProvider %s related method is used with incorrect case: %s. ' ,
103169 $ dataProviderValue ,
104170 $ dataProviderMethodReflection ->getName ()
105- ))->build ();
171+ ))->line ( $ lineNumber )-> build ();
106172 }
107173
108174 if (!$ dataProviderMethodReflection ->isPublic ()) {
109175 $ errors [] = RuleErrorBuilder::message (sprintf (
110176 '@dataProvider %s related method must be public. ' ,
111177 $ dataProviderValue
112- ))->build ();
178+ ))->line ( $ lineNumber )-> build ();
113179 }
114180
115181 if ($ deprecationRulesInstalled && $ this ->phpunit10OrNewer && !$ dataProviderMethodReflection ->isStatic ()) {
116182 $ errors [] = RuleErrorBuilder::message (sprintf (
117183 '@dataProvider %s related method must be static in PHPUnit 10 and newer. ' ,
118184 $ dataProviderValue
119- ))->build ();
185+ ))->line ( $ lineNumber )-> build ();
120186 }
121187
122188 return $ errors ;
123189 }
124190
125- private function getDataProviderValue (PhpDocTagNode $ phpDocTag ): ?string
191+ private function getDataProviderAnnotationValue (PhpDocTagNode $ phpDocTag ): ?string
126192 {
127193 if (preg_match ('/^[^ \t]+/ ' , (string ) $ phpDocTag ->value , $ matches ) !== 1 ) {
128194 return null ;
@@ -134,7 +200,7 @@ private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string
134200 /**
135201 * @return array{ClassReflection|null, string}
136202 */
137- private function parseDataProviderValue (Scope $ scope , string $ dataProviderValue ): array
203+ private function parseDataProviderAnnotationValue (Scope $ scope , string $ dataProviderValue ): array
138204 {
139205 $ parts = explode (':: ' , $ dataProviderValue , 2 );
140206 if (count ($ parts ) <= 1 ) {
@@ -148,4 +214,62 @@ private function parseDataProviderValue(Scope $scope, string $dataProviderValue)
148214 return [null , $ dataProviderValue ];
149215 }
150216
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+
151275}
0 commit comments