Skip to content

Commit eab784d

Browse files
committed
Fix #1768 and #1771
* correctly handle all current and future string types * correctly handle additional int types * fix non-empty-array not correctly set as native type but set with hyphens * fix numeric also contains string (numeric-string specifically)
1 parent b4f9f02 commit eab784d

17 files changed

+237
-43
lines changed

SlevomatCodingStandard/Helpers/AnnotationHelper.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,14 @@ public static function isAnnotationUseless(
260260
);
261261
}
262262

263+
if ($annotationType instanceof UnionTypeNode) {
264+
return false;
265+
}
266+
267+
if ($annotationType instanceof IntersectionTypeNode) {
268+
return false;
269+
}
270+
263271
if ($annotationType instanceof ObjectShapeNode) {
264272
return false;
265273
}
@@ -293,11 +301,10 @@ public static function isAnnotationUseless(
293301
return $enableStandaloneNullTrueFalseTypeHints;
294302
}
295303

296-
if (in_array(
304+
if (TypeHintHelper::isSimpleUnofficialTypeHints(
297305
strtolower($annotationType->name),
298-
['class-string', 'trait-string', 'callable-string', 'numeric-string', 'non-empty-string', 'non-falsy-string', 'literal-string', 'positive-int', 'negative-int'],
299-
true,
300-
)) {
306+
) && !in_array($annotationType->name, ['object', 'mixed'], true)
307+
) {
301308
return false;
302309
}
303310
}

SlevomatCodingStandard/Helpers/AnnotationTypeHelper.php

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace SlevomatCodingStandard\Helpers;
44

5+
use InvalidArgumentException;
56
use PHP_CodeSniffer\Files\File;
67
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode;
78
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
@@ -21,7 +22,10 @@
2122
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
2223
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
2324
use function count;
25+
use function get_class;
2426
use function in_array;
27+
use function preg_match;
28+
use function sprintf;
2529
use function strtolower;
2630

2731
/**
@@ -291,7 +295,16 @@ public static function getTypeHintFromOneType(
291295
return 'array';
292296
}
293297

294-
return $genericName;
298+
if ((bool) preg_match(
299+
'/^\\\\?[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(?:\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/',
300+
$genericName,
301+
)) {
302+
return $genericName;
303+
}
304+
305+
throw new InvalidArgumentException(
306+
sprintf('Invalid type "%1$s" of class %2$s given', $genericName, get_class($typeNode)),
307+
);
295308
}
296309

297310
if ($typeNode instanceof IdentifierTypeNode) {
@@ -303,19 +316,61 @@ public static function getTypeHintFromOneType(
303316
return $enableUnionTypeHint || $enableStandaloneNullTrueFalseTypeHints ? 'false' : 'bool';
304317
}
305318

306-
if (in_array(strtolower($typeNode->name), ['positive-int', 'negative-int'], true)) {
319+
if (in_array(
320+
strtolower($typeNode->name),
321+
['positive-int', 'non-positive-int', 'negative-int', 'non-negative-int', 'literal-int', 'int-mask'],
322+
true,
323+
)) {
307324
return 'int';
308325
}
309326

310327
if (in_array(
311328
strtolower($typeNode->name),
312-
['class-string', 'trait-string', 'callable-string', 'numeric-string', 'non-empty-string', 'non-falsy-string', 'literal-string'],
329+
['callable-array', 'callable-string'],
313330
true,
314331
)) {
332+
return 'callable';
333+
}
334+
335+
// see https://psalm.dev/docs/annotating_code/type_syntax/scalar_types/#class-string-interface-string
336+
if ((bool) preg_match('/-string$/i', $typeNode->name)) {
315337
return 'string';
316338
}
317339

318-
return $typeNode->name;
340+
// here when used literally e.g. as type non-empty-array|null
341+
if (in_array(
342+
strtolower($typeNode->name),
343+
['non-empty-array', 'list', 'non-empty-list'],
344+
true,
345+
)) {
346+
return 'array';
347+
}
348+
349+
if (strtolower($typeNode->name) === 'array-key') {
350+
return $enableUnionTypeHint ? 'int|string' : 'mixed';
351+
}
352+
353+
if (TypeHintHelper::isNeverTypeHint(strtolower($typeNode->name))) {
354+
// don't set a type hint at all if it's not supported by the PHP version yet
355+
return $enableStandaloneNullTrueFalseTypeHints ? 'never' : 'never-returns';
356+
}
357+
358+
if (strtolower($typeNode->name) === 'static') {
359+
// don't set a type hint at all (instead of "self") if it's not supported by the PHP version yet
360+
// use $this to fake it
361+
return $enableUnionTypeHint ? 'static' : '$this';
362+
}
363+
364+
if ((bool) preg_match(
365+
'/^\\\\?[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(?:\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/',
366+
$typeNode->name,
367+
)) {
368+
return $typeNode->name;
369+
}
370+
371+
throw new InvalidArgumentException(
372+
sprintf('Invalid type "%1$s" of class %2$s given', $typeNode->name, get_class($typeNode)),
373+
);
319374
}
320375

321376
if ($typeNode instanceof CallableTypeNode) {
@@ -334,6 +389,11 @@ public static function getTypeHintFromOneType(
334389
return 'object';
335390
}
336391

392+
// $this and self are not strictly equal
393+
if ($typeNode instanceof ThisTypeNode) {
394+
return (string) $typeNode;
395+
}
396+
337397
if ($typeNode instanceof ConstTypeNode) {
338398
if ($typeNode->constExpr instanceof ConstExprIntegerNode) {
339399
return 'int';
@@ -348,7 +408,16 @@ public static function getTypeHintFromOneType(
348408
}
349409
}
350410

351-
return (string) $typeNode;
411+
if ((bool) preg_match(
412+
'/^\\\\?[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(?:\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/',
413+
(string) $typeNode,
414+
)) {
415+
return (string) $typeNode;
416+
}
417+
418+
throw new InvalidArgumentException(
419+
sprintf('Invalid type "%1$s" of class %2$s given', (string) $typeNode, get_class($typeNode)),
420+
);
352421
}
353422

354423
/**

SlevomatCodingStandard/Helpers/TypeHintHelper.php

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use function count;
1414
use function implode;
1515
use function in_array;
16+
use function preg_match;
1617
use function preg_split;
1718
use function sort;
1819
use function sprintf;
@@ -79,7 +80,7 @@ public static function convertLongSimpleTypeHintToShort(string $typeHint): strin
7980

8081
public static function isUnofficialUnionTypeHint(string $typeHint): bool
8182
{
82-
return in_array($typeHint, ['scalar', 'numeric'], true);
83+
return in_array($typeHint, ['scalar', 'numeric', 'array-key'], true);
8384
}
8485

8586
public static function isVoidTypeHint(string $typeHint): bool
@@ -99,7 +100,8 @@ public static function convertUnofficialUnionTypeHintToOfficialTypeHints(string
99100
{
100101
$conversion = [
101102
'scalar' => ['string', 'int', 'float', 'bool'],
102-
'numeric' => ['int', 'float'],
103+
'numeric' => ['int', 'float', 'string'],
104+
'array-key' => ['int', 'string'],
103105
];
104106

105107
return $conversion[$typeHint];
@@ -170,6 +172,7 @@ public static function isSimpleUnofficialTypeHints(string $typeHint): bool
170172
static $simpleUnofficialTypeHints;
171173

172174
if ($simpleUnofficialTypeHints === null) {
175+
// see https://psalm.dev/docs/annotating_code/type_syntax/atomic_types/
173176
$simpleUnofficialTypeHints = [
174177
'null',
175178
'mixed',
@@ -180,24 +183,25 @@ public static function isSimpleUnofficialTypeHints(string $typeHint): bool
180183
'resource',
181184
'static',
182185
'$this',
183-
'class-string',
184-
'trait-string',
185-
'callable-string',
186-
'numeric-string',
187-
'non-empty-string',
188-
'non-falsy-string',
189-
'literal-string',
190186
'array-key',
191187
'list',
188+
'non-empty-array',
189+
'non-empty-list',
192190
'empty',
193191
'positive-int',
192+
'non-positive-int',
194193
'negative-int',
194+
'non-negative-int',
195+
'literal-int',
196+
'int-mask',
195197
'min',
196198
'max',
199+
'callable-array',
200+
'callable-string',
197201
];
198202
}
199203

200-
return in_array($typeHint, $simpleUnofficialTypeHints, true);
204+
return in_array($typeHint, $simpleUnofficialTypeHints, true) || (bool) preg_match('/-string$/', $typeHint);
201205
}
202206

203207
/**

SlevomatCodingStandard/Sniffs/PHP/RequireExplicitAssertionSniff.php

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use function count;
2828
use function implode;
2929
use function in_array;
30+
use function preg_match;
3031
use function sprintf;
3132
use function strpos;
3233
use function trim;
@@ -417,8 +418,7 @@ private function createConditions(string $variableName, TypeNode $typeNode): arr
417418

418419
if ($typeNode->name === 'numeric') {
419420
return [
420-
sprintf('\is_int(%s)', $variableName),
421-
sprintf('\is_float(%s)', $variableName),
421+
sprintf('\is_numeric(%s)', $variableName),
422422
];
423423
}
424424

@@ -432,29 +432,33 @@ private function createConditions(string $variableName, TypeNode $typeNode): arr
432432
}
433433

434434
if ($this->enableIntegerRanges) {
435-
if ($typeNode->name === 'positive-int') {
435+
if ($typeNode->name === 'positive-int' || $typeNode->name === 'non-negative-int') {
436436
return [sprintf('\is_int(%1$s) && %1$s > 0', $variableName)];
437437
}
438438

439-
if ($typeNode->name === 'negative-int') {
439+
if ($typeNode->name === 'negative-int' || $typeNode->name === 'non-positive-int') {
440440
return [sprintf('\is_int(%1$s) && %1$s < 0', $variableName)];
441441
}
442+
443+
if ($typeNode->name === 'literal-int') {
444+
return [sprintf('\is_int(%1$s)', $variableName)];
445+
}
442446
}
443447

444448
if (
445449
$this->enableAdvancedStringTypes
446-
&& in_array($typeNode->name, ['non-empty-string', 'non-falsy-string', 'callable-string', 'numeric-string'], true)
450+
&& (bool) preg_match('/-string$/i', $typeNode->name)
447451
) {
448452
$conditions = [sprintf('\is_string(%s)', $variableName)];
449453

450-
if ($typeNode->name === 'non-empty-string') {
451-
$conditions[] = sprintf("%s !== ''", $variableName);
452-
} elseif ($typeNode->name === 'non-falsy-string') {
453-
$conditions[] = sprintf('(bool) %s === true', $variableName);
454-
} elseif ($typeNode->name === 'callable-string') {
454+
if ($typeNode->name === 'callable-string') {
455455
$conditions[] = sprintf('\is_callable(%s)', $variableName);
456-
} else {
456+
} elseif ($typeNode->name === 'numeric-string') {
457457
$conditions[] = sprintf('\is_numeric(%s)', $variableName);
458+
} elseif ((bool) preg_match('/^non-empty-/i', $typeNode->name)) {
459+
$conditions[] = sprintf("%s !== ''", $variableName);
460+
} elseif ((bool) preg_match('/^non-falsy-/i', $typeNode->name)) {
461+
$conditions[] = sprintf('(bool) %s === true', $variableName);
458462
}
459463

460464
return [implode(' && ', $conditions)];

SlevomatCodingStandard/Sniffs/TestCase.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,35 @@ protected static function checkFile(string $filePath, array $sniffProperties = [
8484
protected static function assertNoSniffErrorInFile(File $phpcsFile): void
8585
{
8686
$errors = $phpcsFile->getErrors();
87-
self::assertEmpty($errors, sprintf('No errors expected, but %d errors found.', count($errors)));
87+
$text = sprintf('No errors expected, but %d errors found:', count($errors));
88+
foreach ($errors as $line => $error) {
89+
$text .= sprintf(
90+
'%sLine %d:%s%s',
91+
PHP_EOL,
92+
$line,
93+
PHP_EOL,
94+
self::getFormattedErrors($error),
95+
);
96+
}
97+
98+
self::assertEmpty($errors, $text);
8899
}
89100

90101
protected static function assertNoSniffWarningInFile(File $phpcsFile): void
91102
{
92103
$warnings = $phpcsFile->getWarnings();
93-
self::assertEmpty($warnings, sprintf('No warnings expected, but %d warnings found.', count($warnings)));
104+
$text = sprintf('No warnings expected, but %d warnings found:', count($warnings));
105+
foreach ($warnings as $line => $warning) {
106+
$text .= sprintf(
107+
'%sLine %d:%s%s',
108+
PHP_EOL,
109+
$line,
110+
PHP_EOL,
111+
self::getFormattedErrors($warning),
112+
);
113+
}
114+
115+
self::assertEmpty($warnings, $text);
94116
}
95117

96118
protected static function assertSniffError(File $phpcsFile, int $line, string $code, ?string $message = null): void

tests/Helpers/TypeHintHelperTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ public static function dataIsSimpleUnofficialTypeHint(): array
8181
['resource', true],
8282
['static', true],
8383
['$this', true],
84+
['array-key', true],
85+
['list', true],
86+
['non-empty-array', true],
87+
['non-empty-list', true],
88+
['empty', true],
89+
['positive-int', true],
90+
['non-positive-int', true],
91+
['negative-int', true],
92+
['non-negative-int', true],
93+
['literal-int', true],
94+
['int-mask', true],
95+
['callable-array', true],
96+
['callable-string', true],
8497

8598
['\Traversable', false],
8699
['int', false],
@@ -379,6 +392,8 @@ public static function dataTypeHintEqualsAnnotation(): array
379392
return [
380393
['scalar', true],
381394
['unionIsNotIntersection', false],
395+
['fooFunctionWithReturnAnnotationComplexString', false],
396+
['fooFunctionWithReturnAnnotationSimpleHyphenedIterable', false],
382397
];
383398
}
384399

tests/Helpers/data/typeHintEqualsAnnotation.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,19 @@ function scalar(): int|bool|float|string
1414
function unionIsNotIntersection(): Foo|Bar
1515
{
1616
}
17+
18+
/**
19+
* @return non-empty-lowercase-string
20+
*/
21+
function fooFunctionWithReturnAnnotationComplexString(): string
22+
{
23+
24+
}
25+
26+
/**
27+
* @return non-empty-array|null
28+
*/
29+
function fooFunctionWithReturnAnnotationSimpleHyphenedIterable(): ?array
30+
{
31+
32+
}

0 commit comments

Comments
 (0)