Skip to content

Commit 7361697

Browse files
committed
PHPCS-240 Required explicit asserts instead of inline var annotations
Signed-off-by: Björn Lange <[email protected]>
1 parent c7e1e76 commit 7361697

File tree

9 files changed

+416
-2
lines changed

9 files changed

+416
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ The base for the BestIt Standard is [PSR-12](https://github.com/php-fig/fig-stan
159159
| BestIt.Functions.MultipleReturn.MultipleReturnsFound | You SHOULD only use a return per method. | no |
160160
| BestIt.Functions.TrailingCommaInCall.MissingTrailingComma | You MUST append a trailing command in your multi line function calls. | no |
161161
| BestIt.NamingConventions.CamelCaseVariable.NotCamelCase | You MUST provide your vars in camel case, lower case first. | yes |
162+
| BestIt.TypeHints.ExplicitAssertions.RequiredExplicitAssertion | Use assertion instead of inline documentation comment. | no |
162163
| BestIt.TypeHints.ReturnTypeDeclaration.MissingReturnTypeHint | Every function or method MUST have a type hint if the return annotation is valid. | yes |
163164
| Generic.Formatting.SpaceAfterCast | There MUST be a space after cast. |
164165
| Generic.Arrays.DisallowLongArraySyntax | Every array syntax MUST be in short array syntax. |

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
],
1616
"autoload": {
1717
"psr-4": {
18-
"BestIt\\": "src/Standards/BestIt"
18+
"BestIt\\": "src/Standards/BestIt"
1919
}
2020
},
2121
"autoload-dev": {

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
<exclude-pattern>*/Fixtures/*.php</exclude-pattern>
1414

1515
<!-- So many copies of the original code from the sniffer/slevomat. Ignore it! -->
16+
<exclude-pattern>*/ExplicitAssertionsSniff.php</exclude-pattern>
1617
<exclude-pattern>*/SniffTestCase.php</exclude-pattern>
1718
</ruleset>

src/Standards/BestIt/CodeSniffer/Helper/DocTagHelper.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHP_CodeSniffer\Files\File;
88
use function array_key_exists;
99
use function in_array;
10+
use function is_int;
1011
use const T_DOC_COMMENT_CLOSE_TAG;
1112
use const T_DOC_COMMENT_STRING;
1213
use const T_DOC_COMMENT_TAG;
@@ -106,8 +107,8 @@ public function getTagTokens(): array
106107
$tagPositions = $this->getCommentStartToken()['comment_tags'];
107108
$tagTokens = [];
108109

109-
/** @var int $tagPos */
110110
foreach ($tagPositions as $tagPos) {
111+
assert(is_int($tagPos));
111112
if ($tagPos >= $iteratedPos) {
112113
$tagTokens[$tagPos] = $this->tokens[$tagPos] + [
113114
'contents' => $this->loadTagContentTokens($tagPos, $iteratedPos),
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace BestIt\Sniffs\TypeHints;
6+
7+
use PHP_CodeSniffer\Files\File;
8+
use PHP_CodeSniffer\Sniffs\Sniff;
9+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
10+
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
11+
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
12+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
13+
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
14+
use SlevomatCodingStandard\Helpers\Annotation\VariableAnnotation;
15+
use SlevomatCodingStandard\Helpers\AnnotationHelper;
16+
use SlevomatCodingStandard\Helpers\IndentationHelper;
17+
use SlevomatCodingStandard\Helpers\TokenHelper;
18+
use SlevomatCodingStandard\Helpers\TypeHintHelper;
19+
use function array_key_exists;
20+
use function array_merge;
21+
use function array_reverse;
22+
use function array_unique;
23+
use function implode;
24+
use function in_array;
25+
use function sprintf;
26+
use function trim;
27+
use const T_AS;
28+
use const T_DOC_COMMENT_OPEN_TAG;
29+
use const T_EQUAL;
30+
use const T_FOREACH;
31+
use const T_LIST;
32+
use const T_OPEN_SHORT_ARRAY;
33+
use const T_SEMICOLON;
34+
use const T_VARIABLE;
35+
use const T_WHILE;
36+
use const T_WHITESPACE;
37+
38+
/**
39+
* Use assertion instead of inline documentation comment.
40+
*
41+
* THIS FILE IS "COPIED"!
42+
*
43+
* @author blange <[email protected]>
44+
* @package BestIt\Sniffs\TypeHints
45+
* @SuppressWarnings(PHPMD)
46+
*/
47+
class ExplicitAssertionsSniff implements Sniff
48+
{
49+
50+
public const CODE_REQUIRED_EXPLICIT_ASSERTION = 'RequiredExplicitAssertion';
51+
52+
/**
53+
* @return array<int, (int|string)>
54+
*/
55+
public function register(): array
56+
{
57+
return [
58+
T_DOC_COMMENT_OPEN_TAG,
59+
];
60+
}
61+
62+
/**
63+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
64+
* @param File $phpcsFile
65+
* @param int $docCommentOpenPointer
66+
*/
67+
public function process(File $phpcsFile, $docCommentOpenPointer): void
68+
{
69+
$tokens = $phpcsFile->getTokens();
70+
71+
$tokenCodes = [T_VARIABLE, T_FOREACH, T_WHILE, T_LIST, T_OPEN_SHORT_ARRAY];
72+
$commentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer'];
73+
74+
$codePointer = TokenHelper::findFirstNonWhitespaceOnNextLine($phpcsFile, $commentClosePointer);
75+
76+
if ($codePointer === null || !in_array($tokens[$codePointer]['code'], $tokenCodes, true)) {
77+
$firstPointerOnPreviousLine = TokenHelper::findFirstNonWhitespaceOnPreviousLine($phpcsFile, $docCommentOpenPointer);
78+
if ($firstPointerOnPreviousLine === null || !in_array($tokens[$firstPointerOnPreviousLine]['code'], $tokenCodes, true)) {
79+
return;
80+
}
81+
82+
$codePointer = $firstPointerOnPreviousLine;
83+
}
84+
85+
$variableAnnotations = AnnotationHelper::getAnnotationsByName($phpcsFile, $docCommentOpenPointer, '@var');
86+
if (count($variableAnnotations) === 0) {
87+
return;
88+
}
89+
90+
/** @var VariableAnnotation $variableAnnotation */
91+
foreach (array_reverse($variableAnnotations) as $variableAnnotation) {
92+
if ($variableAnnotation->isInvalid()) {
93+
continue;
94+
}
95+
96+
if ($variableAnnotation->getVariableName() === null) {
97+
continue;
98+
}
99+
100+
$variableAnnotationType = $variableAnnotation->getType();
101+
102+
if ($variableAnnotationType instanceof UnionTypeNode || $variableAnnotationType instanceof IntersectionTypeNode) {
103+
foreach ($variableAnnotationType->types as $typeNode) {
104+
if (!$this->isValidTypeNode($typeNode)) {
105+
continue 2;
106+
}
107+
}
108+
} elseif (!$this->isValidTypeNode($variableAnnotationType)) {
109+
continue;
110+
}
111+
112+
if ($tokens[$codePointer]['code'] === T_VARIABLE) {
113+
$pointerAfterVariable = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1);
114+
if ($tokens[$pointerAfterVariable]['code'] !== T_EQUAL) {
115+
continue;
116+
}
117+
118+
if ($variableAnnotation->getVariableName() !== $tokens[$codePointer]['content']) {
119+
continue;
120+
}
121+
122+
$pointerToAddAssertion = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $codePointer + 1);
123+
$indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer);
124+
125+
} elseif ($tokens[$codePointer]['code'] === T_LIST) {
126+
$listParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1);
127+
128+
$variablePointerInList = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableAnnotation->getVariableName(), $listParenthesisOpener + 1, $tokens[$listParenthesisOpener]['parenthesis_closer']);
129+
if ($variablePointerInList === null) {
130+
continue;
131+
}
132+
133+
$pointerToAddAssertion = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $codePointer + 1);
134+
$indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer);
135+
136+
} elseif ($tokens[$codePointer]['code'] === T_OPEN_SHORT_ARRAY) {
137+
$pointerAfterList = TokenHelper::findNextEffective($phpcsFile, $tokens[$codePointer]['bracket_closer'] + 1);
138+
if ($tokens[$pointerAfterList]['code'] !== T_EQUAL) {
139+
continue;
140+
}
141+
142+
$variablePointerInList = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableAnnotation->getVariableName(), $codePointer + 1, $tokens[$codePointer]['bracket_closer']);
143+
if ($variablePointerInList === null) {
144+
continue;
145+
}
146+
147+
$pointerToAddAssertion = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $tokens[$codePointer]['bracket_closer'] + 1);
148+
$indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer);
149+
150+
} else {
151+
if ($tokens[$codePointer]['code'] === T_WHILE) {
152+
$variablePointerInWhile = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableAnnotation->getVariableName(), $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer']);
153+
if ($variablePointerInWhile === null) {
154+
continue;
155+
}
156+
157+
$pointerAfterVariableInWhile = TokenHelper::findNextEffective($phpcsFile, $variablePointerInWhile + 1);
158+
if ($tokens[$pointerAfterVariableInWhile]['code'] !== T_EQUAL) {
159+
continue;
160+
}
161+
} else {
162+
$asPointer = TokenHelper::findNext($phpcsFile, T_AS, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer']);
163+
$variablePointerInForeach = TokenHelper::findNextContent($phpcsFile, T_VARIABLE, $variableAnnotation->getVariableName(), $asPointer + 1, $tokens[$codePointer]['parenthesis_closer']);
164+
if ($variablePointerInForeach === null) {
165+
continue;
166+
}
167+
}
168+
169+
$pointerToAddAssertion = $tokens[$codePointer]['scope_opener'];
170+
$indentation = IndentationHelper::addIndentation(IndentationHelper::getIndentation($phpcsFile, $codePointer));
171+
}
172+
173+
$fix = $phpcsFile->addFixableError('Use assertion instead of inline documentation comment.', $variableAnnotation->getStartPointer(), self::CODE_REQUIRED_EXPLICIT_ASSERTION);
174+
if (!$fix) {
175+
continue;
176+
}
177+
178+
$phpcsFile->fixer->beginChangeset();
179+
180+
for ($i = $variableAnnotation->getStartPointer(); $i <= $variableAnnotation->getEndPointer(); $i++) {
181+
$phpcsFile->fixer->replaceToken($i, '');
182+
}
183+
184+
$docCommentUseful = false;
185+
$docCommentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer'];
186+
for ($i = $docCommentOpenPointer + 1; $i < $docCommentClosePointer; $i++) {
187+
$tokenContent = trim($phpcsFile->fixer->getTokenContent($i));
188+
if ($tokenContent === '' || $tokenContent === '*') {
189+
continue;
190+
}
191+
192+
$docCommentUseful = true;
193+
break;
194+
}
195+
196+
$pointerBeforeDocComment = TokenHelper::findPreviousContent($phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $docCommentOpenPointer - 1);
197+
$pointerAfterDocComment = TokenHelper::findNextContent($phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $docCommentClosePointer + 1);
198+
199+
if (!$docCommentUseful) {
200+
for ($i = $pointerBeforeDocComment + 1; $i <= $pointerAfterDocComment; $i++) {
201+
$phpcsFile->fixer->replaceToken($i, '');
202+
}
203+
}
204+
205+
/** @var IdentifierTypeNode|ThisTypeNode|UnionTypeNode $variableAnnotationType */
206+
$variableAnnotationType = $variableAnnotationType;
207+
208+
$assertion = $this->createAssert($variableAnnotation->getVariableName(), $variableAnnotationType);
209+
210+
if ($pointerToAddAssertion < $docCommentClosePointer && array_key_exists($pointerAfterDocComment + 1, $tokens)) {
211+
$phpcsFile->fixer->addContentBefore(
212+
$pointerAfterDocComment + 1,
213+
$indentation . $assertion . $phpcsFile->eolChar
214+
);
215+
} else {
216+
$phpcsFile->fixer->addContent(
217+
$pointerToAddAssertion,
218+
$phpcsFile->eolChar . $indentation . $assertion
219+
);
220+
}
221+
222+
$phpcsFile->fixer->endChangeset();
223+
}
224+
}
225+
226+
private function isValidTypeNode(TypeNode $typeNode): bool
227+
{
228+
if ($typeNode instanceof ThisTypeNode) {
229+
return true;
230+
}
231+
232+
if (!$typeNode instanceof IdentifierTypeNode) {
233+
return false;
234+
}
235+
236+
return !in_array($typeNode->name, ['mixed', 'static'], true);
237+
}
238+
239+
/**
240+
* @param string $variableName
241+
* @param IdentifierTypeNode|ThisTypeNode|UnionTypeNode|IntersectionTypeNode $typeNode
242+
* @return string
243+
*/
244+
private function createAssert(string $variableName, TypeNode $typeNode): string
245+
{
246+
$conditions = [];
247+
248+
if ($typeNode instanceof IdentifierTypeNode || $typeNode instanceof ThisTypeNode) {
249+
$conditions = $this->createConditions($variableName, $typeNode);
250+
} else {
251+
/** @var IdentifierTypeNode|ThisTypeNode $innerTypeNode */
252+
foreach ($typeNode->types as $innerTypeNode) {
253+
$conditions = array_merge($conditions, $this->createConditions($variableName, $innerTypeNode));
254+
}
255+
}
256+
257+
$operator = $typeNode instanceof IntersectionTypeNode ? '&&' : '||';
258+
259+
return sprintf('assert(%s);', implode(sprintf(' %s ', $operator), array_unique($conditions)));
260+
}
261+
262+
/**
263+
* @param string $variableName
264+
* @param IdentifierTypeNode|ThisTypeNode $typeNode
265+
* @return string[]
266+
*/
267+
private function createConditions(string $variableName, TypeNode $typeNode): array
268+
{
269+
if ($typeNode instanceof ThisTypeNode) {
270+
return [sprintf('%s instanceof $this', $variableName)];
271+
}
272+
273+
if ($typeNode->name === 'self') {
274+
return [sprintf('%s instanceof %s', $variableName, $typeNode->name)];
275+
}
276+
277+
if (TypeHintHelper::isSimpleTypeHint($typeNode->name)) {
278+
return [sprintf('\is_%s(%s)', $typeNode->name, $variableName)];
279+
}
280+
281+
if (in_array($typeNode->name, ['resource', 'object'], true)) {
282+
return [sprintf('\is_%s(%s)', $typeNode->name, $variableName)];
283+
}
284+
285+
if (in_array($typeNode->name, ['true', 'false', 'null'], true)) {
286+
return [sprintf('%s === %s', $variableName, $typeNode->name)];
287+
}
288+
289+
if ($typeNode->name === 'numeric') {
290+
return [
291+
sprintf('\is_int(%s)', $variableName),
292+
sprintf('\is_float(%s)', $variableName),
293+
];
294+
}
295+
296+
if ($typeNode->name === 'scalar') {
297+
return [
298+
sprintf('\is_int(%s)', $variableName),
299+
sprintf('\is_float(%s)', $variableName),
300+
sprintf('\is_bool(%s)', $variableName),
301+
sprintf('\is_string(%s)', $variableName),
302+
];
303+
}
304+
305+
return [sprintf('%s instanceof %s', $variableName, $typeNode->name)];
306+
}
307+
308+
}

0 commit comments

Comments
 (0)