Skip to content

Commit b3210c6

Browse files
authored
Add ClassConstantUsageFixer (#1000)
1 parent 6922672 commit b3210c6

File tree

6 files changed

+286
-2
lines changed

6 files changed

+286
-2
lines changed

.dev-tools/infection.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,11 @@
134134
"Ternary": true,
135135
"This": true,
136136
"Throw_": true,
137-
"TrueValue": true,
137+
"TrueValue": {
138+
"ignore": [
139+
"PhpCsFixerCustomFixers\\Fixer\\ClassConstantUsageFixer::getClassConstants"
140+
]
141+
},
138142
"UnwrapArrayChangeKeyCase": true,
139143
"UnwrapArrayChunk": true,
140144
"UnwrapArrayColumn": true,

.dev-tools/src/InfectionConfigBuilder.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ public function build(): array
9494
],
9595
];
9696

97+
$config['mutators']['TrueValue'] = [
98+
'ignore' => [
99+
'PhpCsFixerCustomFixers\\Fixer\\ClassConstantUsageFixer::getClassConstants',
100+
],
101+
];
102+
97103
return $config;
98104
}
99105
}

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.23.0
4+
- Add ClassConstantUsageFixer
5+
36
## v3.22.0
47
- NoSuperfluousConcatenationFixer - add option "keep_concatenation_for_different_quotes"
58
- NoPhpStormGeneratedCommentFixer - handle more comments

README.md

Lines changed: 16 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-3546-brightgreen.svg)
8+
![Tests](https://img.shields.io/badge/tests-3570-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)
@@ -43,6 +43,21 @@ require __DIR__ . '/vendor/kubawerlos/php-cs-fixer-custom-fixers/bootstrap.php';
4343

4444

4545
## Fixers
46+
#### ClassConstantUsageFixer
47+
Class constant must be used instead of a copy of string.
48+
```diff
49+
<?php
50+
class Foo
51+
{
52+
public const BAR = 'bar';
53+
public function bar()
54+
{
55+
- return 'bar';
56+
+ return self::BAR;
57+
}
58+
}
59+
```
60+
4661
#### CommentSurroundedBySpacesFixer
4762
Comments must be surrounded by spaces.
4863
```diff
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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\Tokenizer\Token;
18+
use PhpCsFixer\Tokenizer\Tokens;
19+
20+
final class ClassConstantUsageFixer extends AbstractFixer
21+
{
22+
public function getDefinition(): FixerDefinitionInterface
23+
{
24+
return new FixerDefinition(
25+
'Class constant must be used instead of a copy of string.',
26+
[new CodeSample(
27+
<<<'PHP'
28+
<?php
29+
class Foo
30+
{
31+
public const BAR = 'bar';
32+
public function bar()
33+
{
34+
return 'bar';
35+
}
36+
}
37+
38+
PHP,
39+
)],
40+
'',
41+
);
42+
}
43+
44+
public function getPriority(): int
45+
{
46+
return 0;
47+
}
48+
49+
public function isCandidate(Tokens $tokens): bool
50+
{
51+
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_CONSTANT_ENCAPSED_STRING]);
52+
}
53+
54+
public function isRisky(): bool
55+
{
56+
return false;
57+
}
58+
59+
public function fix(\SplFileInfo $file, Tokens $tokens): void
60+
{
61+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
62+
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
63+
continue;
64+
}
65+
66+
$openParenthesisIndex = $tokens->getNextTokenOfKind($index, ['{']);
67+
\assert(\is_int($openParenthesisIndex));
68+
69+
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openParenthesisIndex);
70+
71+
$this->fixClass($tokens, $openParenthesisIndex, $closeParenthesisIndex);
72+
}
73+
}
74+
75+
private function fixClass(Tokens $tokens, int $openParenthesisIndex, int $closeParenthesisIndex): void
76+
{
77+
[$constantsMap, $constantsIndices] = $this->getClassConstants($tokens, $openParenthesisIndex, $closeParenthesisIndex);
78+
79+
for ($index = $closeParenthesisIndex; $index > $openParenthesisIndex; $index--) {
80+
if (!$tokens[$index]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
81+
continue;
82+
}
83+
84+
if (!isset($constantsMap[$tokens[$index]->getContent()])) {
85+
continue;
86+
}
87+
88+
if (isset($constantsIndices[$index])) {
89+
continue;
90+
}
91+
92+
$tokens->overrideRange(
93+
$index,
94+
$index,
95+
[
96+
new Token([\T_STRING, 'self']),
97+
new Token([\T_DOUBLE_COLON, '::']),
98+
new Token([\T_STRING, $constantsMap[$tokens[$index]->getContent()]]),
99+
],
100+
);
101+
}
102+
}
103+
104+
/**
105+
* @return array{array<string, string>, array<int, true>}
106+
*/
107+
private function getClassConstants(Tokens $tokens, int $openParenthesisIndex, int $closeParenthesisIndex): array
108+
{
109+
$constants = [];
110+
$constantsIndices = [];
111+
for ($index = $openParenthesisIndex; $index < $closeParenthesisIndex; $index++) {
112+
if (!$tokens[$index]->isGivenKind(\T_CONST)) {
113+
continue;
114+
}
115+
116+
$assignTokenIndex = $tokens->getNextTokenOfKind($index, ['=']);
117+
\assert(\is_int($assignTokenIndex));
118+
119+
$constantNameIndex = $tokens->getPrevMeaningfulToken($assignTokenIndex);
120+
\assert(\is_int($constantNameIndex));
121+
122+
$constantValueIndex = $tokens->getNextMeaningfulToken($assignTokenIndex);
123+
\assert(\is_int($constantValueIndex));
124+
125+
$constantsIndices[$constantValueIndex] = true;
126+
127+
if (!$tokens[$constantValueIndex]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
128+
continue;
129+
}
130+
131+
$constants[$tokens[$constantNameIndex]->getContent()] = $tokens[$constantValueIndex]->getContent();
132+
}
133+
134+
return [$this->getClassConstantsMap($constants), $constantsIndices];
135+
}
136+
137+
/**
138+
* @param array<string, string> $constants
139+
*
140+
* @return array<string, string>
141+
*/
142+
private function getClassConstantsMap(array $constants): array
143+
{
144+
$map = [];
145+
$valuesCount = [];
146+
147+
foreach ($constants as $name => $value) {
148+
$map[$value] = $name;
149+
$valuesCount[$value] = ($valuesCount[$value] ?? 0) + 1;
150+
151+
if ($valuesCount[$value] > 1) {
152+
unset($map[$value]);
153+
}
154+
}
155+
156+
return $map;
157+
}
158+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 Tests\Fixer;
13+
14+
/**
15+
* @internal
16+
*
17+
* @covers \PhpCsFixerCustomFixers\Fixer\ClassConstantUsageFixer
18+
*/
19+
final class ClassConstantUsageFixerTest extends AbstractFixerTestCase
20+
{
21+
public function testIsRisky(): void
22+
{
23+
self::assertRiskiness(false);
24+
}
25+
26+
/**
27+
* @dataProvider provideFixCases
28+
*/
29+
public function testFix(string $expected, ?string $input = null): void
30+
{
31+
$this->doTest($expected, $input);
32+
}
33+
34+
/**
35+
* @return iterable<array{0: string, 1?: string}>
36+
*/
37+
public static function provideFixCases(): iterable
38+
{
39+
yield 'non-string constants are ignored' => [
40+
<<<'PHP'
41+
<?php
42+
class Foo
43+
{
44+
public const B = true;
45+
public const I = 10;
46+
public function f()
47+
{
48+
return 10 * f1(true, false, true);
49+
}
50+
}
51+
PHP,
52+
];
53+
54+
yield 'multiple constants with the same value' => [
55+
<<<'PHP'
56+
<?php
57+
class Foo
58+
{
59+
public const BAR = 'a';
60+
public const BAZ = 'a';
61+
public function f()
62+
{
63+
return 'a';
64+
}
65+
}
66+
PHP,
67+
];
68+
69+
yield 'constants all over the class' => [
70+
<<<'PHP'
71+
<?php
72+
class C
73+
{
74+
public const FOO = 'foo';
75+
public const F00 = 0;
76+
public function f()
77+
{
78+
return 'Hello ' . self::FOO . self::BAR . '!';
79+
}
80+
public const BAR = 'bar';
81+
}
82+
PHP,
83+
<<<'PHP'
84+
<?php
85+
class C
86+
{
87+
public const FOO = 'foo';
88+
public const F00 = 0;
89+
public function f()
90+
{
91+
return 'Hello ' . 'foo' . 'bar' . '!';
92+
}
93+
public const BAR = 'bar';
94+
}
95+
PHP,
96+
];
97+
}
98+
}

0 commit comments

Comments
 (0)