-
-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a sniff to ensure constants being typed by native types.
- Loading branch information
Showing
8 changed files
with
526 additions
and
0 deletions.
There are no files selected for viewing
144 changes: 144 additions & 0 deletions
144
SlevomatCodingStandard/Sniffs/TypeHints/MissingNativeConstantTypeSniff.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace SlevomatCodingStandard\Sniffs\TypeHints; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
use SlevomatCodingStandard\Helpers\ClassHelper; | ||
use SlevomatCodingStandard\Helpers\NamespaceHelper; | ||
use SlevomatCodingStandard\Helpers\SniffSettingsHelper; | ||
use SlevomatCodingStandard\Helpers\SuppressHelper; | ||
use SlevomatCodingStandard\Helpers\TokenHelper; | ||
use function array_keys; | ||
use function count; | ||
use function enum_exists; | ||
use function in_array; | ||
use function sprintf; | ||
use const T_CONST; | ||
use const T_EQUAL; | ||
use const T_FALSE; | ||
use const T_MINUS; | ||
use const T_NULL; | ||
use const T_OPEN_SHORT_ARRAY; | ||
use const T_TRUE; | ||
|
||
class MissingNativeConstantTypeSniff implements Sniff | ||
{ | ||
|
||
public const CODE_MISSING_CONSTANT_TYPE = 'MissingConstantType'; | ||
private const NAME = 'SlevomatCodingStandard.TypeHints.MissingConstantType'; | ||
|
||
private const T_LNUMBER = 311; | ||
private const T_DNUMBER = 312; | ||
private const T_STRING = 313; | ||
private const T_CONSTANT_ENCAPSED_STRING = 320; | ||
|
||
private const TOKEN_TO_TYPE_MAP = [ | ||
self::T_DNUMBER => '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<int, (int|string)> | ||
*/ | ||
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); | ||
} | ||
|
||
} |
68 changes: 68 additions & 0 deletions
68
tests/Sniffs/TypeHints/MissingNativeConstantTypeSniffTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace SlevomatCodingStandard\Sniffs\TypeHints; | ||
|
||
use SlevomatCodingStandard\Sniffs\TestCase; | ||
|
||
class MissingNativeConstantTypeSniffTest extends TestCase | ||
{ | ||
|
||
public function testNoErrors(): void | ||
{ | ||
$report = self::checkFile(__DIR__ . '/data/missingConstantTypeHintNoErrors.php'); | ||
self::assertNoSniffErrorInFile($report); | ||
} | ||
|
||
public function testErrors(): void | ||
{ | ||
$report = self::checkFile(__DIR__ . '/data/missingConstantTypeHintErrors.php'); | ||
|
||
self::assertSame(36, $report->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); | ||
} | ||
|
||
} |
55 changes: 55 additions & 0 deletions
55
tests/Sniffs/TypeHints/data/missingConstantTypeHintDisabled.fixed.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php | ||
|
||
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']; | ||
|
||
} | ||
|
||
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']; | ||
|
||
} | ||
|
||
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']; | ||
|
||
}; | ||
|
||
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']; | ||
|
||
} |
55 changes: 55 additions & 0 deletions
55
tests/Sniffs/TypeHints/data/missingConstantTypeHintDisabled.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php | ||
|
||
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']; | ||
|
||
} | ||
|
||
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']; | ||
|
||
} | ||
|
||
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']; | ||
|
||
}; | ||
|
||
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']; | ||
|
||
} |
63 changes: 63 additions & 0 deletions
63
tests/Sniffs/TypeHints/data/missingConstantTypeHintErrors.fixed.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<?php // lint >= 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; | ||
|
||
} |
Oops, something went wrong.