-
-
Notifications
You must be signed in to change notification settings - Fork 181
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SlevomatCodingStandard.TypeHints.ClassConstantTypeHint: New sniff
- Loading branch information
Showing
9 changed files
with
455 additions
and
0 deletions.
There are no files selected for viewing
196 changes: 196 additions & 0 deletions
196
SlevomatCodingStandard/Sniffs/TypeHints/ClassConstantTypeHintSniff.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,196 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace SlevomatCodingStandard\Sniffs\TypeHints; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
use SlevomatCodingStandard\Helpers\AnnotationHelper; | ||
use SlevomatCodingStandard\Helpers\ClassHelper; | ||
use SlevomatCodingStandard\Helpers\DocCommentHelper; | ||
use SlevomatCodingStandard\Helpers\FixerHelper; | ||
use SlevomatCodingStandard\Helpers\SniffSettingsHelper; | ||
use SlevomatCodingStandard\Helpers\TokenHelper; | ||
use function array_key_exists; | ||
use function count; | ||
use function sprintf; | ||
use const T_CONST; | ||
use const T_CONSTANT_ENCAPSED_STRING; | ||
use const T_DNUMBER; | ||
use const T_DOC_COMMENT_WHITESPACE; | ||
use const T_EQUAL; | ||
use const T_FALSE; | ||
use const T_LNUMBER; | ||
use const T_MINUS; | ||
use const T_NULL; | ||
use const T_OPEN_SHORT_ARRAY; | ||
use const T_START_HEREDOC; | ||
use const T_START_NOWDOC; | ||
use const T_TRUE; | ||
|
||
class ClassConstantTypeHintSniff implements Sniff | ||
{ | ||
|
||
public const CODE_MISSING_NATIVE_TYPE_HINT = 'MissingNativeTypeHint'; | ||
public const CODE_USELESS_DOC_COMMENT = 'UselessDocComment'; | ||
public const CODE_USELESS_VAR_ANNOTATION = 'UselessVarAnnotation'; | ||
|
||
public ?bool $enableNativeTypeHint = null; | ||
|
||
/** @var array<int|string, string> */ | ||
private static array $tokenToTypeHintMapping = [ | ||
T_FALSE => 'false', | ||
T_TRUE => 'true', | ||
T_DNUMBER => 'float', | ||
T_LNUMBER => 'int', | ||
T_NULL => 'null', | ||
T_OPEN_SHORT_ARRAY => 'array', | ||
T_CONSTANT_ENCAPSED_STRING => 'string', | ||
T_START_NOWDOC => 'string', | ||
T_START_HEREDOC => 'string', | ||
]; | ||
|
||
/** | ||
* @return array<int, (int|string)> | ||
*/ | ||
public function register(): array | ||
{ | ||
return [ | ||
T_CONST, | ||
]; | ||
} | ||
|
||
/** | ||
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint | ||
* @param int $constantPointer | ||
*/ | ||
public function process(File $phpcsFile, $constantPointer): void | ||
{ | ||
if (ClassHelper::getClassPointer($phpcsFile, $constantPointer) === null) { | ||
// Constant in namespace | ||
return; | ||
} | ||
|
||
$this->checkNativeTypeHint($phpcsFile, $constantPointer); | ||
$this->checkDocComment($phpcsFile, $constantPointer); | ||
} | ||
|
||
private function checkNativeTypeHint(File $phpcsFile, int $constantPointer): void | ||
{ | ||
$this->enableNativeTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableNativeTypeHint, 80300); | ||
|
||
if (!$this->enableNativeTypeHint) { | ||
return; | ||
} | ||
|
||
$namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer); | ||
$typeHintPointer = TokenHelper::findPreviousEffective($phpcsFile, $namePointer - 1); | ||
|
||
if ($typeHintPointer !== $constantPointer) { | ||
// Has type hint | ||
return; | ||
} | ||
|
||
$tokens = $phpcsFile->getTokens(); | ||
|
||
$namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer); | ||
$equalPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $constantPointer + 1); | ||
|
||
$valuePointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1); | ||
if ($tokens[$valuePointer]['code'] === T_MINUS) { | ||
$valuePointer = TokenHelper::findNextEffective($phpcsFile, $valuePointer + 1); | ||
} | ||
|
||
$constantName = $tokens[$namePointer]['content']; | ||
|
||
$typeHint = null; | ||
if (array_key_exists($tokens[$valuePointer]['code'], self::$tokenToTypeHintMapping)) { | ||
$typeHint = self::$tokenToTypeHintMapping[$tokens[$valuePointer]['code']]; | ||
} | ||
|
||
$errorParameters = [ | ||
sprintf('Constant %s does not have native type hint.', $constantName), | ||
$constantPointer, | ||
self::CODE_MISSING_NATIVE_TYPE_HINT, | ||
]; | ||
|
||
if ($typeHint === null) { | ||
$phpcsFile->addError(...$errorParameters); | ||
return; | ||
} | ||
|
||
$fix = $phpcsFile->addFixableError(...$errorParameters); | ||
|
||
if (!$fix) { | ||
return; | ||
} | ||
|
||
$phpcsFile->fixer->beginChangeset(); | ||
$phpcsFile->fixer->addContent($constantPointer, ' ' . $typeHint); | ||
$phpcsFile->fixer->endChangeset(); | ||
} | ||
|
||
private function checkDocComment(File $phpcsFile, int $constantPointer): void | ||
{ | ||
$docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $constantPointer); | ||
if ($docCommentOpenPointer === null) { | ||
return; | ||
} | ||
|
||
$annotations = AnnotationHelper::getAnnotations($phpcsFile, $constantPointer, '@var'); | ||
|
||
if ($annotations === []) { | ||
return; | ||
} | ||
|
||
$tokens = $phpcsFile->getTokens(); | ||
|
||
$namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer); | ||
$constantName = $tokens[$namePointer]['content']; | ||
|
||
$uselessDocComment = !DocCommentHelper::hasDocCommentDescription($phpcsFile, $constantPointer) && count($annotations) === 1; | ||
if ($uselessDocComment) { | ||
$fix = $phpcsFile->addFixableError( | ||
sprintf('Useless documentation comment for constant %s.', $constantName), | ||
$docCommentOpenPointer, | ||
self::CODE_USELESS_DOC_COMMENT, | ||
); | ||
|
||
/** @var int $fixerStart */ | ||
$fixerStart = TokenHelper::findLastTokenOnPreviousLine($phpcsFile, $docCommentOpenPointer); | ||
$fixerEnd = $tokens[$docCommentOpenPointer]['comment_closer']; | ||
} else { | ||
$annotation = $annotations[0]; | ||
|
||
$fix = $phpcsFile->addFixableError( | ||
sprintf('Useless @var annotation for constant %s.', $constantName), | ||
$annotation->getStartPointer(), | ||
self::CODE_USELESS_VAR_ANNOTATION, | ||
); | ||
|
||
/** @var int $fixerStart */ | ||
$fixerStart = TokenHelper::findPreviousContent( | ||
$phpcsFile, | ||
T_DOC_COMMENT_WHITESPACE, | ||
$phpcsFile->eolChar, | ||
$annotation->getStartPointer() - 1, | ||
); | ||
$fixerEnd = $annotation->getEndPointer(); | ||
} | ||
|
||
if (!$fix) { | ||
return; | ||
} | ||
|
||
$phpcsFile->fixer->beginChangeset(); | ||
FixerHelper::removeBetweenIncluding($phpcsFile, $fixerStart, $fixerEnd); | ||
$phpcsFile->fixer->endChangeset(); | ||
} | ||
|
||
private function getConstantNamePointer(File $phpcsFile, int $constantPointer): int | ||
{ | ||
$equalPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $constantPointer + 1); | ||
|
||
return TokenHelper::findPreviousEffective($phpcsFile, $equalPointer - 1); | ||
} | ||
|
||
} |
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
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,69 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace SlevomatCodingStandard\Sniffs\TypeHints; | ||
|
||
use SlevomatCodingStandard\Sniffs\TestCase; | ||
use function range; | ||
|
||
class ClassConstantTypeHintSniffTest extends TestCase | ||
{ | ||
|
||
public function testNativeTypeHintDisabled(): void | ||
{ | ||
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintNativeNoErrors.php', [ | ||
'enableNativeTypeHint' => false, | ||
]); | ||
self::assertNoSniffErrorInFile($report); | ||
} | ||
|
||
public function testNativeTypeHintNoErrors(): void | ||
{ | ||
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintNativeNoErrors.php', [ | ||
'enableNativeTypeHint' => true, | ||
]); | ||
self::assertNoSniffErrorInFile($report); | ||
} | ||
|
||
public function testNativeTypeHintErrors(): void | ||
{ | ||
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintNativeErrors.php', [ | ||
'enableNativeTypeHint' => true, | ||
]); | ||
|
||
self::assertSame(16, $report->getErrorCount()); | ||
|
||
foreach (range(6, 16) as $line) { | ||
self::assertSniffError($report, $line, ClassConstantTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT); | ||
} | ||
self::assertSniffError($report, 19, ClassConstantTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT); | ||
foreach (range(22, 25) as $line) { | ||
self::assertSniffError($report, $line, ClassConstantTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT); | ||
} | ||
|
||
self::assertAllFixedInFile($report); | ||
} | ||
|
||
public function testUselessDocCommentNoErrors(): void | ||
{ | ||
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintUselessDocCommentNoErrors.php', [ | ||
'enableNativeTypeHint' => false, | ||
]); | ||
self::assertNoSniffErrorInFile($report); | ||
} | ||
|
||
public function testUselessDocCommentErrors(): void | ||
{ | ||
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintUselessDocCommentErrors.php', [ | ||
'enableNativeTypeHint' => false, | ||
]); | ||
|
||
self::assertSame(3, $report->getErrorCount()); | ||
|
||
self::assertSniffError($report, 11, ClassConstantTypeHintSniff::CODE_USELESS_VAR_ANNOTATION); | ||
self::assertSniffError($report, 23, ClassConstantTypeHintSniff::CODE_USELESS_VAR_ANNOTATION); | ||
self::assertSniffError($report, 33, ClassConstantTypeHintSniff::CODE_USELESS_DOC_COMMENT); | ||
|
||
self::assertAllFixedInFile($report); | ||
} | ||
|
||
} |
27 changes: 27 additions & 0 deletions
27
tests/Sniffs/TypeHints/data/classConstantTypeHintNativeErrors.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,27 @@ | ||
<?php // lint >= 8.3 | ||
|
||
class Whatever | ||
{ | ||
|
||
const null C_NULL = null; | ||
const true C_TRUE = true; | ||
const false C_FALSE = false; | ||
const int C_NUMBER = 123; | ||
const int C_NEGATIVE_NUMBER = -123; | ||
const float C_FLOAT = 123.456; | ||
const float C_NEGATIVE_FLOAT = -123.456; | ||
const array C_ARRAY = ['php']; | ||
const string C_STRING = 'string'; | ||
const string C_STRING_DOUBLE_QUOTES = "string"; | ||
const string C_NOWDOC = <<<'NOWDOC' | ||
nowdoc | ||
NOWDOC; | ||
const string C_HEREDOC = <<<HEREDOC | ||
heredoc | ||
HEREDOC; | ||
const C_ENUM = SomeEnum::VALUE; | ||
const C_CONSTANT = Whatever::STRING; | ||
const C_CONSTANT_SELF = self::STRING; | ||
const C_CONSTANT_PARENT = parent::STRING; | ||
|
||
} |
27 changes: 27 additions & 0 deletions
27
tests/Sniffs/TypeHints/data/classConstantTypeHintNativeErrors.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,27 @@ | ||
<?php // lint >= 8.3 | ||
|
||
class Whatever | ||
{ | ||
|
||
const C_NULL = null; | ||
const C_TRUE = true; | ||
const C_FALSE = false; | ||
const C_NUMBER = 123; | ||
const C_NEGATIVE_NUMBER = -123; | ||
const C_FLOAT = 123.456; | ||
const C_NEGATIVE_FLOAT = -123.456; | ||
const C_ARRAY = ['php']; | ||
const C_STRING = 'string'; | ||
const C_STRING_DOUBLE_QUOTES = "string"; | ||
const C_NOWDOC = <<<'NOWDOC' | ||
nowdoc | ||
NOWDOC; | ||
const C_HEREDOC = <<<HEREDOC | ||
heredoc | ||
HEREDOC; | ||
const C_ENUM = SomeEnum::VALUE; | ||
const C_CONSTANT = Whatever::STRING; | ||
const C_CONSTANT_SELF = self::STRING; | ||
const C_CONSTANT_PARENT = parent::STRING; | ||
|
||
} |
23 changes: 23 additions & 0 deletions
23
tests/Sniffs/TypeHints/data/classConstantTypeHintNativeNoErrors.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,23 @@ | ||
<?php // lint >= 8.3 | ||
|
||
namespace SomeNamespace; | ||
|
||
const IGNORED = 'ignored'; | ||
|
||
class Whatever | ||
{ | ||
|
||
const null C_NULL = null; | ||
const true C_TRUE = true; | ||
const false C_FALSE = false; | ||
const string C_STRING = 'aa'; | ||
const int C_NUMBER = 123; | ||
const int C_NEGATIVE_NUMBER = -123; | ||
const float C_FLOAT = 123.456; | ||
const float C_NEGATIVE_FLOAT = -123.456; | ||
const array C_ARRAY = ['php']; | ||
const SomeEnum C_ENUM = SomeEnum::VALUE; | ||
const string C_CONSTANT = Whatever::STRING; | ||
|
||
|
||
} |
33 changes: 33 additions & 0 deletions
33
tests/Sniffs/TypeHints/data/classConstantTypeHintUselessDocCommentErrors.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,33 @@ | ||
<?php | ||
|
||
namespace Alphabet; | ||
|
||
class A | ||
{ | ||
|
||
/** | ||
* AA constant | ||
* | ||
*/ | ||
const AA = 'aa'; | ||
|
||
} | ||
|
||
interface B | ||
{ | ||
|
||
/** | ||
* BB constant | ||
* | ||
* @see anything | ||
*/ | ||
public const BB = true; | ||
|
||
} | ||
|
||
new class implements B | ||
{ | ||
|
||
const CC = 0; | ||
|
||
}; |
Oops, something went wrong.