From 01bbd7f9387c0ffab2373745c0bc192232ddc48f Mon Sep 17 00:00:00 2001 From: Maximilian Haye Date: Wed, 23 Oct 2024 17:47:09 +0200 Subject: [PATCH 1/2] feat: support class-string and similar PHPdoc types --- rules/phpdocs_basic.php | 72 ++++++++++++++++++-------- tests/fixtures/phpdoc_tags_general.php | 9 ++++ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/rules/phpdocs_basic.php b/rules/phpdocs_basic.php index 1cceae9..777c31d 100644 --- a/rules/phpdocs_basic.php +++ b/rules/phpdocs_basic.php @@ -157,34 +157,17 @@ function local_moodlecheck_functionarguments(local_moodlecheck_file $file) { // Must be at least type and parameter name. $match = false; } else { - $expectedtype = local_moodlecheck_normalise_function_type((string) $function->arguments[$i][0]); + $expectedtype = local_moodlecheck_normalise_function_type((string)$function->arguments[$i][0]); $expectedparam = (string)$function->arguments[$i][1]; - $documentedtype = local_moodlecheck_normalise_function_type((string) $documentedarguments[$i][0]); + $documentedtype = local_moodlecheck_normalise_function_type((string)$documentedarguments[$i][0]); $documentedparam = $documentedarguments[$i][1]; - $typematch = $expectedtype === $documentedtype; - $parammatch = $expectedparam === $documentedparam; - if ($typematch && $parammatch) { - continue; + if ($expectedparam !== $documentedparam) { + $match = false; } - // Documented types can be a collection (| separated). - foreach (explode('|', $documentedtype) as $documentedtype) { - // Ignore null. They cannot match any type in function. - if (trim($documentedtype) === 'null') { - continue; - } - - if (strlen($expectedtype) && $expectedtype !== $documentedtype) { - // It could be a type hinted array. - if ($expectedtype !== 'array' || substr($documentedtype, -2) !== '[]') { - $match = false; - } - } else if ($documentedtype === 'type') { - $match = false; - } else if ($expectedparam !== $documentedparam) { - $match = false; - } + if (!local_moodlecheck_is_documented_type_allowed($expectedtype, $documentedtype)) { + $match = false; } } } @@ -204,6 +187,49 @@ function local_moodlecheck_functionarguments(local_moodlecheck_file $file) { return $errors; } +/** + * Checks if a documented type is allowed for a parameter with the given real type declaration. + * + * @param string $expectedtype the real type declaration + * @param string $documentedtype the type documented in PHPdoc + * @return bool true if allowed, false if not + */ +function local_moodlecheck_is_documented_type_allowed(string $expectedtype, string $documentedtype): bool { + if ($expectedtype === $documentedtype) { + return true; + } + + // Documented types can be a collection (| separated). + foreach (explode('|', $documentedtype) as $documentedtype) { + // Ignore null. They cannot match any type in function. + if (trim($documentedtype) === 'null') { + continue; + } + + if (strlen($expectedtype) && $expectedtype !== $documentedtype) { + // Allow type-hinted arrays. + if ($expectedtype === 'array' && substr($documentedtype, -2) === '[]') { + continue; + } + + $withoutgenerics = substr($documentedtype, 0, strpos($documentedtype, "<")) ?: $documentedtype; + // Allow class-string and the like for a string parameter. + if ($expectedtype === 'string' && in_array($withoutgenerics, + ["class-string", "interface-string", "trait-string", "enum-string", "callable-string", + "numeric-string", "literal-string", "lowercase-string", "non-empty-string", + "non-empty-lowercase-string"])) { + continue; + } + + return false; + } else if ($documentedtype === 'type') { + return false; + } + } + + return true; +} + /** * Normalise function type to be able to compare it. * diff --git a/tests/fixtures/phpdoc_tags_general.php b/tests/fixtures/phpdoc_tags_general.php index 32021b9..36cb7a3 100644 --- a/tests/fixtures/phpdoc_tags_general.php +++ b/tests/fixtures/phpdoc_tags_general.php @@ -213,6 +213,15 @@ public function correct_param_types5(string $one, ...$params) { echo "yay!"; } + /** + * Correct param types. + * + * @param class-string $myclass + */ + public function correct_param_types6(string $myclass) { + echo "yay!"; + } + /** * Incomplete return annotation (type is missing). * From 7905e6ddde08cfc54b814320553a99c82b09525e Mon Sep 17 00:00:00 2001 From: Maximilian Haye Date: Wed, 23 Oct 2024 17:50:17 +0200 Subject: [PATCH 2/2] feat: support attributes on function parameters --- file.php | 11 +++++++++++ tests/fixtures/phpdoc_method_multiline.php | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/file.php b/file.php index 7ed2f05..bbc5651 100644 --- a/file.php +++ b/file.php @@ -427,6 +427,17 @@ public function &get_functions() { continue; } } + if (version_compare(PHP_VERSION, '8.0.0') >= 0) { + // T_ATTRIBUTE introduced in PHP 8.0. + if ($argtokens[$j][0] === T_ATTRIBUTE) { + [, $attrclose] = $this->find_tag_pair_inlist($argtokens, $j, "#[", "]"); + if ($attrclose) { + // Skip the attribute. + $j = $attrclose; + } + continue; + } + } switch ($argtokens[$j][0]) { // Skip any whitespace, or argument visibility. case T_COMMENT: diff --git a/tests/fixtures/phpdoc_method_multiline.php b/tests/fixtures/phpdoc_method_multiline.php index bbabc27..8ddbaac 100644 --- a/tests/fixtures/phpdoc_method_multiline.php +++ b/tests/fixtures/phpdoc_method_multiline.php @@ -96,4 +96,17 @@ public function function_multiline5( ): array { // Do something. } + + /** + * One function, what else. + * + * @param string $arg + * @return array + */ + public function function_attribute( + #[some_attr] + string $arg + ): array { + // Do something. + } }