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 + { + } }