Skip to content

Commit 69b6074

Browse files
committed
Add TypedClassConstantFixer
1 parent 059fa53 commit 69b6074

File tree

7 files changed

+244
-1
lines changed

7 files changed

+244
-1
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: 14 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-3722-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,19 @@ 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+
*Risky: when constant is not of single type.*
730+
```diff
731+
<?php
732+
class Foo {
733+
- public const MAX_VALUE_OF_SOMETHING = 42;
734+
- public const THE_NAME_OF_SOMEONE = 'John Doe';
735+
+ public const int MAX_VALUE_OF_SOMETHING = 42;
736+
+ public const string THE_NAME_OF_SOMEONE = 'John Doe';
737+
}
738+
```
739+
727740

728741
## Contributing
729742
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",
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
public function getDefinition(): FixerDefinitionInterface
25+
{
26+
return new FixerDefinition(
27+
'Class constants must have a type.',
28+
[
29+
new VersionSpecificCodeSample(
30+
<<<'PHP'
31+
<?php
32+
class Foo {
33+
public const MAX_VALUE_OF_SOMETHING = 42;
34+
public const THE_NAME_OF_SOMEONE = 'John Doe';
35+
}
36+
37+
PHP,
38+
new VersionSpecification(80300),
39+
),
40+
],
41+
'',
42+
'when constant is not of single type',
43+
);
44+
}
45+
46+
public function getPriority(): int
47+
{
48+
return 0;
49+
}
50+
51+
public function isCandidate(Tokens $tokens): bool
52+
{
53+
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_CONST]);
54+
}
55+
56+
public function isRisky(): bool
57+
{
58+
return true;
59+
}
60+
61+
public function fix(\SplFileInfo $file, Tokens $tokens): void
62+
{
63+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
64+
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
65+
continue;
66+
}
67+
68+
$openParenthesisIndex = $tokens->getNextTokenOfKind($index, ['{']);
69+
\assert(\is_int($openParenthesisIndex));
70+
71+
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openParenthesisIndex);
72+
73+
self::fixClass($tokens, $openParenthesisIndex, $closeParenthesisIndex);
74+
}
75+
}
76+
77+
private static function fixClass(Tokens $tokens, int $openParenthesisIndex, int $closeParenthesisIndex): void
78+
{
79+
for ($index = $closeParenthesisIndex; $index > $openParenthesisIndex; $index--) {
80+
if (!$tokens[$index]->isGivenKind(\T_CONST)) {
81+
continue;
82+
}
83+
84+
$constantNameIndex = $tokens->getNextMeaningfulToken($index);
85+
86+
$assignmentIndex = $tokens->getNextMeaningfulToken($constantNameIndex);
87+
if (!$tokens[$assignmentIndex]->equals('=')) {
88+
continue;
89+
}
90+
91+
$tokens->insertAt(
92+
$constantNameIndex,
93+
[
94+
self::getTypeOfExpression($tokens, $assignmentIndex),
95+
new Token([\T_WHITESPACE, ' ']),
96+
],
97+
);
98+
}
99+
}
100+
101+
private static function getTypeOfExpression(Tokens $tokens, int $assignmentIndex): Token
102+
{
103+
$semicolonIndex = $tokens->getNextTokenOfKind($assignmentIndex, [';']);
104+
$beforeSemicolonIndex = $tokens->getPrevMeaningfulToken($semicolonIndex);
105+
106+
$map = [
107+
\T_DNUMBER => [\T_STRING, 'float'],
108+
\T_LNUMBER => [\T_STRING, 'int'],
109+
\T_CONSTANT_ENCAPSED_STRING => [\T_STRING, 'string'],
110+
CT::T_ARRAY_SQUARE_BRACE_CLOSE => [CT::T_ARRAY_TYPEHINT, 'array'],
111+
];
112+
113+
$tokenId = $tokens[$beforeSemicolonIndex]->getId();
114+
115+
if ($tokens[$beforeSemicolonIndex]->isGivenKind(\T_STRING)) {
116+
return new Token([\T_STRING, $tokens[$beforeSemicolonIndex]->getContent()]);
117+
}
118+
119+
if ($tokenId === null & $tokens[$beforeSemicolonIndex]->equals(')')) {
120+
$tokenId = CT::T_ARRAY_SQUARE_BRACE_CLOSE;
121+
}
122+
123+
if (isset($map[$tokenId])) {
124+
return new Token($map[$tokenId]);
125+
}
126+
127+
\dd($tokens[$beforeSemicolonIndex], \token_name($tokens[$beforeSemicolonIndex]->getId()));
128+
129+
return new Token([\T_STRING, 'mixed']);
130+
}
131+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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\TypedClassConstantFixer
18+
*/
19+
final class TypedClassConstantFixerTest extends AbstractFixerTestCase
20+
{
21+
public function testIsRisky(): void
22+
{
23+
self::assertRiskiness(true);
24+
}
25+
26+
/**
27+
* @dataProvider provideFixCases
28+
*
29+
* @requires PHP >= 8.3
30+
*/
31+
public function testFix(string $expected, ?string $input = null): void
32+
{
33+
$this->doTest($expected, $input);
34+
}
35+
36+
/**
37+
* @return iterable<array{0: string, 1?: string}>
38+
*/
39+
public static function provideFixCases(): iterable
40+
{
41+
yield 'non-class constants are ignored' => ['<?php const FOO = 1;'];
42+
43+
yield 'array with long syntax' => [
44+
'<?php class Foo { public const array BAR = array(true, 1, "foo"); }',
45+
'<?php class Foo { public const BAR = array(true, 1, "foo"); }',
46+
];
47+
48+
yield 'array with short syntax' => [
49+
'<?php class Foo { public const array BAR = [true, 1, "foo"]; }',
50+
'<?php class Foo { public const BAR = [true, 1, "foo"]; }',
51+
];
52+
53+
yield 'false' => [
54+
'<?php class Foo { public const false BAR = false; }',
55+
'<?php class Foo { public const BAR = false; }',
56+
];
57+
58+
yield 'true' => [
59+
'<?php class Foo { public const true BAR = true; }',
60+
'<?php class Foo { public const BAR = true; }',
61+
];
62+
63+
yield 'float' => [
64+
'<?php class Foo { public const float BAR = 2.5; }',
65+
'<?php class Foo { public const BAR = 2.5; }',
66+
];
67+
68+
yield 'integer' => [
69+
'<?php class Foo { public const int BAR = 123; }',
70+
'<?php class Foo { public const BAR = 123; }',
71+
];
72+
73+
yield 'null' => [
74+
'<?php class Foo { public const null BAR = null; }',
75+
'<?php class Foo { public const BAR = null; }',
76+
];
77+
78+
yield 'string with double quotes' => [
79+
'<?php class Foo { public const string BAR = "Jane Doe"; }',
80+
'<?php class Foo { public const BAR = "Jane Doe"; }',
81+
];
82+
83+
yield 'string with single quotes' => [
84+
"<?php class Foo { public const string BAR = 'John Doe'; }",
85+
"<?php class Foo { public const BAR = 'John Doe'; }",
86+
];
87+
}
88+
}

0 commit comments

Comments
 (0)