diff --git a/moodle/Sniffs/Commenting/VariableCommentSniff.php b/moodle/Sniffs/Commenting/VariableCommentSniff.php index 3a1e2bd..4f91743 100644 --- a/moodle/Sniffs/Commenting/VariableCommentSniff.php +++ b/moodle/Sniffs/Commenting/VariableCommentSniff.php @@ -17,6 +17,7 @@ namespace MoodleHQ\MoodleCS\moodle\Sniffs\Commenting; +use MoodleHQ\MoodleCS\moodle\Util\TypeUtil; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\AbstractVariableSniff; use PHPCSUtils\Tokens\Collections; @@ -34,23 +35,6 @@ */ class VariableCommentSniff extends AbstractVariableSniff { - /** - * An array of variable types for param/var we will check. - * - * @var string[] - */ - protected static $allowedTypes = [ - 'array', - 'bool', - 'float', - 'int', - 'mixed', - 'object', - 'string', - 'resource', - 'callable', - ]; - /** * Called to process class member vars. * @@ -150,17 +134,7 @@ public function processMemberVar(File $phpcsFile, $stackPtr) { preg_match('`^((?:\|?(?:array\([^\)]*\)|[\\\\a-z0-9\[\]]+))*)( .*)?`i', $tokens[($foundVar + 2)]['content'], $varParts); $varType = $varParts[1]; - // Check var type (can be multiple, separated by '|'). - $typeNames = explode('|', $varType); - $suggestedNames = []; - foreach ($typeNames as $i => $typeName) { - $suggestedName = self::suggestType($typeName); - if (in_array($suggestedName, $suggestedNames, true) === false) { - $suggestedNames[] = $suggestedName; - } - } - - $suggestedType = implode('|', $suggestedNames); + $suggestedType = TypeUtil::getValidatedType($phpcsFile, $string, $varType); if ($varType !== $suggestedType) { $error = 'Expected "%s" but found "%s" for @var tag in member variable comment'; $data = [ @@ -210,82 +184,6 @@ protected function processVariable(File $phpcsFile, $stackPtr) { } } - /** - * Returns a valid variable type for param/var tags. - * - * If type is not one of the standard types, it must be a custom type. - * Returns the correct type name suggestion if type name is invalid. - * - * @param string $varType The variable type to process. - * - * @return string - */ - protected static function suggestType(string $varType): string { - if (in_array($varType, self::$allowedTypes, true) === true) { - return $varType; - } elseif (substr($varType, -2) === '[]') { - return sprintf( - '%s[]', - self::suggestType(substr($varType, 0, -2)) - ); - } else { - $lowerVarType = strtolower($varType); - switch ($lowerVarType) { - case 'bool': - case 'boolean': - return 'bool'; - case 'double': - case 'real': - case 'float': - return 'float'; - case 'int': - case 'integer': - return 'int'; - case 'array()': - case 'array': - return 'array'; - } - - if (strpos($lowerVarType, 'array(') !== false) { - // Valid array declaration: - // array, array(type), array(type1 => type2). - $matches = []; - $pattern = '/^array\(\s*([^\s^=^>]*)(\s*=>\s*(.*))?\s*\)/i'; - if (preg_match($pattern, $varType, $matches) !== 0) { - $type1 = ''; - if (isset($matches[1]) === true) { - $type1 = $matches[1]; - } - - $type2 = ''; - if (isset($matches[3]) === true) { - $type2 = $matches[3]; - } - - $type1 = self::suggestType($type1); - $type2 = self::suggestType($type2); - - // Note: The phpdoc array syntax only allows you to describe the array value type. - // https://docs.phpdoc.org/latest/guide/guides/types.html#arrays - if ($type1 && !$type2) { - // This is an array of [type2, type2, type2]. - return "{$type1}[]"; - } - // This is an array of [type1 => type2, type1 => type2, type1 => type2]. - return "{$type2}[]"; - } else { - return 'array'; - } - } elseif (in_array($lowerVarType, self::$allowedTypes, true) === true) { - // A valid type, but not lower cased. - return $lowerVarType; - } else { - // Must be a custom type name. - return $varType; - } - } - } - /** * @codeCoverageIgnore */ diff --git a/moodle/Tests/Util/TypeUtilTest.php b/moodle/Tests/Util/TypeUtilTest.php new file mode 100644 index 0000000..a543188 --- /dev/null +++ b/moodle/Tests/Util/TypeUtilTest.php @@ -0,0 +1,95 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests\Util; + +use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase; +use MoodleHQ\MoodleCS\moodle\Util\TypeUtil; +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Files\DummyFile; +use PHP_CodeSniffer\Ruleset; + +/** + * Test the Tokens specific utilities class + * + * + * @copyright Andrew Lyons + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \MoodleHQ\MoodleCS\moodle\Util\TypeUtil + */ +final class TypeUtilTest extends MoodleCSBaseTestCase +{ + /** + * @dataProvider getValidTypesProvider + */ + public function testGetValidTypes(string $type, string $expected): void { + $config = new Config(); + $fileContent = <<process(); + $ptr = $file->findNext(T_DOC_COMMENT_STRING, 0); + + $this->assertEquals( + $expected, + TypeUtil::getValidatedType($file, $ptr, $type), + ); + } + + public static function getValidTypesProvider(): array { + return [ + ['string', 'string'], + ['int', 'int'], + ['integer', 'int'], + ['float', 'float'], + ['real', 'float'], + ['double', 'float'], + ['array', 'array'], + ['array()', 'array'], + ['ARRAY()', 'array'], + ['INT', 'int'], + ['Boolean', 'bool'], + ['NULL', 'null'], + ['FALSE', 'false'], + ['true', 'true'], + + // Various array syntaxes. + ['string[]', 'string[]'], + ['array(int => string)', 'string[]'], + ['array(int)', 'int[]'], + ['array(int > string)', 'array'], + + // Union types. + ['string|int', 'string|int'], + ['string|integer', 'string|int'], + ['real|integer', 'float|int'], + + // Some example Moodle classes. + [\core\formatting::class, \core\formatting::class], + [\core\output\notification::class, \core\output\notification::class], + [\core_renderer::class, \core_renderer::class], + + // Standard types. + ['Traversable', 'Traversable'], + [\ArrayAccess::class, \ArrayAccess::class], + ['DateTimeImmutable', 'DateTimeImmutable'], + ]; + } +} diff --git a/moodle/Util/TypeUtil.php b/moodle/Util/TypeUtil.php new file mode 100644 index 0000000..f0a602f --- /dev/null +++ b/moodle/Util/TypeUtil.php @@ -0,0 +1,164 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Util; + +use PHP_CodeSniffer\Files\File; + +/** + * Utility class for handling types. + * + * @copyright Andrew Lyons + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class TypeUtil +{ + /** + * An array of variable types for param/var we will check. + * + * @var string[] + */ + protected static array $allowedTypes = [ + 'array', + 'bool', + 'false', + 'float', + 'int', + 'mixed', + 'null', + 'object', + 'string', + 'true', + 'resource', + 'callable', + ]; + + /** + * Standardise a type to a known type. + * + * @param string $type The type to standardise. + * @return string|null + */ + public static function standardiseType(string $type): ?string { + $type = strtolower($type); + if (in_array($type, self::$allowedTypes, true)) { + return $type; + } + + switch ($type) { + case 'array()': + return 'array'; + case 'boolean': + return 'bool'; + case 'double': + case 'real': + return 'float'; + case 'integer': + return 'int'; + default: + return null; + } + } + + + /** + * Returns a valid variable type for param/var tags. + * + * If type is not one of the standard types, it must be a custom type. + * Returns the correct type name suggestion if type name is invalid. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param string $varType The variable type to process. + * @return string + */ + public static function suggestType( + File $phpcsFile, + int $stackPtr, + string $varType + ): string { + $lowerVarType = strtolower($varType); + if ($normalisedType = self::standardiseType($lowerVarType)) { + return $normalisedType; + } + if (substr($varType, -2) === '[]') { + return sprintf( + '%s[]', + self::suggestType($phpcsFile, $stackPtr, substr($varType, 0, -2)) + ); + } + + if (strpos($lowerVarType, 'array(') !== false) { + // Valid array declaration: + // array, array(type), array(type1 => type2). + $matches = []; + $pattern = '/^array\(\s*([^\s^=^>]*)(\s*=>\s*(.*))?\s*\)/i'; + if (preg_match($pattern, $varType, $matches) !== 0) { + $type1 = ''; + if (isset($matches[1]) === true) { + $type1 = $matches[1]; + } + + $type2 = ''; + if (isset($matches[3]) === true) { + $type2 = $matches[3]; + } + + $type1 = self::suggestType($phpcsFile, $stackPtr, $type1); + $type2 = self::suggestType($phpcsFile, $stackPtr, $type2); + + // Note: The phpdoc array syntax only allows you to describe the array value type. + // https://docs.phpdoc.org/latest/guide/guides/types.html#arrays + if ($type1 && !$type2) { + // This is an array of [type2, type2, type2]. + return "{$type1}[]"; + } + // This is an array of [type1 => type2, type1 => type2, type1 => type2]. + return "{$type2}[]"; + } else { + return 'array'; + } + } + + // Must be a custom type name. + return $varType; + } + + /** + * Validate a type in its entirety. + * + * The method currently supports built-in types, and Union types. + * It does not currently support DNF, or other complex types. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param string $type The type to validate. + * @return string The validated type. + */ + public static function getValidatedType( + File $phpcsFile, + int $stackPtr, + string $type + ): string { + $types = explode('|', $type); + $validatedTypes = []; + foreach ($types as $type) { + $validatedTypes[] = self::suggestType($phpcsFile, $stackPtr, $type); + } + return implode('|', $validatedTypes); + } +}