From 2a4b72499d96f30075a4f7669c970023f00dea3a Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 13 Apr 2026 16:11:56 +0200 Subject: [PATCH] Fix DocBlockParamAllowDefaultValueSniff positional mismatch on partial @param lists When a docblock's @param list omitted intermediate parameters (e.g. only documenting $row and $errors while skipping $accountId between them), the sniff matched docblock tags against the method signature by position. The second @param ($errors) was compared against signature slot 1 ($accountId, int), producing a false-positive "missing type int" fix that added `|int` to the documented type. DocBlockParamTypeMismatchSniff correctly matched by name, removed the `|int`, and the two sniffs entered an infinite fixer loop until phpcbf bailed with "FAILED TO FIX". Index the signature by parameter name and look up each @param by the parsed valueNode->parameterName instead. --- .../DocBlockParamAllowDefaultValueSniff.php | 30 ++++++++++++------- .../DocBlockParamAllowDefaultValue/after.php | 14 +++++++++ .../DocBlockParamAllowDefaultValue/before.php | 14 +++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/PhpCollective/Sniffs/Commenting/DocBlockParamAllowDefaultValueSniff.php b/PhpCollective/Sniffs/Commenting/DocBlockParamAllowDefaultValueSniff.php index 3e57b61..63e9aaa 100644 --- a/PhpCollective/Sniffs/Commenting/DocBlockParamAllowDefaultValueSniff.php +++ b/PhpCollective/Sniffs/Commenting/DocBlockParamAllowDefaultValueSniff.php @@ -56,7 +56,15 @@ public function process(File $phpcsFile, $stackPointer): void $docBlockStartIndex = $tokens[$docBlockEndIndex]['comment_opener']; - $paramCount = 0; + // Index signature entries by parameter name so partial or out-of-order + // @param lists don't cause positional mismatches, which previously + // produced false-positive "missing type" fixes that fought + // DocBlockParamTypeMismatchSniff in an infinite fixer loop. + $signatureByName = []; + foreach ($methodSignature as $sigEntry) { + $signatureByName[$sigEntry['variable']] = $sigEntry; + } + for ($i = $docBlockStartIndex + 1; $i < $docBlockEndIndex; $i++) { if ($tokens[$i]['type'] !== 'T_DOC_COMMENT_TAG') { continue; @@ -65,12 +73,6 @@ public function process(File $phpcsFile, $stackPointer): void continue; } - if (empty($methodSignature[$paramCount])) { - continue; - } - $methodSignatureValue = $methodSignature[$paramCount]; - $paramCount++; - $classNameIndex = $i + 2; if ($tokens[$classNameIndex]['type'] !== 'T_DOC_COMMENT_STRING') { @@ -83,15 +85,21 @@ public function process(File $phpcsFile, $stackPointer): void continue; } - if (empty($methodSignatureValue['typehint']) && empty($methodSignatureValue['default'])) { - continue; - } - /** @var \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\TypelessParamTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode $valueNode */ $valueNode = static::getValueNode($tokens[$i]['content'], $content); if ($valueNode instanceof InvalidTagValueNode || $valueNode instanceof TypelessParamTagValueNode) { return; } + + if (!isset($signatureByName[$valueNode->parameterName])) { + continue; + } + $methodSignatureValue = $signatureByName[$valueNode->parameterName]; + + if (empty($methodSignatureValue['typehint']) && empty($methodSignatureValue['default'])) { + continue; + } + $parts = $this->valueNodeParts($valueNode); // We skip for mixed diff --git a/tests/_data/DocBlockParamAllowDefaultValue/after.php b/tests/_data/DocBlockParamAllowDefaultValue/after.php index a34b335..fba7856 100644 --- a/tests/_data/DocBlockParamAllowDefaultValue/after.php +++ b/tests/_data/DocBlockParamAllowDefaultValue/after.php @@ -85,4 +85,18 @@ public function multipleUnion(int|string|null $value): void public function arrayShape(array $array = null): void { } + + /** + * Partial doc block - only $row and $errors are documented, skipping intermediate + * parameters. Positional matching would wrongly map docblock's $errors to the $accountId + * signature slot and try to add `int` to the list type, fighting + * DocBlockParamTypeMismatchSniff in an infinite fixer loop. Must be matched by name. + * + * @param array $row + * @param list $errors + * @return void + */ + public function partialDocBlock(array $row, int $accountId, array &$errors): void + { + } } diff --git a/tests/_data/DocBlockParamAllowDefaultValue/before.php b/tests/_data/DocBlockParamAllowDefaultValue/before.php index 2abb19a..c644f32 100644 --- a/tests/_data/DocBlockParamAllowDefaultValue/before.php +++ b/tests/_data/DocBlockParamAllowDefaultValue/before.php @@ -85,4 +85,18 @@ public function multipleUnion(int|string|null $value): void public function arrayShape(array $array = null): void { } + + /** + * Partial doc block - only $row and $errors are documented, skipping intermediate + * parameters. Positional matching would wrongly map docblock's $errors to the $accountId + * signature slot and try to add `int` to the list type, fighting + * DocBlockParamTypeMismatchSniff in an infinite fixer loop. Must be matched by name. + * + * @param array $row + * @param list $errors + * @return void + */ + public function partialDocBlock(array $row, int $accountId, array &$errors): void + { + } }