22
33namespace PHPStan \Rules \PHPUnit ;
44
5- use PharIo \Version \UnsupportedVersionConstraintException ;
6- use PharIo \Version \Version ;
7- use PharIo \Version \VersionConstraintParser ;
85use PhpParser \Node ;
96use PHPStan \Analyser \Scope ;
107use PHPStan \Node \InClassMethodNode ;
11- use PHPStan \Php \PhpMinorVersionIterator ;
12- use PHPStan \Php \PhpVersion ;
138use PHPStan \Rules \Rule ;
14- use PHPStan \Rules \RuleErrorBuilder ;
15- use PHPStan \Type \Constant \ConstantIntegerType ;
16- use PHPStan \Type \IntegerRangeType ;
179use PHPUnit \Framework \TestCase ;
18- use function count ;
19- use function is_numeric ;
20- use function preg_match ;
21- use function sprintf ;
22- use function substr_count ;
23- use function version_compare ;
2410
2511/**
2612 * @implements Rule<InClassMethodNode>
2713 */
2814class AttributeRequiresPhpVersionRule implements Rule
2915{
3016
31- private const VERSION_COMPARISON = "/(?P<operator>!=|<|<=|<>|=|==|>|>=)?\s*(?P<version>[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]* \r?$/m " ;
32-
33- private PHPUnitVersion $ PHPUnitVersion ;
34-
3517 private TestMethodsHelper $ testMethodsHelper ;
3618
37- private PhpVersion $ fallbackPhpVersion ;
38-
39- /**
40- * When phpstan-deprecation-rules is installed, rule reports deprecated usages.
41- */
42- private bool $ deprecationRulesInstalled ;
43-
44- /**
45- * Whether warnings about incomplete versions are allowed to be emitted
46- */
47- private bool $ warnAboutIncompleteVersion ;
48-
49- private bool $ bleedingEdge ;
19+ private AttributeVersionRequirementHelper $ attributeVersionRequirementHelper ;
5020
5121 public function __construct (
52- PHPUnitVersion $ PHPUnitVersion ,
5322 TestMethodsHelper $ testMethodsHelper ,
54- bool $ deprecationRulesInstalled ,
55- PhpVersion $ phpVersion ,
56- bool $ bleedingEdge ,
57- bool $ warnAboutIncompleteVersion = true
23+ AttributeVersionRequirementHelper $ attributeVersionRequirementHelper
5824 )
5925 {
60- $ this ->PHPUnitVersion = $ PHPUnitVersion ;
6126 $ this ->testMethodsHelper = $ testMethodsHelper ;
62- $ this ->deprecationRulesInstalled = $ deprecationRulesInstalled ;
63- $ this ->fallbackPhpVersion = $ phpVersion ;
64- $ this ->warnAboutIncompleteVersion = $ warnAboutIncompleteVersion ;
65- $ this ->bleedingEdge = $ bleedingEdge ;
27+ $ this ->attributeVersionRequirementHelper = $ attributeVersionRequirementHelper ;
6628 }
6729
6830 public function getNodeType (): string
@@ -82,145 +44,10 @@ public function processNode(Node $node, Scope $scope): array
8244 return [];
8345 }
8446
85- $ phpstanPharIoVersions = $ this ->getAnalyzedPhpVersions ($ scope );
86- if ($ phpstanPharIoVersions === []) {
87- return [];
88- }
89-
90- $ errors = [];
91- $ parser = new VersionConstraintParser ();
92- foreach ($ reflectionMethod ->getAttributesByName ('PHPUnit\Framework\Attributes\RequiresPhp ' ) as $ attr ) {
93- $ args = $ attr ->getArguments ();
94- if (count ($ args ) !== 1 ) {
95- continue ;
96- }
97-
98- // the following block is mimicing PHPUnit version parsing
99- // see https://github.com/sebastianbergmann/phpunit/blob/43c2cd7b96ee1e800b35e4df23b419a88b53111d/src/Metadata/Version/Requirement.php
100-
101- $ versionRequirement = $ args [0 ];
102-
103- if ($ this ->warnAboutIncompleteVersion ($ versionRequirement )) {
104- $ errors [] = RuleErrorBuilder::message (
105- sprintf ('Version requirement is incomplete. ' ),
106- )
107- ->identifier ('phpunit.attributeRequiresPhpVersion ' )
108- ->build ();
109- }
110-
111- if (
112- !is_numeric ($ versionRequirement )
113- ) {
114- if (!$ this ->bleedingEdge ) {
115- continue ;
116- }
117-
118- try {
119- // check composer like version constraints, e.g. ^1 or ~2
120- $ testPhpVersionConstraint = $ parser ->parse ($ versionRequirement );
121-
122- foreach ($ phpstanPharIoVersions as $ pharIoVersion ) {
123- if ($ testPhpVersionConstraint ->complies ($ pharIoVersion )) {
124- // one of the versions within range matched, check next attribute
125- continue 2 ;
126- }
127- }
128- } catch (UnsupportedVersionConstraintException $ e ) {
129- // test php-src builtin operators as in version_compare()
130- if (preg_match (self ::VERSION_COMPARISON , $ versionRequirement , $ matches ) <= 0 ) {
131- $ errors [] = RuleErrorBuilder::message (
132- sprintf ($ e ->getMessage ()),
133- )
134- ->identifier ('phpunit.attributeRequiresPhpVersion ' )
135- ->build ();
136-
137- continue ;
138- }
139-
140- $ operator = $ matches ['operator ' ] !== '' ? $ matches ['operator ' ] : '>= ' ;
141-
142- foreach ($ phpstanPharIoVersions as $ pharIoVersion ) {
143- if (version_compare ($ pharIoVersion ->getVersionString (), $ matches ['version ' ], $ operator )) {
144- // one of the versions within range matched, check next attribute
145- continue 2 ;
146- }
147- }
148- }
149-
150- $ errors [] = RuleErrorBuilder::message (
151- sprintf ('Version requirement will always evaluate to false. ' ),
152- )
153- ->identifier ('phpunit.attributeRequiresPhpVersion ' )
154- ->build ();
155-
156- continue ;
157- }
158-
159- if ($ this ->PHPUnitVersion ->requiresPhpversionAttributeWithOperator ()->yes ()) {
160- $ errors [] = RuleErrorBuilder::message (
161- sprintf ('Version requirement is missing operator. ' ),
162- )
163- ->identifier ('phpunit.attributeRequiresPhpVersion ' )
164- ->build ();
165- } elseif (
166- $ this ->deprecationRulesInstalled
167- && $ this ->PHPUnitVersion ->deprecatesPhpversionAttributeWithoutOperator ()->yes ()
168- ) {
169- $ errors [] = RuleErrorBuilder::message (
170- sprintf ('Version requirement without operator is deprecated. ' ),
171- )
172- ->identifier ('phpunit.attributeRequiresPhpVersion ' )
173- ->build ();
174- }
175- }
176-
177- return $ errors ;
178- }
179-
180- /**
181- * @return Version[]
182- */
183- private function getAnalyzedPhpVersions (Scope $ scope ): array
184- {
185- $ scopePhpVersion = $ scope ->getPhpVersion ()->getType ();
186- if ($ scopePhpVersion instanceof ConstantIntegerType) {
187- $ v = new PhpVersion ($ scopePhpVersion ->getValue ());
188- return [new Version ($ v ->getVersionString ())];
189- } elseif ($ scopePhpVersion instanceof IntegerRangeType) {
190- if ($ scopePhpVersion ->getMin () === null || $ scopePhpVersion ->getMax () === null ) {
191- return [];
192- }
193-
194- $ versions = [];
195- $ minorVersionIterator = new PhpMinorVersionIterator (
196- new PhpVersion ($ scopePhpVersion ->getMin ()),
197- new PhpVersion ($ scopePhpVersion ->getMax ()),
198- );
199- foreach ($ minorVersionIterator as $ phpstanVersion ) {
200- $ versions [] = new Version ($ phpstanVersion ->getVersionString ());
201- }
202- return $ versions ;
203- }
204-
205- return [new Version ($ this ->fallbackPhpVersion ->getVersionString ())];
206- }
207-
208- // see https://github.com/sebastianbergmann/phpunit/issues/6451
209- private function warnAboutIncompleteVersion (string $ versionRequirement ): bool
210- {
211- if (!$ this ->bleedingEdge ) {
212- return false ;
213- }
214-
215- if (!$ this ->warnAboutIncompleteVersion ) {
216- return false ;
217- }
218-
219- if (!$ this ->PHPUnitVersion ->warnsAboutIncompleteVersion ()->yes ()) {
220- return false ;
221- }
222-
223- return substr_count ($ versionRequirement , '. ' ) !== 2 ;
47+ return $ this ->attributeVersionRequirementHelper ->checkRequiresPhpVersion (
48+ $ reflectionMethod ->getAttributesByName ('PHPUnit\Framework\Attributes\RequiresPhp ' ),
49+ $ scope ,
50+ );
22451 }
22552
22653}
0 commit comments