Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
- if: github.event_name != 'pull_request'
run: rm ./.dev-tools/composer.lock
- run: composer update --no-progress
- run: "sed -i 's#constant: 0#constant: 100#g' .dev-tools/phpstan.neon"
- run: composer apply-typed_class_constant
- run: composer analyse

test:
Expand Down
2 changes: 2 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PhpCsFixerCustomFixers\Fixer\NoSuperfluousConcatenationFixer;
use PhpCsFixerCustomFixers\Fixer\PhpdocOnlyAllowedAnnotationsFixer;
use PhpCsFixerCustomFixers\Fixer\PromotedConstructorPropertyFixer;
use PhpCsFixerCustomFixers\Fixer\TypedClassConstantFixer;
use PhpCsFixerCustomFixers\Fixers;

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

$rules[PhpdocOnlyAllowedAnnotationsFixer::name()]['elements'][] = 'phpstan-type';
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CHANGELOG for PHP CS Fixer: custom fixers

## v3.26.0
- Add TypedClassConstantFixer

## v3.25.0
- Add ForeachUseValueFixer
- Add NoUselessWriteVisibilityFixer
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![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)
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
![Tests](https://img.shields.io/badge/tests-3691-brightgreen.svg)
![Tests](https://img.shields.io/badge/tests-3750-brightgreen.svg)
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)

[![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)
Expand Down Expand Up @@ -724,6 +724,18 @@ The string key of an array or generator must be trimmed and have no double space
];
```

#### TypedClassConstantFixer
Class constants must have a type.
```diff
<?php
class Foo {
- public const MAX_VALUE_OF_SOMETHING = 42;
- public const THE_NAME_OF_SOMEONE = 'John Doe';
+ public const int MAX_VALUE_OF_SOMETHING = 42;
+ public const string THE_NAME_OF_SOMEONE = 'John Doe';
}
```


## Contributing
Request a feature or report a bug by creating an [issue](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues).
Expand Down
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"@prepare-dev-tools",
"php-cs-fixer fix --quiet --rules=php_unit_attributes || exit 0"
],
"apply-typed_class_constant": [
"@prepare-dev-tools",
"php-cs-fixer fix --quiet --rules=PhpCsFixerCustomFixers/typed_class_constant || exit 0"
],
"fix": [
"@prepare-dev-tools",
"php-cs-fixer fix --ansi --verbose || exit 0",
Expand Down
201 changes: 201 additions & 0 deletions src/Fixer/TypedClassConstantFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php declare(strict_types=1);

/*
* This file is part of PHP CS Fixer: custom fixers.
*
* (c) 2018 Kuba Werłos
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace PhpCsFixerCustomFixers\Fixer;

use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

final class TypedClassConstantFixer extends AbstractFixer
{
private const INTEGER_KINDS = [\T_LNUMBER, '+', '-', '*', '(', ')', \T_POW, \T_SL, \T_SR, \T_LINE];
private const FLOAT_KINDS = [\T_DNUMBER, ...self::INTEGER_KINDS, '/'];
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];

public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Class constants must have a type.',
[
new VersionSpecificCodeSample(
<<<'PHP'
<?php
class Foo {
public const MAX_VALUE_OF_SOMETHING = 42;
public const THE_NAME_OF_SOMEONE = 'John Doe';
}

PHP,
new VersionSpecification(80300),
),
],
);
}

/**
* Must run after SingleClassElementPerStatementFixer.
*/
public function getPriority(): int
{
return 0;
}

public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_CONST]);
}

public function isRisky(): bool
{
return false;
}

public function fix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 1; $index > 0; $index--) {
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
continue;
}

$openParenthesisIndex = $tokens->getNextTokenOfKind($index, ['{']);
\assert(\is_int($openParenthesisIndex));

$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openParenthesisIndex);

self::fixClass($tokens, $openParenthesisIndex, $closeParenthesisIndex);
}
}

private static function fixClass(Tokens $tokens, int $openParenthesisIndex, int $closeParenthesisIndex): void
{
for ($index = $closeParenthesisIndex; $index > $openParenthesisIndex; $index--) {
if (!$tokens[$index]->isGivenKind(\T_CONST)) {
continue;
}

$constantNameIndex = $tokens->getNextMeaningfulToken($index);
\assert(\is_int($constantNameIndex));

$assignmentIndex = $tokens->getNextMeaningfulToken($constantNameIndex);
\assert(\is_int($assignmentIndex));

if (!$tokens[$assignmentIndex]->equals('=')) {
continue;
}

$expressionStartIndex = $tokens->getNextMeaningfulToken($assignmentIndex);
\assert(\is_int($expressionStartIndex));

if ($tokens[$expressionStartIndex]->isGivenKind(\T_NS_SEPARATOR)) {
$expressionStartIndex = $tokens->getNextMeaningfulToken($expressionStartIndex);
\assert(\is_int($expressionStartIndex));
}

$type = self::getTypeOfExpression($tokens, $expressionStartIndex);

$tokens->insertAt(
$constantNameIndex,
[
new Token([$type === 'array' ? CT::T_ARRAY_TYPEHINT : \T_STRING, $type]),
new Token([\T_WHITESPACE, ' ']),
],
);
}
}

private static function getTypeOfExpression(Tokens $tokens, int $index): string
{
$semicolonIndex = $tokens->getNextTokenOfKind($index, [';']);
\assert(\is_int($semicolonIndex));

$beforeSemicolonIndex = $tokens->getPrevMeaningfulToken($semicolonIndex);
\assert(\is_int($beforeSemicolonIndex));

$foundKinds = [];

$questionMarkCount = 0;
do {
if ($questionMarkCount > 1) {
return 'mixed';
}
$kind = $tokens[$index]->getId() ?? $tokens[$index]->getContent();
if ($kind === '?') {
$questionMarkCount++;
$foundKinds = [];
continue;
}
$foundKinds[] = $kind;

$index = $tokens->getNextMeaningfulToken($index);
\assert(\is_int($index));
} while ($index < $semicolonIndex);

if ($foundKinds === [\T_STRING]) {
$lowercasedContent = \strtolower($tokens[$beforeSemicolonIndex]->getContent());
if (\in_array($lowercasedContent, ['false', 'true', 'null'], true)) {
return $lowercasedContent;
}
}

return self::getTypeOfExpressionForTokenKinds($foundKinds);
}

/**
* @param list<int|string> $tokenKinds
*/
private static function getTypeOfExpressionForTokenKinds(array $tokenKinds): string
{
if (self::isOfTypeBasedOnKinds($tokenKinds, [], [\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
return 'array';
}

if (self::isOfTypeBasedOnKinds($tokenKinds, self::INTEGER_KINDS, [])) {
return 'int';
}

if (self::isOfTypeBasedOnKinds($tokenKinds, self::FLOAT_KINDS, [])) {
return 'float';
}

if (self::isOfTypeBasedOnKinds($tokenKinds, self::STRING_KINDS, ['.', CT::T_CLASS_CONSTANT])) {
return 'string';
}

return 'mixed';
}

/**
* @param list<int|string> $expressionTokenKinds
* @param list<int|string> $expectedKinds
* @param list<int|string> $instantWinners
*/
private static function isOfTypeBasedOnKinds(
array $expressionTokenKinds,
array $expectedKinds,
array $instantWinners
): bool {
foreach ($expressionTokenKinds as $index => $expressionTokenKind) {
if (\in_array($expressionTokenKind, $instantWinners, true)) {
return true;
}
if (\in_array($expressionTokenKind, $expectedKinds, true)) {
unset($expressionTokenKinds[$index]);
}
}

return $expressionTokenKinds === [];
}
}
Loading