Skip to content

Commit 5e98279

Browse files
committed
Add TypedClassConstantFixer
1 parent 059fa53 commit 5e98279

File tree

7 files changed

+348
-1
lines changed

7 files changed

+348
-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-3729-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 can be of different types.*
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: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 TOKEN_TO_TYPE_MAP = [
25+
\T_DNUMBER => 'float',
26+
\T_LNUMBER => 'int',
27+
\T_CONSTANT_ENCAPSED_STRING => 'string',
28+
CT::T_ARRAY_SQUARE_BRACE_CLOSE => 'array',
29+
];
30+
31+
public function getDefinition(): FixerDefinitionInterface
32+
{
33+
return new FixerDefinition(
34+
'Class constants must have a type.',
35+
[
36+
new VersionSpecificCodeSample(
37+
<<<'PHP'
38+
<?php
39+
class Foo {
40+
public const MAX_VALUE_OF_SOMETHING = 42;
41+
public const THE_NAME_OF_SOMEONE = 'John Doe';
42+
}
43+
44+
PHP,
45+
new VersionSpecification(80300),
46+
),
47+
],
48+
'',
49+
'when constant can be of different types',
50+
);
51+
}
52+
53+
public function getPriority(): int
54+
{
55+
return 0;
56+
}
57+
58+
public function isCandidate(Tokens $tokens): bool
59+
{
60+
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_CONST]);
61+
}
62+
63+
public function isRisky(): bool
64+
{
65+
return true;
66+
}
67+
68+
public function fix(\SplFileInfo $file, Tokens $tokens): void
69+
{
70+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
71+
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
72+
continue;
73+
}
74+
75+
$openParenthesisIndex = $tokens->getNextTokenOfKind($index, ['{']);
76+
\assert(\is_int($openParenthesisIndex));
77+
78+
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openParenthesisIndex);
79+
80+
self::fixClass($tokens, $openParenthesisIndex, $closeParenthesisIndex);
81+
}
82+
}
83+
84+
private static function fixClass(Tokens $tokens, int $openParenthesisIndex, int $closeParenthesisIndex): void
85+
{
86+
for ($index = $closeParenthesisIndex; $index > $openParenthesisIndex; $index--) {
87+
if (!$tokens[$index]->isGivenKind(\T_CONST)) {
88+
continue;
89+
}
90+
91+
$constantNameIndex = $tokens->getNextMeaningfulToken($index);
92+
\assert(\is_int($constantNameIndex));
93+
94+
$assignmentIndex = $tokens->getNextMeaningfulToken($constantNameIndex);
95+
\assert(\is_int($assignmentIndex));
96+
97+
if (!$tokens[$assignmentIndex]->equals('=')) {
98+
continue;
99+
}
100+
101+
$type = self::getTypeOfExpression($tokens, $assignmentIndex);
102+
103+
$tokens->insertAt(
104+
$constantNameIndex,
105+
[
106+
new Token([$type === 'array' ? CT::T_ARRAY_TYPEHINT : \T_STRING, $type]),
107+
new Token([\T_WHITESPACE, ' ']),
108+
],
109+
);
110+
}
111+
}
112+
113+
private static function getTypeOfExpression(Tokens $tokens, int $assignmentIndex): string
114+
{
115+
$semicolonIndex = $tokens->getNextTokenOfKind($assignmentIndex, [';']);
116+
\assert(\is_int($semicolonIndex));
117+
118+
$beforeSemicolonIndex = $tokens->getPrevMeaningfulToken($semicolonIndex);
119+
\assert(\is_int($beforeSemicolonIndex));
120+
121+
$tokenId = $tokens[$beforeSemicolonIndex]->getId();
122+
123+
if (isset(self::TOKEN_TO_TYPE_MAP[$tokenId])) {
124+
return self::TOKEN_TO_TYPE_MAP[$tokenId];
125+
}
126+
127+
if ($tokens[$beforeSemicolonIndex]->isGivenKind(\T_STRING)) {
128+
$lowercasedContent = \strtolower($tokens[$beforeSemicolonIndex]->getContent());
129+
if (\in_array($lowercasedContent, ['false', 'true', 'null'], true)) {
130+
return $lowercasedContent;
131+
}
132+
}
133+
134+
if ($tokens[$beforeSemicolonIndex]->equals(')')) {
135+
$openParenthesisIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $beforeSemicolonIndex);
136+
137+
$arrayIndex = $tokens->getPrevMeaningfulToken($openParenthesisIndex);
138+
\assert(\is_int($arrayIndex));
139+
140+
if ($tokens[$arrayIndex]->isGivenKind(\T_ARRAY)) {
141+
return 'array';
142+
}
143+
}
144+
145+
return 'mixed';
146+
}
147+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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 'integer as result of sum' => [
74+
'<?php class Foo { public const int BAR = 1 + 2 + 3; }',
75+
'<?php class Foo { public const BAR = 1 + 2 + 3; }',
76+
];
77+
78+
yield 'null' => [
79+
'<?php class Foo { public const null BAR = null; }',
80+
'<?php class Foo { public const BAR = null; }',
81+
];
82+
83+
yield 'NULL' => [
84+
'<?php class Foo { public const null BAR = NULL; }',
85+
'<?php class Foo { public const BAR = NULL; }',
86+
];
87+
88+
yield 'string with double quotes' => [
89+
'<?php class Foo { public const string BAR = "Jane Doe"; }',
90+
'<?php class Foo { public const BAR = "Jane Doe"; }',
91+
];
92+
93+
yield 'string with single quotes' => [
94+
"<?php class Foo { public const string BAR = 'John Doe'; }",
95+
"<?php class Foo { public const BAR = 'John Doe'; }",
96+
];
97+
98+
yield 'unknown other constant' => [
99+
'<?php class Foo { public const mixed BAR = CONSTANT_FROM_FAR_AWAY; }',
100+
'<?php class Foo { public const BAR = CONSTANT_FROM_FAR_AWAY; }',
101+
];
102+
103+
yield 'expression of unknown type' => [
104+
'<?php class Foo { public const mixed BAR = 10 * (FLOAT_OR_INTEGER + 7); }',
105+
'<?php class Foo { public const BAR = 10 * (FLOAT_OR_INTEGER + 7); }',
106+
];
107+
108+
yield 'multiple constants' => [
109+
<<<'PHP'
110+
<?php
111+
class Foo {
112+
public const int ONE = 1;
113+
protected const bool|float ALREADY_TYPED = true;
114+
private const string NAME = 'name';
115+
}
116+
const NOT_CLASS = true;
117+
class Bar {
118+
const array ARRAY_LONG_SYNTAX = array();
119+
const array ARRAY_SHORT_SYNTAX = [];
120+
const string lowercased_name = 'lowercased_name';
121+
}
122+
PHP,
123+
<<<'PHP'
124+
<?php
125+
class Foo {
126+
public const ONE = 1;
127+
protected const bool|float ALREADY_TYPED = true;
128+
private const NAME = 'name';
129+
}
130+
const NOT_CLASS = true;
131+
class Bar {
132+
const ARRAY_LONG_SYNTAX = array();
133+
const ARRAY_SHORT_SYNTAX = [];
134+
const lowercased_name = 'lowercased_name';
135+
}
136+
PHP,
137+
];
138+
139+
// if someone does these, they deserve to have their code broken
140+
yield 'constant that can be of different types' => [
141+
'<?php class Foo { public const string BAR = SHOULD_BE_INT ? 1 : "one"; }',
142+
'<?php class Foo { public const BAR = SHOULD_BE_INT ? 1 : "one"; }',
143+
];
144+
145+
yield 'constant that can be of different types - more complex case' => [
146+
<<<'PHP'
147+
<?php
148+
class HellCoreServiceManagerHelper
149+
{
150+
const float OPTION_666__YES__1010011010_VALUE_4_1_3
151+
= IS_OVERRIDEN_BY_BEELZEBOSS
152+
? "Hell yeah"
153+
: CIRCLES_MANAGER_ACCESS === [0o1232, 'super_manager', false, -66.6]
154+
? true
155+
: HellComponent443556::SHOULDNT_NOT_BE_DIFFERENT_THAN_NULL
156+
? null
157+
: 0.001;
158+
}
159+
PHP,
160+
<<<'PHP'
161+
<?php
162+
class HellCoreServiceManagerHelper
163+
{
164+
const OPTION_666__YES__1010011010_VALUE_4_1_3
165+
= IS_OVERRIDEN_BY_BEELZEBOSS
166+
? "Hell yeah"
167+
: CIRCLES_MANAGER_ACCESS === [0o1232, 'super_manager', false, -66.6]
168+
? true
169+
: HellComponent443556::SHOULDNT_NOT_BE_DIFFERENT_THAN_NULL
170+
? null
171+
: 0.001;
172+
}
173+
PHP,
174+
];
175+
}
176+
}

0 commit comments

Comments
 (0)