Skip to content

Commit 67f2533

Browse files
authored
Add TypedClassConstantFixer (#1029)
1 parent 059fa53 commit 67f2533

File tree

9 files changed

+585
-2
lines changed

9 files changed

+585
-2
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ jobs:
2323
- if: github.event_name != 'pull_request'
2424
run: rm ./.dev-tools/composer.lock
2525
- run: composer update --no-progress
26+
- run: "sed -i 's#constant: 0#constant: 100#g' .dev-tools/phpstan.neon"
27+
- run: composer apply-typed_class_constant
2628
- run: composer analyse
2729

2830
test:

.php-cs-fixer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use PhpCsFixerCustomFixers\Fixer\NoSuperfluousConcatenationFixer;
2121
use PhpCsFixerCustomFixers\Fixer\PhpdocOnlyAllowedAnnotationsFixer;
2222
use PhpCsFixerCustomFixers\Fixer\PromotedConstructorPropertyFixer;
23+
use PhpCsFixerCustomFixers\Fixer\TypedClassConstantFixer;
2324
use PhpCsFixerCustomFixers\Fixers;
2425

2526
// sanity check
@@ -54,6 +55,7 @@
5455
unset($rules['modernize_strpos']); // TODO: remove when dropping support to PHP <8.0
5556
unset($rules['php_unit_attributes']); // TODO: remove when dropping support to PHP <8.0
5657
unset($rules[PromotedConstructorPropertyFixer::name()]); // TODO: remove when dropping support to PHP <8.0
58+
unset($rules[TypedClassConstantFixer::name()]); // TODO: remove when dropping support to PHP <8.3
5759
$rules['trailing_comma_in_multiline'] = ['after_heredoc' => true, 'elements' => ['arguments', 'arrays']]; // TODO: remove when dropping support to PHP <8.0
5860

5961
$rules[PhpdocOnlyAllowedAnnotationsFixer::name()]['elements'][] = 'phpstan-type';

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.26.0
4+
- Add TypedClassConstantFixer
5+
36
## v3.25.0
47
- Add ForeachUseValueFixer
58
- Add NoUselessWriteVisibilityFixer

README.md

Lines changed: 13 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-3691-brightgreen.svg)
8+
![Tests](https://img.shields.io/badge/tests-3750-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)
@@ -724,6 +724,18 @@ The string key of an array or generator must be trimmed and have no double space
724724
];
725725
```
726726

727+
#### TypedClassConstantFixer
728+
Class constants must have a type.
729+
```diff
730+
<?php
731+
class Foo {
732+
- public const MAX_VALUE_OF_SOMETHING = 42;
733+
- public const THE_NAME_OF_SOMEONE = 'John Doe';
734+
+ public const int MAX_VALUE_OF_SOMETHING = 42;
735+
+ public const string THE_NAME_OF_SOMEONE = 'John Doe';
736+
}
737+
```
738+
727739

728740
## Contributing
729741
Request a feature or report a bug by creating an [issue](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues).

composer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
"@prepare-dev-tools",
4040
"php-cs-fixer fix --quiet --rules=php_unit_attributes || exit 0"
4141
],
42+
"apply-typed_class_constant": [
43+
"@prepare-dev-tools",
44+
"php-cs-fixer fix --quiet --rules=PhpCsFixerCustomFixers/typed_class_constant || exit 0"
45+
],
4246
"fix": [
4347
"@prepare-dev-tools",
4448
"php-cs-fixer fix --ansi --verbose || exit 0",

src/Fixer/TypedClassConstantFixer.php

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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\FixerDefinition;
15+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
16+
use PhpCsFixer\FixerDefinition\VersionSpecification;
17+
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
18+
use PhpCsFixer\Tokenizer\CT;
19+
use PhpCsFixer\Tokenizer\Token;
20+
use PhpCsFixer\Tokenizer\Tokens;
21+
22+
final class TypedClassConstantFixer extends AbstractFixer
23+
{
24+
private const INTEGER_KINDS = [\T_LNUMBER, '+', '-', '*', '(', ')', \T_POW, \T_SL, \T_SR, \T_LINE];
25+
private const FLOAT_KINDS = [\T_DNUMBER, ...self::INTEGER_KINDS, '/'];
26+
private const STRING_KINDS = [\T_CONSTANT_ENCAPSED_STRING, \T_START_HEREDOC, \T_ENCAPSED_AND_WHITESPACE, \T_END_HEREDOC, \T_LNUMBER, \T_DNUMBER, \T_CLASS_C, \T_DIR, \T_FILE, \T_FUNC_C, \T_METHOD_C, \T_NS_C, \T_TRAIT_C];
27+
28+
public function getDefinition(): FixerDefinitionInterface
29+
{
30+
return new FixerDefinition(
31+
'Class constants must have a type.',
32+
[
33+
new VersionSpecificCodeSample(
34+
<<<'PHP'
35+
<?php
36+
class Foo {
37+
public const MAX_VALUE_OF_SOMETHING = 42;
38+
public const THE_NAME_OF_SOMEONE = 'John Doe';
39+
}
40+
41+
PHP,
42+
new VersionSpecification(80300),
43+
),
44+
],
45+
);
46+
}
47+
48+
/**
49+
* Must run after SingleClassElementPerStatementFixer.
50+
*/
51+
public function getPriority(): int
52+
{
53+
return 0;
54+
}
55+
56+
public function isCandidate(Tokens $tokens): bool
57+
{
58+
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_CONST]);
59+
}
60+
61+
public function isRisky(): bool
62+
{
63+
return false;
64+
}
65+
66+
public function fix(\SplFileInfo $file, Tokens $tokens): void
67+
{
68+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
69+
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
70+
continue;
71+
}
72+
73+
$openParenthesisIndex = $tokens->getNextTokenOfKind($index, ['{']);
74+
\assert(\is_int($openParenthesisIndex));
75+
76+
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openParenthesisIndex);
77+
78+
self::fixClass($tokens, $openParenthesisIndex, $closeParenthesisIndex);
79+
}
80+
}
81+
82+
private static function fixClass(Tokens $tokens, int $openParenthesisIndex, int $closeParenthesisIndex): void
83+
{
84+
for ($index = $closeParenthesisIndex; $index > $openParenthesisIndex; $index--) {
85+
if (!$tokens[$index]->isGivenKind(\T_CONST)) {
86+
continue;
87+
}
88+
89+
$constantNameIndex = $tokens->getNextMeaningfulToken($index);
90+
\assert(\is_int($constantNameIndex));
91+
92+
$assignmentIndex = $tokens->getNextMeaningfulToken($constantNameIndex);
93+
\assert(\is_int($assignmentIndex));
94+
95+
if (!$tokens[$assignmentIndex]->equals('=')) {
96+
continue;
97+
}
98+
99+
$expressionStartIndex = $tokens->getNextMeaningfulToken($assignmentIndex);
100+
\assert(\is_int($expressionStartIndex));
101+
102+
if ($tokens[$expressionStartIndex]->isGivenKind(\T_NS_SEPARATOR)) {
103+
$expressionStartIndex = $tokens->getNextMeaningfulToken($expressionStartIndex);
104+
\assert(\is_int($expressionStartIndex));
105+
}
106+
107+
$type = self::getTypeOfExpression($tokens, $expressionStartIndex);
108+
109+
$tokens->insertAt(
110+
$constantNameIndex,
111+
[
112+
new Token([$type === 'array' ? CT::T_ARRAY_TYPEHINT : \T_STRING, $type]),
113+
new Token([\T_WHITESPACE, ' ']),
114+
],
115+
);
116+
}
117+
}
118+
119+
private static function getTypeOfExpression(Tokens $tokens, int $index): string
120+
{
121+
$semicolonIndex = $tokens->getNextTokenOfKind($index, [';']);
122+
\assert(\is_int($semicolonIndex));
123+
124+
$beforeSemicolonIndex = $tokens->getPrevMeaningfulToken($semicolonIndex);
125+
\assert(\is_int($beforeSemicolonIndex));
126+
127+
$foundKinds = [];
128+
129+
$questionMarkCount = 0;
130+
do {
131+
if ($questionMarkCount > 1) {
132+
return 'mixed';
133+
}
134+
$kind = $tokens[$index]->getId() ?? $tokens[$index]->getContent();
135+
if ($kind === '?') {
136+
$questionMarkCount++;
137+
$foundKinds = [];
138+
continue;
139+
}
140+
$foundKinds[] = $kind;
141+
142+
$index = $tokens->getNextMeaningfulToken($index);
143+
\assert(\is_int($index));
144+
} while ($index < $semicolonIndex);
145+
146+
if ($foundKinds === [\T_STRING]) {
147+
$lowercasedContent = \strtolower($tokens[$beforeSemicolonIndex]->getContent());
148+
if (\in_array($lowercasedContent, ['false', 'true', 'null'], true)) {
149+
return $lowercasedContent;
150+
}
151+
}
152+
153+
return self::getTypeOfExpressionForTokenKinds($foundKinds);
154+
}
155+
156+
/**
157+
* @param list<int|string> $tokenKinds
158+
*/
159+
private static function getTypeOfExpressionForTokenKinds(array $tokenKinds): string
160+
{
161+
if (self::isOfTypeBasedOnKinds($tokenKinds, [], [\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
162+
return 'array';
163+
}
164+
165+
if (self::isOfTypeBasedOnKinds($tokenKinds, self::INTEGER_KINDS, [])) {
166+
return 'int';
167+
}
168+
169+
if (self::isOfTypeBasedOnKinds($tokenKinds, self::FLOAT_KINDS, [])) {
170+
return 'float';
171+
}
172+
173+
if (self::isOfTypeBasedOnKinds($tokenKinds, self::STRING_KINDS, ['.', CT::T_CLASS_CONSTANT])) {
174+
return 'string';
175+
}
176+
177+
return 'mixed';
178+
}
179+
180+
/**
181+
* @param list<int|string> $expressionTokenKinds
182+
* @param list<int|string> $expectedKinds
183+
* @param list<int|string> $instantWinners
184+
*/
185+
private static function isOfTypeBasedOnKinds(
186+
array $expressionTokenKinds,
187+
array $expectedKinds,
188+
array $instantWinners
189+
): bool {
190+
foreach ($expressionTokenKinds as $index => $expressionTokenKind) {
191+
if (\in_array($expressionTokenKind, $instantWinners, true)) {
192+
return true;
193+
}
194+
if (\in_array($expressionTokenKind, $expectedKinds, true)) {
195+
unset($expressionTokenKinds[$index]);
196+
}
197+
}
198+
199+
return $expressionTokenKinds === [];
200+
}
201+
}

0 commit comments

Comments
 (0)