From 149130f4165982cf0af0feeff72416c0b675d7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismail=20=C3=96zg=C3=BCn=20Turan?= Date: Mon, 14 Oct 2024 10:09:41 +0200 Subject: [PATCH] Create a sniff to ensure constants being typed by native types. --- .../MissingNativeConstantTypeSniff.php | 144 ++++++++++++++++++ .../MissingNativeConstantTypeSniffTest.php | 68 +++++++++ .../missingConstantTypeHintDisabled.fixed.php | 55 +++++++ .../data/missingConstantTypeHintDisabled.php | 55 +++++++ .../missingConstantTypeHintErrors.fixed.php | 63 ++++++++ .../data/missingConstantTypeHintErrors.php | 63 ++++++++ .../missingConstantTypeHintIgnoreErrors.php | 23 +++ .../data/missingConstantTypeHintNoErrors.php | 55 +++++++ 8 files changed, 526 insertions(+) create mode 100644 SlevomatCodingStandard/Sniffs/TypeHints/MissingNativeConstantTypeSniff.php create mode 100644 tests/Sniffs/TypeHints/MissingNativeConstantTypeSniffTest.php create mode 100644 tests/Sniffs/TypeHints/data/missingConstantTypeHintDisabled.fixed.php create mode 100644 tests/Sniffs/TypeHints/data/missingConstantTypeHintDisabled.php create mode 100644 tests/Sniffs/TypeHints/data/missingConstantTypeHintErrors.fixed.php create mode 100644 tests/Sniffs/TypeHints/data/missingConstantTypeHintErrors.php create mode 100644 tests/Sniffs/TypeHints/data/missingConstantTypeHintIgnoreErrors.php create mode 100644 tests/Sniffs/TypeHints/data/missingConstantTypeHintNoErrors.php 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..96191b4a0 --- /dev/null +++ b/tests/Sniffs/TypeHints/data/missingConstantTypeHintErrors.php @@ -0,0 +1,63 @@ += 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']; + +}