diff --git a/SlevomatCodingStandard/Sniffs/TypeHints/MissingNativeConstantTypeSniff.php b/SlevomatCodingStandard/Sniffs/TypeHints/MissingNativeConstantTypeSniff.php new file mode 100644 index 000000000..ce7474e83 --- /dev/null +++ b/SlevomatCodingStandard/Sniffs/TypeHints/MissingNativeConstantTypeSniff.php @@ -0,0 +1,144 @@ + 'float', + self::T_LNUMBER => 'int', + T_NULL => 'null', + T_TRUE => 'true', + T_FALSE => 'false', + T_OPEN_SHORT_ARRAY => 'array', + self::T_CONSTANT_ENCAPSED_STRING => 'string', + ]; + + /** @var bool */ + public $enable = true; + + /** + * @return array + */ + public function register(): array + { + return [ + T_CONST, + ]; + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + * @param int $stackPtr + */ + public function process(File $phpcsFile, $stackPtr): void + { + $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80300); + + if (!$this->enable) { + return; + } + + if (SuppressHelper::isSniffSuppressed($phpcsFile, $stackPtr, self::NAME)) { + return; + } + + $tokens = $phpcsFile->getTokens(); + + /** @var int $classPointer */ + $classPointer = array_keys($tokens[$stackPtr]['conditions'])[count($tokens[$stackPtr]['conditions']) - 1]; + $typePointer = TokenHelper::findNextEffective($phpcsFile, $stackPtr + 1); + if (in_array($tokens[$typePointer]['code'], [T_NULL, T_TRUE, T_FALSE], true)) { + return; + } + + if ( + $tokens[$typePointer]['code'] === self::T_STRING + && in_array($tokens[$typePointer]['content'], ['int', 'string', 'float', 'double', 'array', 'object'], true) + ) { + return; + } + + $equalSignPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $stackPtr + 1); + $namePointer = TokenHelper::findPreviousEffective($phpcsFile, $equalSignPointer - 1); + + if ( + $tokens[$typePointer]['code'] === self::T_STRING + && $namePointer !== $typePointer + ) { + $className = NamespaceHelper::resolveClassName($phpcsFile, $tokens[$typePointer]['content'], $typePointer); + if (enum_exists($className)) { + return; + } + } + + $assignedValuePointer = TokenHelper::findNextEffective($phpcsFile, $equalSignPointer + 1); + if ($tokens[$assignedValuePointer]['code'] === T_MINUS) { + $assignedValuePointer = TokenHelper::findNextEffective($phpcsFile, $assignedValuePointer + 1); + } + + $fixableType = self::TOKEN_TO_TYPE_MAP[$tokens[$assignedValuePointer]['code']] ?? null; + if ($fixableType === null) { + $className = NamespaceHelper::resolveClassName($phpcsFile, $tokens[$assignedValuePointer]['content'], $assignedValuePointer); + if (enum_exists($className)) { + $fixableType = $tokens[$assignedValuePointer]['content']; + } + } + + if ($fixableType !== null) { + $message = sprintf( + 'Constant %s::%s is missing a type (%s).', + ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer), + $tokens[$namePointer]['content'], + $fixableType + ); + + $fix = $phpcsFile->addFixableError($message, $typePointer, self::CODE_MISSING_CONSTANT_TYPE); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContentBefore($typePointer, $fixableType . ' '); + $phpcsFile->fixer->endChangeset(); + } + + return; + } + + $message = sprintf( + 'Constant %s::%s is missing a type.', + ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer), + $tokens[$namePointer]['content'] + ); + + $phpcsFile->addError($message, $stackPtr, self::CODE_MISSING_CONSTANT_TYPE); + } + +} diff --git a/tests/Sniffs/TypeHints/MissingNativeConstantTypeSniffTest.php b/tests/Sniffs/TypeHints/MissingNativeConstantTypeSniffTest.php new file mode 100644 index 000000000..3060300e9 --- /dev/null +++ b/tests/Sniffs/TypeHints/MissingNativeConstantTypeSniffTest.php @@ -0,0 +1,68 @@ +getErrorCount()); + + for ($i = 8; $i < 16; $i++) { + self::assertSniffError($report, $i, MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE); + } + + for ($i = 23; $i < 31; $i++) { + self::assertSniffError($report, $i, MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE); + } + + for ($i = 38; $i < 46; $i++) { + self::assertSniffError($report, $i, MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE); + } + + for ($i = 53; $i < 61; $i++) { + self::assertSniffError($report, $i, MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE); + } + + self::assertAllFixedInFile($report); + } + + public function testIgnoredBySuppress(): void + { + $report = self::checkFile(__DIR__ . '/data/missingConstantTypeHintIgnoreErrors.php'); + + self::assertSame(0, $report->getErrorCount()); + } + + public function testWithEnableConfigEnabled(): void + { + $report = self::checkFile( + __DIR__ . '/data/missingConstantTypeHintErrors.php', + ['enable' => true], + [MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE] + ); + self::assertAllFixedInFile($report); + } + + public function testWithEnableConfigDisabled(): void + { + $report = self::checkFile( + __DIR__ . '/data/missingConstantTypeHintDisabled.php', + ['enable' => false], + [MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE] + ); + self::assertAllFixedInFile($report); + } + +} diff --git a/tests/Sniffs/TypeHints/data/missingConstantTypeHintDisabled.fixed.php b/tests/Sniffs/TypeHints/data/missingConstantTypeHintDisabled.fixed.php new file mode 100644 index 000000000..17f0476f9 --- /dev/null +++ b/tests/Sniffs/TypeHints/data/missingConstantTypeHintDisabled.fixed.php @@ -0,0 +1,55 @@ += 8.3 + +namespace PersonalHomePage; + +class A +{ + + const null AA = null; + const true AAA = true; + const false AAAA = false; + const string AAAAA = 'aa'; + const int AAAAAA = 123; + const float AAAAAAA = 123.456; + const array AAAAAAAA = ['php']; + const int AAAAAAAAA = -123; + const float AAAAAAAAAA = -123.456; + +} + +interface B +{ + + public const null BB = null; + public const true BBB = true; + public const false BBBB = false; + public const string BBBBB = 'aa'; + public const int BBBBBB = 123; + public const float BBBBBBB = 123.456; + public const array BBBBBBBB = ['php']; + public const int BBBBBBBBB = -123; + public const float BBBBBBBBBB = -123.456; + +} + +new class implements B +{ + + const null CC = null; + const true CCC = true; + const false CCCC = false; + const string CCCCC = 'aa'; + const int CCCCCC = 123; + const float CCCCCCC = 123.456; + const array CCCCCCCC = ['php']; + const int CCCCCCCCC = -123; + const float CCCCCCCCCC = -123.456; + +}; + +abstract class C +{ + + const null DD = null; + const true DDD = true; + const false DDDD = false; + const string DDDDD = 'aa'; + const int DDDDDD = 123; + const float DDDDDDD = 123.456; + const array DDDDDDDD = ['php']; + const int DDDDDDDDD = -123; + const float DDDDDDDDDD = -123.456; + +} diff --git a/tests/Sniffs/TypeHints/data/missingConstantTypeHintErrors.php b/tests/Sniffs/TypeHints/data/missingConstantTypeHintErrors.php new file mode 100644 index 000000000..ca3bba25f --- /dev/null +++ b/tests/Sniffs/TypeHints/data/missingConstantTypeHintErrors.php @@ -0,0 +1,63 @@ += 8.3 + +namespace PersonalHomePage; + +class A +{ + + const AA = null; + const AAA = true; + const AAAA = false; + const AAAAA = 'aa'; + const AAAAAA = 123; + const AAAAAAA = 123.456; + const AAAAAAAA = ['php']; + const AAAAAAAAA = -123; + const AAAAAAAAAA = -123.456; + +} + +interface B +{ + + public const BB = null; + public const BBB = true; + public const BBBB = false; + public const BBBBB = 'aa'; + public const BBBBBB = 123; + public const BBBBBBB = 123.456; + public const BBBBBBBB = ['php']; + public const BBBBBBBBB = -123; + public const BBBBBBBBBB = -123.456; + +} + +new class implements B +{ + + const CC = null; + const CCC = true; + const CCCC = false; + const CCCCC = 'aa'; + const CCCCCC = 123; + const CCCCCCC = 123.456; + const CCCCCCCC = ['php']; + const CCCCCCCCC = -123; + const CCCCCCCCCC = -123.456; + +}; + +abstract class C +{ + + const DD = null; + const DDD = true; + const DDDD = false; + const DDDDD = 'aa'; + const DDDDDD = 123; + const DDDDDDD = 123.456; + const DDDDDDDD = ['php']; + const DDDDDDDDD = -123; + const DDDDDDDDDD = -123.456; + +} diff --git a/tests/Sniffs/TypeHints/data/missingConstantTypeHintIgnoreErrors.php b/tests/Sniffs/TypeHints/data/missingConstantTypeHintIgnoreErrors.php new file mode 100644 index 000000000..84756ca9b --- /dev/null +++ b/tests/Sniffs/TypeHints/data/missingConstantTypeHintIgnoreErrors.php @@ -0,0 +1,23 @@ += 8.3 + +namespace PersonalHomePage; + +class A +{ + + const null AA = null; + const true AAA = true; + const false AAAA = false; + const string AAAAA = 'aa'; + const int AAAAAA = 123; + const float AAAAAAA = 123.456; + const array AAAAAAAA = ['php']; + +} + +interface B +{ + + public const null BB = null; + public const true BBB = true; + public const false BBBB = false; + public const string BBBBB = 'aa'; + public const int BBBBBB = 123; + public const float BBBBBBB = 123.456; + public const array BBBBBBBB = ['php']; + +} + +new class implements B +{ + + const null CC = null; + const true CCC = true; + const false CCCC = false; + const string CCCCC = 'aa'; + const int CCCCCC = 123; + const float CCCCCCC = 123.456; + const array CCCCCCCC = ['php']; + +}; + +abstract class C +{ + + const null DD = null; + const true DDD = true; + const false DDDD = false; + const string DDDDD = 'aa'; + const int DDDDDD = 123; + const float DDDDDDD = 123.456; + const array DDDDDDDD = ['php']; + +}