Skip to content

Commit cae7dc6

Browse files
committed
SlevomatCodingStandard.TypeHints.ClassConstantTypeHint: New sniff
1 parent ad01519 commit cae7dc6

9 files changed

+455
-0
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\TypeHints;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use SlevomatCodingStandard\Helpers\AnnotationHelper;
8+
use SlevomatCodingStandard\Helpers\ClassHelper;
9+
use SlevomatCodingStandard\Helpers\DocCommentHelper;
10+
use SlevomatCodingStandard\Helpers\FixerHelper;
11+
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
12+
use SlevomatCodingStandard\Helpers\TokenHelper;
13+
use function array_key_exists;
14+
use function count;
15+
use function sprintf;
16+
use const T_CONST;
17+
use const T_CONSTANT_ENCAPSED_STRING;
18+
use const T_DNUMBER;
19+
use const T_DOC_COMMENT_WHITESPACE;
20+
use const T_EQUAL;
21+
use const T_FALSE;
22+
use const T_LNUMBER;
23+
use const T_MINUS;
24+
use const T_NULL;
25+
use const T_OPEN_SHORT_ARRAY;
26+
use const T_START_HEREDOC;
27+
use const T_START_NOWDOC;
28+
use const T_TRUE;
29+
30+
class ClassConstantTypeHintSniff implements Sniff
31+
{
32+
33+
public const CODE_MISSING_NATIVE_TYPE_HINT = 'MissingNativeTypeHint';
34+
public const CODE_USELESS_DOC_COMMENT = 'UselessDocComment';
35+
public const CODE_USELESS_VAR_ANNOTATION = 'UselessVarAnnotation';
36+
37+
public ?bool $enableNativeTypeHint = null;
38+
39+
/** @var array<int|string, string> */
40+
private static array $tokenToTypeHintMapping = [
41+
T_FALSE => 'false',
42+
T_TRUE => 'true',
43+
T_DNUMBER => 'float',
44+
T_LNUMBER => 'int',
45+
T_NULL => 'null',
46+
T_OPEN_SHORT_ARRAY => 'array',
47+
T_CONSTANT_ENCAPSED_STRING => 'string',
48+
T_START_NOWDOC => 'string',
49+
T_START_HEREDOC => 'string',
50+
];
51+
52+
/**
53+
* @return array<int, (int|string)>
54+
*/
55+
public function register(): array
56+
{
57+
return [
58+
T_CONST,
59+
];
60+
}
61+
62+
/**
63+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
64+
* @param int $constantPointer
65+
*/
66+
public function process(File $phpcsFile, $constantPointer): void
67+
{
68+
if (ClassHelper::getClassPointer($phpcsFile, $constantPointer) === null) {
69+
// Constant in namespace
70+
return;
71+
}
72+
73+
$this->checkNativeTypeHint($phpcsFile, $constantPointer);
74+
$this->checkDocComment($phpcsFile, $constantPointer);
75+
}
76+
77+
private function checkNativeTypeHint(File $phpcsFile, int $constantPointer): void
78+
{
79+
$this->enableNativeTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableNativeTypeHint, 80300);
80+
81+
if (!$this->enableNativeTypeHint) {
82+
return;
83+
}
84+
85+
$namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer);
86+
$typeHintPointer = TokenHelper::findPreviousEffective($phpcsFile, $namePointer - 1);
87+
88+
if ($typeHintPointer !== $constantPointer) {
89+
// Has type hint
90+
return;
91+
}
92+
93+
$tokens = $phpcsFile->getTokens();
94+
95+
$namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer);
96+
$equalPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $constantPointer + 1);
97+
98+
$valuePointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1);
99+
if ($tokens[$valuePointer]['code'] === T_MINUS) {
100+
$valuePointer = TokenHelper::findNextEffective($phpcsFile, $valuePointer + 1);
101+
}
102+
103+
$constantName = $tokens[$namePointer]['content'];
104+
105+
$typeHint = null;
106+
if (array_key_exists($tokens[$valuePointer]['code'], self::$tokenToTypeHintMapping)) {
107+
$typeHint = self::$tokenToTypeHintMapping[$tokens[$valuePointer]['code']];
108+
}
109+
110+
$errorParameters = [
111+
sprintf('Constant %s does not have native type hint.', $constantName),
112+
$constantPointer,
113+
self::CODE_MISSING_NATIVE_TYPE_HINT,
114+
];
115+
116+
if ($typeHint === null) {
117+
$phpcsFile->addError(...$errorParameters);
118+
return;
119+
}
120+
121+
$fix = $phpcsFile->addFixableError(...$errorParameters);
122+
123+
if (!$fix) {
124+
return;
125+
}
126+
127+
$phpcsFile->fixer->beginChangeset();
128+
$phpcsFile->fixer->addContent($constantPointer, ' ' . $typeHint);
129+
$phpcsFile->fixer->endChangeset();
130+
}
131+
132+
private function checkDocComment(File $phpcsFile, int $constantPointer): void
133+
{
134+
$docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $constantPointer);
135+
if ($docCommentOpenPointer === null) {
136+
return;
137+
}
138+
139+
$annotations = AnnotationHelper::getAnnotations($phpcsFile, $constantPointer, '@var');
140+
141+
if ($annotations === []) {
142+
return;
143+
}
144+
145+
$tokens = $phpcsFile->getTokens();
146+
147+
$namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer);
148+
$constantName = $tokens[$namePointer]['content'];
149+
150+
$uselessDocComment = !DocCommentHelper::hasDocCommentDescription($phpcsFile, $constantPointer) && count($annotations) === 1;
151+
if ($uselessDocComment) {
152+
$fix = $phpcsFile->addFixableError(
153+
sprintf('Useless documentation comment for constant %s.', $constantName),
154+
$docCommentOpenPointer,
155+
self::CODE_USELESS_DOC_COMMENT,
156+
);
157+
158+
/** @var int $fixerStart */
159+
$fixerStart = TokenHelper::findLastTokenOnPreviousLine($phpcsFile, $docCommentOpenPointer);
160+
$fixerEnd = $tokens[$docCommentOpenPointer]['comment_closer'];
161+
} else {
162+
$annotation = $annotations[0];
163+
164+
$fix = $phpcsFile->addFixableError(
165+
sprintf('Useless @var annotation for constant %s.', $constantName),
166+
$annotation->getStartPointer(),
167+
self::CODE_USELESS_VAR_ANNOTATION,
168+
);
169+
170+
/** @var int $fixerStart */
171+
$fixerStart = TokenHelper::findPreviousContent(
172+
$phpcsFile,
173+
T_DOC_COMMENT_WHITESPACE,
174+
$phpcsFile->eolChar,
175+
$annotation->getStartPointer() - 1,
176+
);
177+
$fixerEnd = $annotation->getEndPointer();
178+
}
179+
180+
if (!$fix) {
181+
return;
182+
}
183+
184+
$phpcsFile->fixer->beginChangeset();
185+
FixerHelper::removeBetweenIncluding($phpcsFile, $fixerStart, $fixerEnd);
186+
$phpcsFile->fixer->endChangeset();
187+
}
188+
189+
private function getConstantNamePointer(File $phpcsFile, int $constantPointer): int
190+
{
191+
$equalPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $constantPointer + 1);
192+
193+
return TokenHelper::findPreviousEffective($phpcsFile, $equalPointer - 1);
194+
}
195+
196+
}

doc/type-hints.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## Type hints
22

3+
#### SlevomatCodingStandard.TypeHints.ClassConstantTypeHint 🔧
4+
5+
* Checks for missing typehints in case they can be declared natively.
6+
* Reports useless `@var` annotation (or whole documentation comment) because the type of constant is always clear.
7+
8+
Sniff provides the following settings:
9+
10+
* `enableNativeTypeHint`: enforces native typehint. It's on by default if you're on PHP 8.3+
11+
312
#### SlevomatCodingStandard.TypeHints.DeclareStrictTypes 🔧
413

514
Enforces having `declare(strict_types = 1)` at the top of each PHP file. Allows configuring how many newlines should be between the `<?php` opening tag and the `declare` statement.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\TypeHints;
4+
5+
use SlevomatCodingStandard\Sniffs\TestCase;
6+
use function range;
7+
8+
class ClassConstantTypeHintSniffTest extends TestCase
9+
{
10+
11+
public function testNativeTypeHintDisabled(): void
12+
{
13+
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintNativeNoErrors.php', [
14+
'enableNativeTypeHint' => false,
15+
]);
16+
self::assertNoSniffErrorInFile($report);
17+
}
18+
19+
public function testNativeTypeHintNoErrors(): void
20+
{
21+
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintNativeNoErrors.php', [
22+
'enableNativeTypeHint' => true,
23+
]);
24+
self::assertNoSniffErrorInFile($report);
25+
}
26+
27+
public function testNativeTypeHintErrors(): void
28+
{
29+
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintNativeErrors.php', [
30+
'enableNativeTypeHint' => true,
31+
]);
32+
33+
self::assertSame(16, $report->getErrorCount());
34+
35+
foreach (range(6, 16) as $line) {
36+
self::assertSniffError($report, $line, ClassConstantTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
37+
}
38+
self::assertSniffError($report, 19, ClassConstantTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
39+
foreach (range(22, 25) as $line) {
40+
self::assertSniffError($report, $line, ClassConstantTypeHintSniff::CODE_MISSING_NATIVE_TYPE_HINT);
41+
}
42+
43+
self::assertAllFixedInFile($report);
44+
}
45+
46+
public function testUselessDocCommentNoErrors(): void
47+
{
48+
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintUselessDocCommentNoErrors.php', [
49+
'enableNativeTypeHint' => false,
50+
]);
51+
self::assertNoSniffErrorInFile($report);
52+
}
53+
54+
public function testUselessDocCommentErrors(): void
55+
{
56+
$report = self::checkFile(__DIR__ . '/data/classConstantTypeHintUselessDocCommentErrors.php', [
57+
'enableNativeTypeHint' => false,
58+
]);
59+
60+
self::assertSame(3, $report->getErrorCount());
61+
62+
self::assertSniffError($report, 11, ClassConstantTypeHintSniff::CODE_USELESS_VAR_ANNOTATION);
63+
self::assertSniffError($report, 23, ClassConstantTypeHintSniff::CODE_USELESS_VAR_ANNOTATION);
64+
self::assertSniffError($report, 33, ClassConstantTypeHintSniff::CODE_USELESS_DOC_COMMENT);
65+
66+
self::assertAllFixedInFile($report);
67+
}
68+
69+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php // lint >= 8.3
2+
3+
class Whatever
4+
{
5+
6+
const null C_NULL = null;
7+
const true C_TRUE = true;
8+
const false C_FALSE = false;
9+
const int C_NUMBER = 123;
10+
const int C_NEGATIVE_NUMBER = -123;
11+
const float C_FLOAT = 123.456;
12+
const float C_NEGATIVE_FLOAT = -123.456;
13+
const array C_ARRAY = ['php'];
14+
const string C_STRING = 'string';
15+
const string C_STRING_DOUBLE_QUOTES = "string";
16+
const string C_NOWDOC = <<<'NOWDOC'
17+
nowdoc
18+
NOWDOC;
19+
const string C_HEREDOC = <<<HEREDOC
20+
heredoc
21+
HEREDOC;
22+
const C_ENUM = SomeEnum::VALUE;
23+
const C_CONSTANT = Whatever::STRING;
24+
const C_CONSTANT_SELF = self::STRING;
25+
const C_CONSTANT_PARENT = parent::STRING;
26+
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php // lint >= 8.3
2+
3+
class Whatever
4+
{
5+
6+
const C_NULL = null;
7+
const C_TRUE = true;
8+
const C_FALSE = false;
9+
const C_NUMBER = 123;
10+
const C_NEGATIVE_NUMBER = -123;
11+
const C_FLOAT = 123.456;
12+
const C_NEGATIVE_FLOAT = -123.456;
13+
const C_ARRAY = ['php'];
14+
const C_STRING = 'string';
15+
const C_STRING_DOUBLE_QUOTES = "string";
16+
const C_NOWDOC = <<<'NOWDOC'
17+
nowdoc
18+
NOWDOC;
19+
const C_HEREDOC = <<<HEREDOC
20+
heredoc
21+
HEREDOC;
22+
const C_ENUM = SomeEnum::VALUE;
23+
const C_CONSTANT = Whatever::STRING;
24+
const C_CONSTANT_SELF = self::STRING;
25+
const C_CONSTANT_PARENT = parent::STRING;
26+
27+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php // lint >= 8.3
2+
3+
namespace SomeNamespace;
4+
5+
const IGNORED = 'ignored';
6+
7+
class Whatever
8+
{
9+
10+
const null C_NULL = null;
11+
const true C_TRUE = true;
12+
const false C_FALSE = false;
13+
const string C_STRING = 'aa';
14+
const int C_NUMBER = 123;
15+
const int C_NEGATIVE_NUMBER = -123;
16+
const float C_FLOAT = 123.456;
17+
const float C_NEGATIVE_FLOAT = -123.456;
18+
const array C_ARRAY = ['php'];
19+
const SomeEnum C_ENUM = SomeEnum::VALUE;
20+
const string C_CONSTANT = Whatever::STRING;
21+
22+
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Alphabet;
4+
5+
class A
6+
{
7+
8+
/**
9+
* AA constant
10+
*
11+
*/
12+
const AA = 'aa';
13+
14+
}
15+
16+
interface B
17+
{
18+
19+
/**
20+
* BB constant
21+
*
22+
* @see anything
23+
*/
24+
public const BB = true;
25+
26+
}
27+
28+
new class implements B
29+
{
30+
31+
const CC = 0;
32+
33+
};

0 commit comments

Comments
 (0)