Skip to content

Commit

Permalink
Merge pull request #126 from andrewnicols/typeNormalisation
Browse files Browse the repository at this point in the history
Type normalisation
  • Loading branch information
stronk7 committed Mar 20, 2024
2 parents 24bc48a + 6027bd8 commit 6974134
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 104 deletions.
106 changes: 2 additions & 104 deletions moodle/Sniffs/Commenting/VariableCommentSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
*/
Expand Down
95 changes: 95 additions & 0 deletions moodle/Tests/Util/TypeUtilTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.

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 <andrew@nicols.co.uk>
* @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 = <<<EOF
<?php
/** @var {$type} Type description */
EOF;
$file = new DummyFile($fileContent, new Ruleset($config), $config);
$file->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'],
];
}
}
164 changes: 164 additions & 0 deletions moodle/Util/TypeUtil.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.

namespace MoodleHQ\MoodleCS\moodle\Util;

use PHP_CodeSniffer\Files\File;

/**
* Utility class for handling types.
*
* @copyright Andrew Lyons <andrew@nicols.co.uk>
* @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);
}
}

0 comments on commit 6974134

Please sign in to comment.