22
33namespace PHPStan \Rules \PHPUnit ;
44
5+ use PharIo \Version \UnsupportedVersionConstraintException ;
6+ use PharIo \Version \Version ;
7+ use PharIo \Version \VersionConstraintParser ;
58use PhpParser \Node ;
69use PHPStan \Analyser \Scope ;
710use PHPStan \Node \InClassMethodNode ;
11+ use PHPStan \Php \PhpMinorVersionIterator ;
12+ use PHPStan \Php \PhpVersion ;
813use PHPStan \Rules \Rule ;
914use PHPStan \Rules \RuleErrorBuilder ;
15+ use PHPStan \Type \Constant \ConstantIntegerType ;
16+ use PHPStan \Type \IntegerRangeType ;
1017use PHPUnit \Framework \TestCase ;
1118use function count ;
1219use function is_numeric ;
20+ use function preg_match ;
1321use function sprintf ;
22+ use function version_compare ;
1423
1524/**
1625 * @implements Rule<InClassMethodNode>
1726 */
1827class AttributeRequiresPhpVersionRule implements Rule
1928{
2029
30+ private const VERSION_COMPARISON = "/(?P<operator>!=|<|<=|<>|=|==|>|>=)?\s*(?P<version>[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]* \r?$/m " ;
31+
2132 private PHPUnitVersion $ PHPUnitVersion ;
2233
2334 private TestMethodsHelper $ testMethodsHelper ;
2435
36+ private PhpVersion $ fallbackPhpVersion ;
37+
2538 /**
2639 * When phpstan-deprecation-rules is installed, it reports deprecated usages.
2740 */
@@ -30,12 +43,14 @@ class AttributeRequiresPhpVersionRule implements Rule
3043 public function __construct (
3144 PHPUnitVersion $ PHPUnitVersion ,
3245 TestMethodsHelper $ testMethodsHelper ,
33- bool $ deprecationRulesInstalled
46+ bool $ deprecationRulesInstalled ,
47+ PhpVersion $ phpVersion
3448 )
3549 {
3650 $ this ->PHPUnitVersion = $ PHPUnitVersion ;
3751 $ this ->testMethodsHelper = $ testMethodsHelper ;
3852 $ this ->deprecationRulesInstalled = $ deprecationRulesInstalled ;
53+ $ this ->fallbackPhpVersion = $ phpVersion ;
3954 }
4055
4156 public function getNodeType (): string
@@ -55,16 +70,64 @@ public function processNode(Node $node, Scope $scope): array
5570 return [];
5671 }
5772
73+ $ phpstanPharIoVersions = $ this ->getAnalyzedPhpVersions ($ scope );
74+ if ($ phpstanPharIoVersions === []) {
75+ return [];
76+ }
77+
5878 $ errors = [];
79+ $ parser = new VersionConstraintParser ();
5980 foreach ($ reflectionMethod ->getAttributesByName ('PHPUnit\Framework\Attributes\RequiresPhp ' ) as $ attr ) {
6081 $ args = $ attr ->getArguments ();
6182 if (count ($ args ) !== 1 ) {
6283 continue ;
6384 }
6485
86+ // the following block is mimicing PHPUnit version parsing
87+ // see https://github.com/sebastianbergmann/phpunit/blob/43c2cd7b96ee1e800b35e4df23b419a88b53111d/src/Metadata/Version/Requirement.php
88+
89+ $ versionRequirement = $ args [0 ];
6590 if (
66- !is_numeric ($ args [ 0 ] )
91+ !is_numeric ($ versionRequirement )
6792 ) {
93+ try {
94+ // check composer like version constraints, e.g. ^1 or ~2
95+ $ testPhpVersionConstraint = $ parser ->parse ($ versionRequirement );
96+
97+ foreach ($ phpstanPharIoVersions as $ pharIoVersion ) {
98+ if ($ testPhpVersionConstraint ->complies ($ pharIoVersion )) {
99+ // one of the versions within range matched, check next attribute
100+ continue 2 ;
101+ }
102+ }
103+ } catch (UnsupportedVersionConstraintException $ e ) {
104+ // test php-src builtin operators as in version_compare()
105+ if (preg_match (self ::VERSION_COMPARISON , $ versionRequirement , $ matches ) <= 0 ) {
106+ $ errors [] = RuleErrorBuilder::message (
107+ sprintf ($ e ->getMessage ()),
108+ )
109+ ->identifier ('phpunit.attributeRequiresPhpVersion ' )
110+ ->build ();
111+
112+ continue ;
113+ }
114+
115+ $ operator = $ matches ['operator ' ] !== '' ? $ matches ['operator ' ] : '>= ' ;
116+
117+ foreach ($ phpstanPharIoVersions as $ pharIoVersion ) {
118+ if (version_compare ($ pharIoVersion ->getVersionString (), $ matches ['version ' ], $ operator )) {
119+ // one of the versions within range matched, check next attribute
120+ continue 2 ;
121+ }
122+ }
123+ }
124+
125+ $ errors [] = RuleErrorBuilder::message (
126+ sprintf ('Version requirement will always evaluate to false. ' ),
127+ )
128+ ->identifier ('phpunit.attributeRequiresPhpVersion ' )
129+ ->build ();
130+
68131 continue ;
69132 }
70133
@@ -84,10 +147,37 @@ public function processNode(Node $node, Scope $scope): array
84147 ->identifier ('phpunit.attributeRequiresPhpVersion ' )
85148 ->build ();
86149 }
87-
88150 }
89151
90152 return $ errors ;
91153 }
92154
155+ /**
156+ * @return Version[]
157+ */
158+ private function getAnalyzedPhpVersions (Scope $ scope ): array
159+ {
160+ $ scopePhpVersion = $ scope ->getPhpVersion ()->getType ();
161+ if ($ scopePhpVersion instanceof ConstantIntegerType) {
162+ $ v = new PhpVersion ($ scopePhpVersion ->getValue ());
163+ return [new Version ($ v ->getVersionString ())];
164+ } elseif ($ scopePhpVersion instanceof IntegerRangeType) {
165+ if ($ scopePhpVersion ->getMin () === null || $ scopePhpVersion ->getMax () === null ) {
166+ return [];
167+ }
168+
169+ $ versions = [];
170+ $ minorVersionIterator = new PhpMinorVersionIterator (
171+ new PhpVersion ($ scopePhpVersion ->getMin ()),
172+ new PhpVersion ($ scopePhpVersion ->getMax ()),
173+ );
174+ foreach ($ minorVersionIterator as $ phpstanVersion ) {
175+ $ versions [] = new Version ($ phpstanVersion ->getVersionString ());
176+ }
177+ return $ versions ;
178+ }
179+
180+ return [new Version ($ this ->fallbackPhpVersion ->getVersionString ())];
181+ }
182+
93183}
0 commit comments