diff --git a/rector.php b/rector.php index 4a6d3db3656..47dd720db67 100644 --- a/rector.php +++ b/rector.php @@ -7,6 +7,7 @@ use Rector\DeadCode\Rector\ConstFetch\RemovePhpVersionIdCheckRector; use Rector\DeadCode\Rector\Property\RemoveUnusedPrivatePropertyRector; use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; +use Rector\TypeDeclaration\Rector\Expression\InlineVarDocTagToAssertRector; return RectorConfig::configure() ->withPreparedSets( @@ -57,4 +58,5 @@ ], RemoveUnusedPrivatePropertyRector::class => [__DIR__ . '/src/Configuration/RectorConfigBuilder.php'], + InlineVarDocTagToAssertRector::class, ]); diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_all_basic_types.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_all_basic_types.php.inc new file mode 100644 index 00000000000..c64d0726b62 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_all_basic_types.php.inc @@ -0,0 +1,73 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_classes_and_interfaces.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_classes_and_interfaces.php.inc new file mode 100644 index 00000000000..be9bfb083a6 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_classes_and_interfaces.php.inc @@ -0,0 +1,43 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_intersection_types.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_intersection_types.php.inc new file mode 100644 index 00000000000..d37b309a5bb --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_intersection_types.php.inc @@ -0,0 +1,19 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_nullable_types.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_nullable_types.php.inc new file mode 100644 index 00000000000..01690e8c85b --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_nullable_types.php.inc @@ -0,0 +1,19 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_self_static_and_parent.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_self_static_and_parent.php.inc new file mode 100644 index 00000000000..64f0e9ff54e --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_self_static_and_parent.php.inc @@ -0,0 +1,45 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_union_types.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_union_types.php.inc new file mode 100644 index 00000000000..9c41cac7860 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_union_types.php.inc @@ -0,0 +1,43 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_variable_in_some_function.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_variable_in_some_function.php.inc new file mode 100644 index 00000000000..92586d4f03e --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_variable_in_some_function.php.inc @@ -0,0 +1,21 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_variable_in_some_method.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_variable_in_some_method.php.inc new file mode 100644 index 00000000000..e2a99b9c8fe --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/convert_variable_in_some_method.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/preserve_comments.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/preserve_comments.php.inc new file mode 100644 index 00000000000..c32ec25508d --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/preserve_comments.php.inc @@ -0,0 +1,77 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_complex_types.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_complex_types.php.inc new file mode 100644 index 00000000000..2e787e5fa74 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_complex_types.php.inc @@ -0,0 +1,11 @@ + $foo */ +$foo = getArrayOfString(); + +/** @var string[] $foo */ +$foo = getArrayOfString(); + +/** @var Collection $foo */ +$foo = getCollection(); + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_incorrect_php_docs.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_incorrect_php_docs.php.inc new file mode 100644 index 00000000000..96bd7fce826 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_incorrect_php_docs.php.inc @@ -0,0 +1,18 @@ + $foo */ +$foo = getFooAndCollection(); + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_nullable_complex_types.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_nullable_complex_types.php.inc new file mode 100644 index 00000000000..d15ef550ed2 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_nullable_complex_types.php.inc @@ -0,0 +1,11 @@ + $foo */ +$foo = getArrayOfString(); + +/** @var ?string[] $foo */ +$foo = getArrayOfString(); + +/** @var ?Collection $foo */ +$foo = getCollection(); + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_some_basic_narrowed_types.php.inc b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_some_basic_narrowed_types.php.inc new file mode 100644 index 00000000000..c8d97a15212 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/Fixture/skip_some_basic_narrowed_types.php.inc @@ -0,0 +1,14 @@ +|false $foo */ +$foo = getArrayOfString(); + +/** @var string[]|null $foo */ +$foo = getArrayOfString(); + +/** @var string|Collection $foo */ +$foo = getCollection(); + +/** @var array|Collection $foo */ +$foo = getCollectionOrArray(); + diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/InlineVarDocTagToAssertRectorTest.php b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/InlineVarDocTagToAssertRectorTest.php new file mode 100644 index 00000000000..2627bf49724 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/InlineVarDocTagToAssertRectorTest.php @@ -0,0 +1,31 @@ +doTestFile($filePath); + } + + /** + * @return Iterator + */ + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/config/configured_rule.php b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/config/configured_rule.php new file mode 100644 index 00000000000..951809d2aab --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector/config/configured_rule.php @@ -0,0 +1,12 @@ +phpVersion(PhpVersionFeature::STRING_IN_ASSERT_ARG); + $rectorConfig->rule(InlineVarDocTagToAssertRector::class); +}; diff --git a/rules/TypeDeclaration/PhpDocParser/TypeExpressionFromVarTagResolver.php b/rules/TypeDeclaration/PhpDocParser/TypeExpressionFromVarTagResolver.php new file mode 100644 index 00000000000..43b817a8f74 --- /dev/null +++ b/rules/TypeDeclaration/PhpDocParser/TypeExpressionFromVarTagResolver.php @@ -0,0 +1,154 @@ +scalarStringToTypeMapper->mapScalarStringToType($typeNode->name); + $scalarTypeFunction = $this->getScalarTypeFunction($scalarType::class); + if ($scalarTypeFunction !== null) { + $arg = new Arg($variable); + return new FuncCall(new Name($scalarTypeFunction), [$arg]); + } + + if ($scalarType instanceof NullType) { + return new Identical($variable, new ConstFetch(new Name('null'))); + } + + if ($scalarType instanceof ConstantBooleanType) { + return new Identical( + $variable, + new ConstFetch(new Name($scalarType->getValue() ? 'true' : 'false')) + ); + } + + if ($scalarType instanceof MixedType && ! $scalarType->isExplicitMixed()) { + return new Instanceof_($variable, new Name($typeNode->name)); + } + } elseif ($typeNode instanceof NullableTypeNode) { + $unionExpressions = []; + $nullableTypeExpression = $this->resolveTypeExpressionFromVarTag($typeNode->type, $variable); + if ($nullableTypeExpression === false) { + return false; + } + + $unionExpressions[] = $nullableTypeExpression; + $nullExpression = $this->resolveTypeExpressionFromVarTag(new IdentifierTypeNode('null'), $variable); + assert($nullExpression !== false); + $unionExpressions[] = $nullExpression; + return $this->generateOrExpression($unionExpressions); + } elseif ($typeNode instanceof BracketsAwareUnionTypeNode) { + $unionExpressions = []; + foreach ($typeNode->types as $typeNode) { + $unionExpression = $this->resolveTypeExpressionFromVarTag($typeNode, $variable); + if ($unionExpression === false) { + return false; + } + + $unionExpressions[] = $unionExpression; + } + + return $this->generateOrExpression($unionExpressions); + } elseif ($typeNode instanceof BracketsAwareIntersectionTypeNode) { + $intersectionExpressions = []; + foreach ($typeNode->types as $typeNode) { + $intersectionExpression = $this->resolveTypeExpressionFromVarTag($typeNode, $variable); + if ($intersectionExpression === false) { + return false; + } + + $intersectionExpressions[] = $intersectionExpression; + } + + return $this->generateAndExpression($intersectionExpressions); + } + + return false; + } + + /** + * @param Expr[] $unionExpressions + * @return BooleanOr + */ + private function generateOrExpression(array $unionExpressions) + { + $booleanOr = new BooleanOr($unionExpressions[0], $unionExpressions[1]); + if (count($unionExpressions) == 2) { + return $booleanOr; + } + + array_splice($unionExpressions, 0, 2, [$booleanOr]); + return $this->generateOrExpression($unionExpressions); + } + + /** + * @param Expr[] $intersectionExpressions + * @return BooleanAnd + */ + private function generateAndExpression(array $intersectionExpressions) + { + $booleanAnd = new BooleanAnd($intersectionExpressions[0], $intersectionExpressions[1]); + if (count($intersectionExpressions) == 2) { + return $booleanAnd; + } + + array_splice($intersectionExpressions, 0, 2, [$booleanAnd]); + return $this->generateAndExpression($intersectionExpressions); + } + + /** + * @param class-string $className + */ + private function getScalarTypeFunction(string $className): ?string + { + return match ($className) { + IntegerType::class => 'is_int', + BooleanType::class => 'is_bool', + FloatType::class => 'is_float', + StringType::class => 'is_string', + ArrayType::class => 'is_array', + CallableType::class => 'is_callable', + ObjectWithoutClassType::class => 'is_object', + IterableType::class => 'is_iterable', + default => null, + }; + } +} diff --git a/rules/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector.php b/rules/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector.php new file mode 100644 index 00000000000..476b00488ed --- /dev/null +++ b/rules/TypeDeclaration/Rector/Expression/InlineVarDocTagToAssertRector.php @@ -0,0 +1,130 @@ +> + */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + /** + * @param Expression $node + * @return Node[]|null + */ + public function refactor(Node $node): ?array + { + if (! $node->expr instanceof Assign) { + return null; + } + + if (! $node->expr->var instanceof Variable) { + return null; + } + + $docComment = $node->getDocComment(); + if (! $docComment instanceof Doc) { + return null; + } + + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + + if (! $phpDocInfo instanceof PhpDocInfo || $phpDocInfo->getPhpDocNode()->children === []) { + return null; + } + + $expressionVariableName = $node->expr->var->name; + foreach ($phpDocInfo->getPhpDocNode()->children as $phpDocChildNode) { + if (! $phpDocChildNode instanceof PhpDocTagNode) { + continue; + } + + $tagValueNode = $phpDocChildNode->value; + // handle only basic types, keep phpstan/psalm helper ones + if ($tagValueNode instanceof VarTagValueNode && $phpDocChildNode->name === '@var') { + //remove $ from variable name + $variableName = substr($tagValueNode->variableName, 1); + if ($variableName === $expressionVariableName && $tagValueNode->description === '') { + $typeExpression = $this->typeExpressionFromVarTagResolver->resolveTypeExpressionFromVarTag( + $tagValueNode->type, + new Variable($variableName) + ); + if ($typeExpression !== false) { + $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $tagValueNode); + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + $node->setAttribute(AttributeKey::DO_NOT_CHANGE, true); + + $arg = new Arg($typeExpression); + $funcCall = new FuncCall(new Name('assert'), [$arg]); + $expression = new Expression($funcCall); + return [$node, $expression]; + } + } + } + } + + return null; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::STRING_IN_ASSERT_ARG; + } +} diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index 2ebbfd23a98..59b11f5170d 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -48,6 +48,7 @@ use Rector\TypeDeclaration\Rector\Closure\AddClosureVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeRector; use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector; +use Rector\TypeDeclaration\Rector\Expression\InlineVarDocTagToAssertRector; use Rector\TypeDeclaration\Rector\Function_\AddFunctionVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\FunctionLike\AddParamTypeSplFixedArrayRector; use Rector\TypeDeclaration\Rector\FunctionLike\AddReturnTypeDeclarationFromYieldsRector; @@ -134,5 +135,6 @@ final class TypeDeclarationLevel StrictArrayParamDimFetchRector::class, StrictStringParamConcatRector::class, TypedPropertyFromJMSSerializerAttributeTypeRector::class, + InlineVarDocTagToAssertRector::class, ]; } diff --git a/src/Rector/AbstractRector.php b/src/Rector/AbstractRector.php index 80a7a9d0a77..1f76add243a 100644 --- a/src/Rector/AbstractRector.php +++ b/src/Rector/AbstractRector.php @@ -263,6 +263,10 @@ protected function mirrorComments(Node $newNode, Node $oldNode): void return; } + if ($newNode->getAttribute(AttributeKey::DO_NOT_CHANGE) === true) { + return; + } + $newNode->setAttribute(AttributeKey::PHP_DOC_INFO, $oldNode->getAttribute(AttributeKey::PHP_DOC_INFO)); if (! $newNode instanceof Nop) { $newNode->setAttribute(AttributeKey::COMMENTS, $oldNode->getAttribute(AttributeKey::COMMENTS));