Skip to content

Commit c75deee

Browse files
authored
Add PhpUnitRequiresExplicitConstraintFixer (#1007)
1 parent 2c04a10 commit c75deee

File tree

4 files changed

+455
-1
lines changed

4 files changed

+455
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG for PHP CS Fixer: custom fixers
22

3+
## v3.24.0
4+
- Add PhpUnitRequiresExplicitConstraintFixer
5+
36
## v3.23.0
47
- Add ClassConstantUsageFixer
58

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Latest stable version](https://img.shields.io/packagist/v/kubawerlos/php-cs-fixer-custom-fixers.svg?label=current%20version)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
66
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
77
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
8-
![Tests](https://img.shields.io/badge/tests-3570-brightgreen.svg)
8+
![Tests](https://img.shields.io/badge/tests-3607-brightgreen.svg)
99
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
1010

1111
[![CI status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml/badge.svg)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml)
@@ -463,6 +463,23 @@ PHPUnit `fail`, `markTestIncomplete` and `markTestSkipped` functions must not be
463463
}
464464
```
465465

466+
#### PhpUnitRequiresExplicitConstraintFixer
467+
Assertions and attributes for PHP and PHPUnit versions must have explicit version constraint.
468+
```diff
469+
<?php
470+
class FooTest extends TestCase {
471+
/**
472+
- * @requires PHP 8.4
473+
+ * @requires PHP >= 8.4
474+
*/
475+
public function testBar() {}
476+
477+
- #[\PHPUnit\Framework\Attributes\RequiresPhpunit('12.0')]
478+
+ #[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 12.0')]
479+
public function testBaz() {}
480+
}
481+
```
482+
466483
#### PhpdocArrayStyleFixer
467484
Generic array style should be used in PHPDoc.
468485
DEPRECATED: use `phpdoc_array_type` instead.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace PhpCsFixerCustomFixers\Fixer;
13+
14+
use PhpCsFixer\FixerDefinition\CodeSample;
15+
use PhpCsFixer\FixerDefinition\FixerDefinition;
16+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
17+
use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
18+
use PhpCsFixer\Preg;
19+
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
20+
use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
21+
use PhpCsFixer\Tokenizer\Analyzer\FullyQualifiedNameAnalyzer;
22+
use PhpCsFixer\Tokenizer\CT;
23+
use PhpCsFixer\Tokenizer\Token;
24+
use PhpCsFixer\Tokenizer\Tokens;
25+
26+
final class PhpUnitRequiresExplicitConstraintFixer extends AbstractFixer
27+
{
28+
public function getDefinition(): FixerDefinitionInterface
29+
{
30+
return new FixerDefinition(
31+
'Assertions and attributes for PHP and PHPUnit versions must have explicit version constraint.',
32+
[new CodeSample(
33+
<<<'PHP'
34+
<?php
35+
class FooTest extends TestCase {
36+
/**
37+
* @requires PHP 8.4
38+
*/
39+
public function testBar() {}
40+
41+
#[\PHPUnit\Framework\Attributes\RequiresPhpunit('12.0')]
42+
public function testBaz() {}
43+
}
44+
45+
PHP,
46+
)],
47+
);
48+
}
49+
50+
public function getPriority(): int
51+
{
52+
return 0;
53+
}
54+
55+
public function isCandidate(Tokens $tokens): bool
56+
{
57+
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_EXTENDS, \T_FUNCTION]);
58+
}
59+
60+
public function isRisky(): bool
61+
{
62+
return false;
63+
}
64+
65+
public function fix(\SplFileInfo $file, Tokens $tokens): void
66+
{
67+
$phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
68+
69+
/** @var list<int> $indices */
70+
foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens) as $indices) {
71+
$this->fixClass($tokens, $indices[0], $indices[1]);
72+
}
73+
}
74+
75+
private function fixClass(Tokens $tokens, int $index, int $endIndex): void
76+
{
77+
while ($index < $endIndex) {
78+
$index = $tokens->getNextTokenOfKind($index, ['{', [\T_FUNCTION]]);
79+
if ($index === null || $index >= $endIndex) {
80+
return;
81+
}
82+
83+
if ($tokens[$index]->equals('{')) {
84+
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
85+
continue;
86+
}
87+
88+
self::fixMethod($tokens, $index);
89+
}
90+
}
91+
92+
private static function fixMethod(Tokens $tokens, int $index): void
93+
{
94+
$index = $tokens->getPrevTokenOfKind($index, [';', [\T_DOC_COMMENT], [CT::T_ATTRIBUTE_CLOSE]]);
95+
if ($index === null || $tokens[$index]->equals(';')) {
96+
return;
97+
}
98+
99+
if ($tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
100+
self::fixPhpDoc($tokens, $index);
101+
}
102+
103+
if ($tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
104+
self::fixAttribute($tokens, $index);
105+
}
106+
}
107+
108+
private static function fixPhpDoc(Tokens $tokens, int $index): void
109+
{
110+
$tokens[$index] = new Token([
111+
\T_DOC_COMMENT,
112+
Preg::replaceCallback(
113+
'/(@requires\\s+\\S+\\s+)(.+?)(\\s*)$/m',
114+
static function (array $matches): string {
115+
\assert(\is_string($matches[1]));
116+
\assert(\is_string($matches[2]));
117+
\assert(\is_string($matches[3]));
118+
if (Preg::match('/^[\\d\\.-]+(dev|(RC|alpha|beta)[\\d\\.])?$/', $matches[2])) {
119+
$matches[2] = '>= ' . $matches[2];
120+
}
121+
122+
return $matches[1] . $matches[2] . $matches[3];
123+
},
124+
$tokens[$index]->getContent(),
125+
),
126+
]);
127+
}
128+
129+
private static function fixAttribute(Tokens $tokens, int $index): void
130+
{
131+
$fullyQualifiedNameAnalyzer = new FullyQualifiedNameAnalyzer($tokens);
132+
foreach (AttributeAnalyzer::collect($tokens, $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index)) as $attributeAnalysis) {
133+
foreach ($attributeAnalysis->getAttributes() as $attribute) {
134+
$attributeName = \strtolower($fullyQualifiedNameAnalyzer->getFullyQualifiedName($attribute['name'], $attribute['start'], NamespaceUseAnalysis::TYPE_CLASS));
135+
if (
136+
$attributeName === 'phpunit\\framework\\attributes\\requiresphp'
137+
|| $attributeName === 'phpunit\\framework\\attributes\\requiresphpunit'
138+
) {
139+
$stringIndex = $tokens->getPrevMeaningfulToken($attribute['end']);
140+
\assert(\is_int($stringIndex));
141+
if (!$tokens[$stringIndex]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
142+
continue;
143+
}
144+
145+
$openParenthesisIndex = $tokens->getPrevMeaningfulToken($stringIndex);
146+
\assert(\is_int($openParenthesisIndex));
147+
if (!$tokens[$openParenthesisIndex]->equals('(')) {
148+
continue;
149+
}
150+
151+
$quote = \substr($tokens[$stringIndex]->getContent(), -1, 1);
152+
$tokens[$stringIndex] = new Token([
153+
\T_CONSTANT_ENCAPSED_STRING,
154+
$quote . self::fixString(\substr($tokens[$stringIndex]->getContent(), 1, -1)) . $quote,
155+
]);
156+
}
157+
}
158+
}
159+
}
160+
161+
private static function fixString(string $string): string
162+
{
163+
if (Preg::match('/^[\\d\\.-]+(dev|(RC|alpha|beta)[\\d\\.])?$/', $string)) {
164+
return '>= ' . $string;
165+
}
166+
167+
return $string;
168+
}
169+
}

0 commit comments

Comments
 (0)