Skip to content

Commit d5911f8

Browse files
authored
feat(FunctionComment): Allow PHPStan advanced string types (#3205017)
1 parent 0714066 commit d5911f8

File tree

4 files changed

+164
-166
lines changed

4 files changed

+164
-166
lines changed

coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php

Lines changed: 0 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -49,36 +49,6 @@ class FunctionCommentSniff implements Sniff
4949
'TRUEFALSE' => 'bool',
5050
];
5151

52-
/**
53-
* An array of variable types for param/var we will check.
54-
*
55-
* @var array<string>
56-
*/
57-
public $allowedTypes = [
58-
'array',
59-
'array-key',
60-
'bool',
61-
'callable',
62-
'double',
63-
'float',
64-
'int',
65-
'positive-int',
66-
'negative-int',
67-
'iterable',
68-
'mixed',
69-
'object',
70-
'resource',
71-
'callable',
72-
'true',
73-
'false',
74-
'null',
75-
'scalar',
76-
'stdClass',
77-
'\stdClass',
78-
'string',
79-
'void',
80-
];
81-
8252

8353
/**
8454
* Returns an array of tokens this test wants to listen for.
@@ -756,72 +726,6 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
756726
}
757727
}//end if
758728

759-
$suggestedName = '';
760-
$typeName = '';
761-
if (count($typeNames) === 1) {
762-
$typeName = $param['type'];
763-
$suggestedName = static::suggestType($typeName);
764-
}
765-
766-
// This runs only if there is only one type name and the type name
767-
// is not one of the disallowed type names.
768-
if (count($typeNames) === 1 && $typeName === $suggestedName) {
769-
// Check type hint for array and custom type.
770-
$suggestedTypeHint = '';
771-
if (strpos($suggestedName, 'array') !== false && $suggestedName !== 'array-key') {
772-
$suggestedTypeHint = 'array';
773-
} else if (strpos($suggestedName, 'callable') !== false) {
774-
$suggestedTypeHint = 'callable';
775-
} else if (substr($suggestedName, -2) === '[]') {
776-
$suggestedTypeHint = 'array';
777-
} else if ($suggestedName === 'object') {
778-
$suggestedTypeHint = '';
779-
} else if (in_array($typeName, $this->allowedTypes) === false) {
780-
$suggestedTypeHint = $suggestedName;
781-
}
782-
783-
if ($suggestedTypeHint !== '' && isset($realParams[$checkPos]) === true) {
784-
$typeHint = $realParams[$checkPos]['type_hint'];
785-
// Primitive type hints are allowed to be omitted.
786-
if ($typeHint === '' && in_array($suggestedTypeHint, $this->allowedTypes) === false) {
787-
$error = 'Type hint "%s" missing for %s';
788-
$data = [
789-
$suggestedTypeHint,
790-
$param['var'],
791-
];
792-
$phpcsFile->addError($error, $stackPtr, 'TypeHintMissing', $data);
793-
} else if ($typeHint !== $suggestedTypeHint && $typeHint !== '') {
794-
// The type hint could be fully namespaced, so we check
795-
// for the part after the last "\".
796-
$nameParts = explode('\\', $suggestedTypeHint);
797-
$lastPart = end($nameParts);
798-
if ($lastPart !== $typeHint && $this->isAliasedType($typeHint, $suggestedTypeHint, $phpcsFile) === false) {
799-
$error = 'Expected type hint "%s"; found "%s" for %s';
800-
$data = [
801-
$lastPart,
802-
$typeHint,
803-
$param['var'],
804-
];
805-
$phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data);
806-
}
807-
}//end if
808-
} else if ($suggestedTypeHint === ''
809-
&& isset($realParams[$checkPos]) === true
810-
) {
811-
$typeHint = $realParams[$checkPos]['type_hint'];
812-
if ($typeHint !== ''
813-
&& in_array($typeHint, $this->allowedTypes) === false
814-
) {
815-
$error = 'Unknown type hint "%s" found for %s';
816-
$data = [
817-
$typeHint,
818-
$param['var'],
819-
];
820-
$phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data);
821-
}
822-
}//end if
823-
}//end if
824-
825729
// Check number of spaces after the type.
826730
$spaces = 1;
827731
if ($param['type_space'] !== $spaces) {
@@ -1025,73 +929,6 @@ public static function suggestType($type)
1025929
}//end suggestType()
1026930

1027931

1028-
/**
1029-
* Checks if a used type hint is an alias defined by a "use" statement.
1030-
*
1031-
* @param string $typeHint The type hint used.
1032-
* @param string $suggestedTypeHint The fully qualified type to
1033-
* check against.
1034-
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being checked.
1035-
*
1036-
* @return boolean
1037-
*/
1038-
protected function isAliasedType($typeHint, $suggestedTypeHint, File $phpcsFile)
1039-
{
1040-
$tokens = $phpcsFile->getTokens();
1041-
1042-
// Iterate over all "use" statements in the file.
1043-
$usePtr = 0;
1044-
while ($usePtr !== false) {
1045-
$usePtr = $phpcsFile->findNext(T_USE, ($usePtr + 1));
1046-
if ($usePtr === false) {
1047-
return false;
1048-
}
1049-
1050-
// Only check use statements in the global scope.
1051-
if (empty($tokens[$usePtr]['conditions']) === false) {
1052-
continue;
1053-
}
1054-
1055-
// Now comes the original class name, possibly with namespace
1056-
// backslashes.
1057-
$originalClass = $phpcsFile->findNext(Tokens::$emptyTokens, ($usePtr + 1), null, true);
1058-
if ($originalClass === false || ($tokens[$originalClass]['code'] !== T_STRING
1059-
&& $tokens[$originalClass]['code'] !== T_NS_SEPARATOR)
1060-
) {
1061-
continue;
1062-
}
1063-
1064-
$originalClassName = '';
1065-
while (in_array($tokens[$originalClass]['code'], [T_STRING, T_NS_SEPARATOR]) === true) {
1066-
$originalClassName .= $tokens[$originalClass]['content'];
1067-
$originalClass++;
1068-
}
1069-
1070-
if (ltrim($originalClassName, '\\') !== ltrim($suggestedTypeHint, '\\')) {
1071-
continue;
1072-
}
1073-
1074-
// Now comes the "as" keyword signaling an alias name for the class.
1075-
$asPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($originalClass + 1), null, true);
1076-
if ($asPtr === false || $tokens[$asPtr]['code'] !== T_AS) {
1077-
continue;
1078-
}
1079-
1080-
// Now comes the name the class is aliased to.
1081-
$aliasPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($asPtr + 1), null, true);
1082-
if ($aliasPtr === false || $tokens[$aliasPtr]['code'] !== T_STRING
1083-
|| $tokens[$aliasPtr]['content'] !== $typeHint
1084-
) {
1085-
continue;
1086-
}
1087-
1088-
// We found a use statement that aliases the used type hint!
1089-
return true;
1090-
}//end while
1091-
1092-
}//end isAliasedType()
1093-
1094-
1095932
/**
1096933
* Determines if a comment line is part of an @code/@endcode example.
1097934
*

tests/Drupal/Commenting/FunctionCommentUnitTest.inc

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ function test14(stdClass $user) {
172172
}
173173

174174
/**
175-
* Array parameter type mismatch.
175+
* Array parameter type mismatch is allowed, use PHPStan to validate types.
176176
*
177177
* @param array $foo
178178
* Comment here.
@@ -822,3 +822,84 @@ function test_return_integer_min(): int {
822822
function test_return_integer_max(): int {
823823
return 50;
824824
}
825+
826+
/**
827+
* PHPStan: Advanced string types.
828+
*
829+
* @param class-string $param1
830+
* Parameter.
831+
* @param class-string<Foo> $param2
832+
* Parameter.
833+
* @param callable-string $param3
834+
* Parameter.
835+
* @param numeric-string $param4
836+
* Parameter.
837+
* @param non-empty-string $param5
838+
* Parameter.
839+
* @param non-falsy-string $param6
840+
* Parameter.
841+
* @param literal-string $param7
842+
* Parameter.
843+
* @param numeric-string|null $param8
844+
* Parameter.
845+
*
846+
* @see https://phpstan.org/writing-php-code/phpdoc-types#other-advanced-string-types
847+
*/
848+
function test_string_types(string $param1, string $param2, string $param3, string $param4, string $param5, string $param6, string $param7, $param8) {
849+
}
850+
851+
/**
852+
* @return class-string
853+
* Class string.
854+
*/
855+
function test_return_class_string(): string {
856+
return '';
857+
}
858+
859+
/**
860+
* @return class-string<Foo>
861+
* Class string.
862+
*/
863+
function test_return_class_string_foo(): string {
864+
return '';
865+
}
866+
867+
/**
868+
* @return callable-string
869+
* Callable string.
870+
*/
871+
function test_return_callable_string(): string {
872+
return '';
873+
}
874+
875+
/**
876+
* @return numeric-string
877+
* Numeric string.
878+
*/
879+
function test_return_numeric_string(): string {
880+
return '';
881+
}
882+
883+
/**
884+
* @return non-empty-string
885+
* Non empty string.
886+
*/
887+
function test_return_non_empty_string(): string {
888+
return 'foo';
889+
}
890+
891+
/**
892+
* @return non-falsy-string
893+
* Non falsy string.
894+
*/
895+
function test_return_non_falsy_string(): string {
896+
return '';
897+
}
898+
899+
/**
900+
* @return literal-string
901+
* Literal string.
902+
*/
903+
function test_return_literal_string(): string {
904+
return '';
905+
}

tests/Drupal/Commenting/FunctionCommentUnitTest.inc.fixed

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ function test14(stdClass $user) {
183183
}
184184

185185
/**
186-
* Array parameter type mismatch.
186+
* Array parameter type mismatch is allowed, use PHPStan to validate types.
187187
*
188188
* @param array $foo
189189
* Comment here.
@@ -848,3 +848,84 @@ function test_return_integer_min(): int {
848848
function test_return_integer_max(): int {
849849
return 50;
850850
}
851+
852+
/**
853+
* PHPStan: Advanced string types.
854+
*
855+
* @param class-string $param1
856+
* Parameter.
857+
* @param class-string<Foo> $param2
858+
* Parameter.
859+
* @param callable-string $param3
860+
* Parameter.
861+
* @param numeric-string $param4
862+
* Parameter.
863+
* @param non-empty-string $param5
864+
* Parameter.
865+
* @param non-falsy-string $param6
866+
* Parameter.
867+
* @param literal-string $param7
868+
* Parameter.
869+
* @param numeric-string|null $param8
870+
* Parameter.
871+
*
872+
* @see https://phpstan.org/writing-php-code/phpdoc-types#other-advanced-string-types
873+
*/
874+
function test_string_types(string $param1, string $param2, string $param3, string $param4, string $param5, string $param6, string $param7, $param8) {
875+
}
876+
877+
/**
878+
* @return class-string
879+
* Class string.
880+
*/
881+
function test_return_class_string(): string {
882+
return '';
883+
}
884+
885+
/**
886+
* @return class-string<Foo>
887+
* Class string.
888+
*/
889+
function test_return_class_string_foo(): string {
890+
return '';
891+
}
892+
893+
/**
894+
* @return callable-string
895+
* Callable string.
896+
*/
897+
function test_return_callable_string(): string {
898+
return '';
899+
}
900+
901+
/**
902+
* @return numeric-string
903+
* Numeric string.
904+
*/
905+
function test_return_numeric_string(): string {
906+
return '';
907+
}
908+
909+
/**
910+
* @return non-empty-string
911+
* Non empty string.
912+
*/
913+
function test_return_non_empty_string(): string {
914+
return 'foo';
915+
}
916+
917+
/**
918+
* @return non-falsy-string
919+
* Non falsy string.
920+
*/
921+
function test_return_non_falsy_string(): string {
922+
return '';
923+
}
924+
925+
/**
926+
* @return literal-string
927+
* Literal string.
928+
*/
929+
function test_return_literal_string(): string {
930+
return '';
931+
}

tests/Drupal/Commenting/FunctionCommentUnitTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ protected function getErrorList(string $testFile): array
3838
126 => 2,
3939
147 => 1,
4040
148 => 2,
41-
180 => 1,
4241
187 => 1,
4342
195 => 1,
4443
205 => 1,

0 commit comments

Comments
 (0)