From d5911f812b69ca3bda5524899bdd06b3b4e687ff Mon Sep 17 00:00:00 2001
From: Klaus Purer <klaus.purer@protonmail.ch>
Date: Tue, 18 Apr 2023 14:07:59 +0200
Subject: [PATCH] feat(FunctionComment): Allow PHPStan advanced string types
 (#3205017)

---
 .../Commenting/FunctionCommentSniff.php       | 163 ------------------
 .../Commenting/FunctionCommentUnitTest.inc    |  83 ++++++++-
 .../FunctionCommentUnitTest.inc.fixed         |  83 ++++++++-
 .../Commenting/FunctionCommentUnitTest.php    |   1 -
 4 files changed, 164 insertions(+), 166 deletions(-)

diff --git a/coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php b/coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php
index ffa6a88f..9affdc9b 100644
--- a/coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php
+++ b/coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php
@@ -49,36 +49,6 @@ class FunctionCommentSniff implements Sniff
         'TRUEFALSE' => 'bool',
     ];
 
-    /**
-     * An array of variable types for param/var we will check.
-     *
-     * @var array<string>
-     */
-    public $allowedTypes = [
-        'array',
-        'array-key',
-        'bool',
-        'callable',
-        'double',
-        'float',
-        'int',
-        'positive-int',
-        'negative-int',
-        'iterable',
-        'mixed',
-        'object',
-        'resource',
-        'callable',
-        'true',
-        'false',
-        'null',
-        'scalar',
-        'stdClass',
-        '\stdClass',
-        'string',
-        'void',
-    ];
-
 
     /**
      * Returns an array of tokens this test wants to listen for.
@@ -756,72 +726,6 @@ class FunctionCommentSniff implements Sniff
                 }
             }//end if
 
-            $suggestedName = '';
-            $typeName      = '';
-            if (count($typeNames) === 1) {
-                $typeName      = $param['type'];
-                $suggestedName = static::suggestType($typeName);
-            }
-
-            // This runs only if there is only one type name and the type name
-            // is not one of the disallowed type names.
-            if (count($typeNames) === 1 && $typeName === $suggestedName) {
-                // Check type hint for array and custom type.
-                $suggestedTypeHint = '';
-                if (strpos($suggestedName, 'array') !== false && $suggestedName !== 'array-key') {
-                    $suggestedTypeHint = 'array';
-                } else if (strpos($suggestedName, 'callable') !== false) {
-                    $suggestedTypeHint = 'callable';
-                } else if (substr($suggestedName, -2) === '[]') {
-                    $suggestedTypeHint = 'array';
-                } else if ($suggestedName === 'object') {
-                    $suggestedTypeHint = '';
-                } else if (in_array($typeName, $this->allowedTypes) === false) {
-                    $suggestedTypeHint = $suggestedName;
-                }
-
-                if ($suggestedTypeHint !== '' && isset($realParams[$checkPos]) === true) {
-                    $typeHint = $realParams[$checkPos]['type_hint'];
-                    // Primitive type hints are allowed to be omitted.
-                    if ($typeHint === '' && in_array($suggestedTypeHint, $this->allowedTypes) === false) {
-                        $error = 'Type hint "%s" missing for %s';
-                        $data  = [
-                            $suggestedTypeHint,
-                            $param['var'],
-                        ];
-                        $phpcsFile->addError($error, $stackPtr, 'TypeHintMissing', $data);
-                    } else if ($typeHint !== $suggestedTypeHint && $typeHint !== '') {
-                        // The type hint could be fully namespaced, so we check
-                        // for the part after the last "\".
-                        $nameParts = explode('\\', $suggestedTypeHint);
-                        $lastPart  = end($nameParts);
-                        if ($lastPart !== $typeHint && $this->isAliasedType($typeHint, $suggestedTypeHint, $phpcsFile) === false) {
-                            $error = 'Expected type hint "%s"; found "%s" for %s';
-                            $data  = [
-                                $lastPart,
-                                $typeHint,
-                                $param['var'],
-                            ];
-                            $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data);
-                        }
-                    }//end if
-                } else if ($suggestedTypeHint === ''
-                    && isset($realParams[$checkPos]) === true
-                ) {
-                    $typeHint = $realParams[$checkPos]['type_hint'];
-                    if ($typeHint !== ''
-                        && in_array($typeHint, $this->allowedTypes) === false
-                    ) {
-                        $error = 'Unknown type hint "%s" found for %s';
-                        $data  = [
-                            $typeHint,
-                            $param['var'],
-                        ];
-                        $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data);
-                    }
-                }//end if
-            }//end if
-
             // Check number of spaces after the type.
             $spaces = 1;
             if ($param['type_space'] !== $spaces) {
@@ -1025,73 +929,6 @@ class FunctionCommentSniff implements Sniff
     }//end suggestType()
 
 
-    /**
-     * Checks if a used type hint is an alias defined by a "use" statement.
-     *
-     * @param string                      $typeHint          The type hint used.
-     * @param string                      $suggestedTypeHint The fully qualified type to
-     *                                                       check against.
-     * @param \PHP_CodeSniffer\Files\File $phpcsFile         The file being checked.
-     *
-     * @return boolean
-     */
-    protected function isAliasedType($typeHint, $suggestedTypeHint, File $phpcsFile)
-    {
-        $tokens = $phpcsFile->getTokens();
-
-        // Iterate over all "use" statements in the file.
-        $usePtr = 0;
-        while ($usePtr !== false) {
-            $usePtr = $phpcsFile->findNext(T_USE, ($usePtr + 1));
-            if ($usePtr === false) {
-                return false;
-            }
-
-            // Only check use statements in the global scope.
-            if (empty($tokens[$usePtr]['conditions']) === false) {
-                continue;
-            }
-
-            // Now comes the original class name, possibly with namespace
-            // backslashes.
-            $originalClass = $phpcsFile->findNext(Tokens::$emptyTokens, ($usePtr + 1), null, true);
-            if ($originalClass === false || ($tokens[$originalClass]['code'] !== T_STRING
-                && $tokens[$originalClass]['code'] !== T_NS_SEPARATOR)
-            ) {
-                continue;
-            }
-
-            $originalClassName = '';
-            while (in_array($tokens[$originalClass]['code'], [T_STRING, T_NS_SEPARATOR]) === true) {
-                $originalClassName .= $tokens[$originalClass]['content'];
-                $originalClass++;
-            }
-
-            if (ltrim($originalClassName, '\\') !== ltrim($suggestedTypeHint, '\\')) {
-                continue;
-            }
-
-            // Now comes the "as" keyword signaling an alias name for the class.
-            $asPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($originalClass + 1), null, true);
-            if ($asPtr === false || $tokens[$asPtr]['code'] !== T_AS) {
-                continue;
-            }
-
-            // Now comes the name the class is aliased to.
-            $aliasPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($asPtr + 1), null, true);
-            if ($aliasPtr === false || $tokens[$aliasPtr]['code'] !== T_STRING
-                || $tokens[$aliasPtr]['content'] !== $typeHint
-            ) {
-                continue;
-            }
-
-            // We found a use statement that aliases the used type hint!
-            return true;
-        }//end while
-
-    }//end isAliasedType()
-
-
     /**
      * Determines if a comment line is part of an @code/@endcode example.
      *
diff --git a/tests/Drupal/Commenting/FunctionCommentUnitTest.inc b/tests/Drupal/Commenting/FunctionCommentUnitTest.inc
index 3174a394..b2a397a5 100644
--- a/tests/Drupal/Commenting/FunctionCommentUnitTest.inc
+++ b/tests/Drupal/Commenting/FunctionCommentUnitTest.inc
@@ -172,7 +172,7 @@ function test14(stdClass $user) {
 }
 
 /**
- * Array parameter type mismatch.
+ * Array parameter type mismatch is allowed, use PHPStan to validate types.
  *
  * @param array $foo
  *   Comment here.
@@ -822,3 +822,84 @@ function test_return_integer_min(): int {
 function test_return_integer_max(): int {
   return 50;
 }
+
+/**
+ * PHPStan: Advanced string types.
+ *
+ * @param class-string $param1
+ *   Parameter.
+ * @param class-string<Foo> $param2
+ *   Parameter.
+ * @param callable-string $param3
+ *   Parameter.
+ * @param numeric-string $param4
+ *   Parameter.
+ * @param non-empty-string $param5
+ *   Parameter.
+ * @param non-falsy-string $param6
+ *   Parameter.
+ * @param literal-string $param7
+ *   Parameter.
+ * @param numeric-string|null $param8
+ *   Parameter.
+ *
+ * @see https://phpstan.org/writing-php-code/phpdoc-types#other-advanced-string-types
+ */
+function test_string_types(string $param1, string $param2, string $param3, string $param4, string $param5, string $param6, string $param7, $param8) {
+}
+
+/**
+ * @return class-string
+ *   Class string.
+ */
+function test_return_class_string(): string {
+  return '';
+}
+
+/**
+ * @return class-string<Foo>
+ *   Class string.
+ */
+function test_return_class_string_foo(): string {
+  return '';
+}
+
+/**
+ * @return callable-string
+ *   Callable string.
+ */
+function test_return_callable_string(): string {
+  return '';
+}
+
+/**
+ * @return numeric-string
+ *   Numeric string.
+ */
+function test_return_numeric_string(): string {
+  return '';
+}
+
+/**
+ * @return non-empty-string
+ *   Non empty string.
+ */
+function test_return_non_empty_string(): string {
+  return 'foo';
+}
+
+/**
+ * @return non-falsy-string
+ *   Non falsy string.
+ */
+function test_return_non_falsy_string(): string {
+  return '';
+}
+
+/**
+ * @return literal-string
+ *   Literal string.
+ */
+function test_return_literal_string(): string {
+  return '';
+}
diff --git a/tests/Drupal/Commenting/FunctionCommentUnitTest.inc.fixed b/tests/Drupal/Commenting/FunctionCommentUnitTest.inc.fixed
index 34f2340d..363885a7 100644
--- a/tests/Drupal/Commenting/FunctionCommentUnitTest.inc.fixed
+++ b/tests/Drupal/Commenting/FunctionCommentUnitTest.inc.fixed
@@ -183,7 +183,7 @@ function test14(stdClass $user) {
 }
 
 /**
- * Array parameter type mismatch.
+ * Array parameter type mismatch is allowed, use PHPStan to validate types.
  *
  * @param array $foo
  *   Comment here.
@@ -848,3 +848,84 @@ function test_return_integer_min(): int {
 function test_return_integer_max(): int {
   return 50;
 }
+
+/**
+ * PHPStan: Advanced string types.
+ *
+ * @param class-string $param1
+ *   Parameter.
+ * @param class-string<Foo> $param2
+ *   Parameter.
+ * @param callable-string $param3
+ *   Parameter.
+ * @param numeric-string $param4
+ *   Parameter.
+ * @param non-empty-string $param5
+ *   Parameter.
+ * @param non-falsy-string $param6
+ *   Parameter.
+ * @param literal-string $param7
+ *   Parameter.
+ * @param numeric-string|null $param8
+ *   Parameter.
+ *
+ * @see https://phpstan.org/writing-php-code/phpdoc-types#other-advanced-string-types
+ */
+function test_string_types(string $param1, string $param2, string $param3, string $param4, string $param5, string $param6, string $param7, $param8) {
+}
+
+/**
+ * @return class-string
+ *   Class string.
+ */
+function test_return_class_string(): string {
+  return '';
+}
+
+/**
+ * @return class-string<Foo>
+ *   Class string.
+ */
+function test_return_class_string_foo(): string {
+  return '';
+}
+
+/**
+ * @return callable-string
+ *   Callable string.
+ */
+function test_return_callable_string(): string {
+  return '';
+}
+
+/**
+ * @return numeric-string
+ *   Numeric string.
+ */
+function test_return_numeric_string(): string {
+  return '';
+}
+
+/**
+ * @return non-empty-string
+ *   Non empty string.
+ */
+function test_return_non_empty_string(): string {
+  return 'foo';
+}
+
+/**
+ * @return non-falsy-string
+ *   Non falsy string.
+ */
+function test_return_non_falsy_string(): string {
+  return '';
+}
+
+/**
+ * @return literal-string
+ *   Literal string.
+ */
+function test_return_literal_string(): string {
+  return '';
+}
diff --git a/tests/Drupal/Commenting/FunctionCommentUnitTest.php b/tests/Drupal/Commenting/FunctionCommentUnitTest.php
index d1094e20..462b7d43 100644
--- a/tests/Drupal/Commenting/FunctionCommentUnitTest.php
+++ b/tests/Drupal/Commenting/FunctionCommentUnitTest.php
@@ -38,7 +38,6 @@ class FunctionCommentUnitTest extends CoderSniffUnitTest
                 126 => 2,
                 147 => 1,
                 148 => 2,
-                180 => 1,
                 187 => 1,
                 195 => 1,
                 205 => 1,
-- 
GitLab