Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ namespace Rector\Tests\TypeDeclarationDocblocks\Rector\Property\MergePhpstanDocT

final class KeepNativeDescription
{
/**
* @param string|array|null $excludePattern Optional regex; classes matching this are excluded
* from the layer even when $pattern matches.
* @phpstan-param string|list<string|null>|null $excludePattern
*/
public function layerPattern(string $name, string|array $pattern, string|array|null $excludePattern = null): self
{
}

/**
* @return array some items
* @phpstan-return list<string>
Expand All @@ -36,14 +27,6 @@ namespace Rector\Tests\TypeDeclarationDocblocks\Rector\Property\MergePhpstanDocT

final class KeepNativeDescription
{
/**
* @param string|list<string|null>|null $excludePattern Optional regex; classes matching this are excluded
* from the layer even when $pattern matches.
*/
public function layerPattern(string $name, string|array $pattern, string|array|null $excludePattern = null): self
{
}

/**
* @return list<string> some items
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Rector\Tests\TypeDeclarationDocblocks\Rector\Property\MergePhpstanDocTagIntoNativeRector\Fixture;

final class MultilineContinueNextTag
{
/**
* @param array|null $excludePattern Optional regex; classes matching this are excluded
* from the layer even when $pattern matches.
* @param array $pattern
* @param array $options
* @phpstan-param list<string>|null $excludePattern
* @phpstan-param list<string> $pattern
* @phpstan-param array{
* strict: bool,
* } $options
* @return array
* @phpstan-return list<string>
*/
public function run(array $pattern, ?array $excludePattern, array $options): array
{
}
}

?>
-----
<?php

namespace Rector\Tests\TypeDeclarationDocblocks\Rector\Property\MergePhpstanDocTagIntoNativeRector\Fixture;

final class MultilineContinueNextTag
{
/**
* @param array|null $excludePattern Optional regex; classes matching this are excluded
* from the layer even when $pattern matches.
* @param array $options
* @phpstan-param list<string>|null $excludePattern
* @param list<string> $pattern
* @phpstan-param array{
* strict: bool,
* } $options
* @return list<string>
*/
public function run(array $pattern, ?array $excludePattern, array $options): array
{
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Rector\Tests\TypeDeclarationDocblocks\Rector\Property\MergePhpstanDocTagIntoNativeRector\Fixture;

final class SkipMultilineNativeDescription
{
/**
* @param string|array|null $excludePattern Optional regex; classes matching this are excluded
* from the layer even when $pattern matches.
* @phpstan-param string|list<string|null>|null $excludePattern
*/
public function layerPattern(string $name, string|array $pattern, string|array|null $excludePattern = null): self
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Rector\Tests\TypeDeclarationDocblocks\Rector\Property\MergePhpstanDocTagIntoNativeRector\Fixture;

final class SkipMultilinePhpstanTag
{
/**
* @var array
*
* @phpstan-var array{
* name: string,
* age: int,
* }
*/
private $items;

/**
* @param array $data
*
* @phpstan-param array{
* name: string,
* } $data
*/
public function run(array $data)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\Attribute;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
Expand Down Expand Up @@ -100,74 +102,69 @@ public function refactor(Node $node): ?Node

$phpDocNode = $phpDocInfo->getPhpDocNode();

$phpstanParamNames = [];
$phpstanVarNames = [];
$hasPhpstanReturn = false;
$phpstanTagKeys = [];
$multilineTagKeys = [];
$nativeDescriptions = [];

foreach ($phpDocNode->children as $phpDocChildNode) {
if (! $phpDocChildNode instanceof PhpDocTagNode) {
continue;
}

// remember native tag descriptions, so they survive when the native tag is overruled
if (in_array($phpDocChildNode->name, self::PHPSTAN_TAG_TO_NATIVE_TAG, true)) {
$tagValueNode = $phpDocChildNode->value;
if (($tagValueNode instanceof ParamTagValueNode || $tagValueNode instanceof VarTagValueNode || $tagValueNode instanceof ReturnTagValueNode) && $tagValueNode->description !== '') {
$nativeDescriptions[$this->resolveTagKey($tagValueNode)] = $tagValueNode->description;
}

$tagValueNode = $this->resolveMergeableTagValueNode($phpDocChildNode);
if (! $tagValueNode instanceof PhpDocTagValueNode) {
continue;
}

if ($phpDocChildNode->name === '@phpstan-param' && $phpDocChildNode->value instanceof ParamTagValueNode) {
$phpstanParamNames[] = $phpDocChildNode->value->parameterName;
} elseif ($phpDocChildNode->name === '@phpstan-var' && $phpDocChildNode->value instanceof VarTagValueNode) {
$phpstanVarNames[] = $phpDocChildNode->value->variableName;
} elseif ($phpDocChildNode->name === '@phpstan-return' && $phpDocChildNode->value instanceof ReturnTagValueNode) {
$hasPhpstanReturn = true;
$tagKey = $this->resolveTagKey($tagValueNode);

// skip multiline tag, as phpcs does not support multiline native tag yet
if ($this->isMultiline($phpDocChildNode)) {
$multilineTagKeys[] = $tagKey;
}

if (isset(self::PHPSTAN_TAG_TO_NATIVE_TAG[$phpDocChildNode->name])) {
$phpstanTagKeys[] = $tagKey;
} elseif ($tagValueNode->description !== '') {
// remember native tag descriptions, so they survive when the native tag is overruled
$nativeDescriptions[$tagKey] = $tagValueNode->description;
}
}

if ($phpstanParamNames === [] && $phpstanVarNames === [] && ! $hasPhpstanReturn) {
$mergeableTagKeys = array_diff($phpstanTagKeys, $multilineTagKeys);
if ($mergeableTagKeys === []) {
return null;
}

$hasChanged = false;

foreach ($phpDocNode->children as $key => $phpDocChildNode) {
if (! $phpDocChildNode instanceof PhpDocTagNode) {
continue;
}

$tagValueNode = $this->resolveMergeableTagValueNode($phpDocChildNode);
if (! $tagValueNode instanceof PhpDocTagValueNode) {
continue;
}

$tagKey = $this->resolveTagKey($tagValueNode);
if (! in_array($tagKey, $mergeableTagKeys, true)) {
continue;
}

$nativeTagName = self::PHPSTAN_TAG_TO_NATIVE_TAG[$phpDocChildNode->name] ?? null;

// drop the weaker native tag that the @phpstan-* variant overrules
if ($this->isOverruledNativeTag(
$phpDocChildNode,
$phpstanParamNames,
$phpstanVarNames,
$hasPhpstanReturn
)) {
if ($nativeTagName === null) {
unset($phpDocNode->children[$key]);
$hasChanged = true;
continue;
}

// promote the @phpstan-* tag to its native counterpart
$nativeTagName = self::PHPSTAN_TAG_TO_NATIVE_TAG[$phpDocChildNode->name] ?? null;
if ($nativeTagName !== null) {
// keep the description of the overruled native tag
$tagValueNode = $phpDocChildNode->value;
if (($tagValueNode instanceof ParamTagValueNode || $tagValueNode instanceof VarTagValueNode || $tagValueNode instanceof ReturnTagValueNode) && $tagValueNode->description === '') {
$tagValueNode->description = $nativeDescriptions[$this->resolveTagKey($tagValueNode)] ?? '';
}

$phpDocNode->children[$key] = new PhpDocTagNode($nativeTagName, $phpDocChildNode->value);
$hasChanged = true;
// promote the @phpstan-* tag to its native counterpart, keeping the description of the overruled native tag
if ($tagValueNode->description === '') {
$tagValueNode->description = $nativeDescriptions[$tagKey] ?? '';
}
}

if (! $hasChanged) {
return null;
$phpDocNode->children[$key] = new PhpDocTagNode($nativeTagName, $tagValueNode);
}

$phpDocNode->children = array_values($phpDocNode->children);
Expand All @@ -188,29 +185,31 @@ public function refactor(Node $node): ?Node
return $node;
}

/**
* @param string[] $phpstanParamNames
* @param string[] $phpstanVarNames
*/
private function isOverruledNativeTag(
PhpDocTagNode $phpDocTagNode,
array $phpstanParamNames,
array $phpstanVarNames,
bool $hasPhpstanReturn
): bool {
if ($phpDocTagNode->name === '@param' && $phpDocTagNode->value instanceof ParamTagValueNode) {
return in_array($phpDocTagNode->value->parameterName, $phpstanParamNames, true);
private function resolveMergeableTagValueNode(
PhpDocTagNode $phpDocTagNode
): ParamTagValueNode|VarTagValueNode|ReturnTagValueNode|null {
if (! isset(self::PHPSTAN_TAG_TO_NATIVE_TAG[$phpDocTagNode->name]) && ! in_array(
$phpDocTagNode->name,
self::PHPSTAN_TAG_TO_NATIVE_TAG,
true
)) {
return null;
}

if ($phpDocTagNode->name === '@var' && $phpDocTagNode->value instanceof VarTagValueNode) {
return in_array($phpDocTagNode->value->variableName, $phpstanVarNames, true);
$tagValueNode = $phpDocTagNode->value;
if ($tagValueNode instanceof ParamTagValueNode || $tagValueNode instanceof VarTagValueNode || $tagValueNode instanceof ReturnTagValueNode) {
return $tagValueNode;
}

if ($phpDocTagNode->name === '@return' && $phpDocTagNode->value instanceof ReturnTagValueNode) {
return $hasPhpstanReturn;
}
return null;
}

private function isMultiline(PhpDocTagNode $phpDocTagNode): bool
{
$startLine = $phpDocTagNode->getAttribute(Attribute::START_LINE);
$endLine = $phpDocTagNode->getAttribute(Attribute::END_LINE);

return false;
return is_int($startLine) && is_int($endLine) && $endLine > $startLine;
}

private function resolveTagKey(ParamTagValueNode|VarTagValueNode|ReturnTagValueNode $tagValueNode): string
Expand Down
Loading