[TypeDeclaration] Add ArrayShapeFromConstantArrayReturnRector (#1908)

This commit is contained in:
Tomas Votruba 2022-03-07 12:28:39 +01:00 committed by GitHub
parent 55a13c0020
commit 8057e33f91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 446 additions and 29 deletions

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
use Nette\Utils\FileSystem;
use Nette\Utils\Strings;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
@ -45,14 +46,14 @@ final class CleanPhpstanCommand extends Command
return self::FAILURE;
}
$originalContent = (string) file_get_contents(self::FILE);
$originalContent = FileSystem::read(self::FILE);
$newContent = str_replace(
'reportUnmatchedIgnoredErrors: false',
'reportUnmatchedIgnoredErrors: true',
$originalContent
);
file_put_contents(self::FILE, $newContent);
FileSystem::write(self::FILE, $newContent);
$process = new Process(['composer', 'phpstan']);
$process->run();
@ -63,7 +64,7 @@ final class CleanPhpstanCommand extends Command
$output->writeln($result);
if (! $isFailure) {
file_put_contents(self::FILE, $originalContent);
FileSystem::write(self::FILE, $originalContent);
return self::SUCCESS;
}
@ -74,8 +75,8 @@ final class CleanPhpstanCommand extends Command
$matchAll = Strings::matchAll($result, self::ONELINE_IGNORED_PATTERN_REGEX);
foreach ($matchAll as $match) {
$newContent = str_replace(' - \'' . $match['content'] . '\'', '', $newContent);
foreach ($matchAll as $singleMatchAll) {
$newContent = str_replace(" - '" . $singleMatchAll['content'] . "'", '', $newContent);
}
$newContent = str_replace(
@ -83,7 +84,7 @@ final class CleanPhpstanCommand extends Command
'reportUnmatchedIgnoredErrors: false',
$newContent
);
file_put_contents(self::FILE, $newContent);
FileSystem::write(self::FILE, $newContent);
$process2 = new Process(['composer', 'phpstan']);
$process2->run();

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
use Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnNewRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictTypedCallRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictTypedPropertyRector;
@ -22,7 +23,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
$services->set(ReturnTypeFromStrictTypedCallRector::class);
$services->set(AddVoidReturnTypeWhereNoReturnRector::class);
$services->set(ReturnTypeFromReturnNewRector::class);
$services->set(TypedPropertyFromStrictGetterMethodReturnTypeRector::class);
$services->set(AddMethodCallBasedStrictParamTypeRector::class);
$services->set(ArrayShapeFromConstantArrayReturnRector::class);
};

View File

@ -95,7 +95,7 @@ final class IndentTest extends TestCase
}
/**
* @return int[]
* @return array{int-one: int, int-greater-than-one: int}
*/
private static function sizes(): array
{

View File

@ -28,6 +28,9 @@ use Rector\PHPStanStaticTypeMapper\Utils\TypeUnwrapper;
use Rector\StaticTypeMapper\StaticTypeMapper;
use ReturnTypeWillChange;
/**
* @see https://wiki.php.net/rfc/internal_method_return_types#proposal
*/
final class PhpDocFromTypeDeclarationDecorator
{
/**

View File

@ -67,7 +67,7 @@ final class RectorWithLineChange implements SerializableInterface
}
/**
* @return array<string, mixed>
* @return array{rector_class: class-string<RectorInterface>, line: int}
*/
public function jsonSerialize(): array
{

View File

@ -9,6 +9,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\Generic\GenericClassStringType;
@ -213,6 +214,14 @@ final class TypeComparator
return false;
}
if ($firstType instanceof ConstantArrayType) {
return false;
}
if ($secondType instanceof ConstantArrayType) {
return false;
}
$firstKeyType = $this->normalizeSingleUnionType($firstType->getKeyType());
$secondKeyType = $this->normalizeSingleUnionType($secondType->getKeyType());

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Rector\PHPStanStaticTypeMapper\TypeMapper;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper;
final class ArrayShapeTypeMapper
{
public function __construct(
private readonly PHPStanStaticTypeMapper $phpStanStaticTypeMapper
) {
}
public function mapConstantArrayType(ConstantArrayType $constantArrayType): ArrayShapeNode|ArrayType|null
{
// empty array
if ($constantArrayType->getKeyType() instanceof NeverType) {
return new ArrayType(new MixedType(), new MixedType());
}
$arrayShapeItemNodes = [];
foreach ($constantArrayType->getKeyTypes() as $index => $keyType) {
// not real array shape
if (! $keyType instanceof ConstantStringType) {
return null;
}
$keyDocTypeNode = new IdentifierTypeNode($keyType->getValue());
$valueType = $constantArrayType->getValueTypes()[$index];
$valueDocTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode(
$valueType,
TypeKind::RETURN()
);
$arrayShapeItemNodes[] = new ArrayShapeItemNode(
$keyDocTypeNode,
$constantArrayType->isOptionalKey($index),
$valueDocTypeNode
);
}
return new ArrayShapeNode($arrayShapeItemNodes);
}
}

View File

@ -53,6 +53,8 @@ final class ArrayTypeMapper implements TypeMapperInterface
private DetailedTypeAnalyzer $detailedTypeAnalyzer;
private ArrayShapeTypeMapper $arrayShapeTypeMapper;
// To avoid circular dependency
#[Required]
@ -61,13 +63,15 @@ final class ArrayTypeMapper implements TypeMapperInterface
UnionTypeCommonTypeNarrower $unionTypeCommonTypeNarrower,
ReflectionProvider $reflectionProvider,
GenericClassStringTypeNormalizer $genericClassStringTypeNormalizer,
DetailedTypeAnalyzer $detailedTypeAnalyzer
DetailedTypeAnalyzer $detailedTypeAnalyzer,
ArrayShapeTypeMapper $arrayShapeTypeMapper
): void {
$this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper;
$this->unionTypeCommonTypeNarrower = $unionTypeCommonTypeNarrower;
$this->reflectionProvider = $reflectionProvider;
$this->genericClassStringTypeNormalizer = $genericClassStringTypeNormalizer;
$this->detailedTypeAnalyzer = $detailedTypeAnalyzer;
$this->arrayShapeTypeMapper = $arrayShapeTypeMapper;
}
/**
@ -89,6 +93,13 @@ final class ArrayTypeMapper implements TypeMapperInterface
return $this->createArrayTypeNodeFromUnionType($itemType, $typeKind);
}
if ($type instanceof ConstantArrayType && $typeKind->equals(TypeKind::RETURN())) {
$arrayShapeNode = $this->arrayShapeTypeMapper->mapConstantArrayType($type);
if ($arrayShapeNode instanceof TypeNode) {
return $arrayShapeNode;
}
}
if ($itemType instanceof ArrayType && $this->isGenericArrayCandidate($itemType)) {
return $this->createGenericArrayType($type, $typeKind, true);
}

View File

@ -8,7 +8,7 @@ includes:
- vendor/symplify/phpstan-rules/config/services-rules.neon
- vendor/symplify/phpstan-rules/config/static-rules.neon
- vendor/symplify/phpstan-rules/config/string-to-constant-rules.neon
- vendor/symplify/phpstan-rules/config/symfony-rules.neon
- vendor/symplify/phpstan-rules/packages/symfony/config/symfony-rules.neon
- vendor/symplify/phpstan-rules/config/test-rules.neon
parameters:
@ -575,3 +575,10 @@ parameters:
-
message: '#Make callable type explicit#'
path: src/NodeManipulator/BinaryOpManipulator.php
# resolve later
- '#Add explicit array type to assigned "(.*?)" expression#'
- '#Complete known array shape to the method @return type#'
# @todo remove from phpstan-rules for reoctr
- '#Function "array_walk\(\)" cannot be used/left in the code\: #'
- '#Instead of "Nette\\Utils\\FileSystem" class/interface use "Symplify\\SmartFileSystem\\SmartFileSystem"#'

View File

@ -16,6 +16,7 @@ use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Privatization\Rector\Class_\FinalizeClassesWithoutChildrenRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
@ -73,6 +74,10 @@ return static function (ContainerConfigurator $containerConfigurator): void {
MyCLabsClassToEnumRector::class,
SpatieEnumClassToEnumRector::class,
ArrayShapeFromConstantArrayReturnRector::class => [
__DIR__ . '/rules/Transform/Rector/ClassMethod/ReturnTypeWillChangeRector.php',
],
// test paths
'*/tests/**/Fixture/*',
'*/rules-tests/**/Fixture/*',

View File

@ -2,7 +2,7 @@
namespace Rector\Tests\CodingStyle\Rector\ClassConst\VarConstantCommentRector\Fixture;
class PairClassStringArray
final class PairClassStringArray
{
private const CLASSES = [
\stdClass::class => PairClassStringArray::class,
@ -15,7 +15,7 @@ class PairClassStringArray
namespace Rector\Tests\CodingStyle\Rector\ClassConst\VarConstantCommentRector\Fixture;
class PairClassStringArray
final class PairClassStringArray
{
/**
* @var array<string, class-string<\Rector\Tests\CodingStyle\Rector\ClassConst\VarConstantCommentRector\Fixture\PairClassStringArray>>

View File

@ -37,7 +37,7 @@ namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddArrayReturnDocTypeR
class ChildHasPriority extends ParentClassWithDefinedReturn
{
/**
* @return array<int, array<string, int|float|string>>
* @return array<int, array{a: string, b: int, c: float}>
*/
public function getData()
{

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;
final class ArrayShapeFromConstantArrayReturnRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}
/**
* @return Iterator<SmartFileInfo>
*/
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Fixture;
final class IncludeConstants
{
public const NAME = 'name';
public function run(string $name)
{
return [self::NAME => $name];
}
}
?>
-----
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Fixture;
final class IncludeConstants
{
public const NAME = 'name';
/**
* @return array{name: string}
*/
public function run(string $name)
{
return [self::NAME => $name];
}
}
?>

View File

@ -0,0 +1,43 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Fixture;
use Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Source\ExternalConstant;
final class IncludeExternalConstants
{
/**
* @return array<string, string|int>
*/
public function run(int $line, string $file): array
{
return [
ExternalConstant::LINE => $line,
ExternalConstant::FILE => $file,
];
}
}
?>
-----
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Fixture;
use Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Source\ExternalConstant;
final class IncludeExternalConstants
{
/**
* @return array{line: int, file: string}
*/
public function run(int $line, string $file): array
{
return [
ExternalConstant::LINE => $line,
ExternalConstant::FILE => $file,
];
}
}
?>

View File

@ -0,0 +1,11 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Fixture;
final class SkipEmptyArray
{
public function run()
{
return [];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Fixture;
use PhpParser\Node\Stmt\ClassMethod;
final class SkipNoStringKeys
{
public function run()
{
return [ClassMethod::class];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Fixture;
final class SomeClass
{
public function run(string $name)
{
return ['name' => $name];
}
}
?>
-----
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Fixture;
final class SomeClass
{
/**
* @return array{name: string}
*/
public function run(string $name)
{
return ['name' => $name];
}
}
?>

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\Source;
final class ExternalConstant
{
public const LINE = 'line';
public const FILE = 'file';
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Rector\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(ArrayShapeFromConstantArrayReturnRector::class);
};

View File

@ -20,7 +20,7 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
final class DowngradePhp72JsonConstRector extends AbstractRector
{
/**
* @var array<string>
* @var string[]
*/
private const CONSTANTS = ['JSON_INVALID_UTF8_IGNORE', 'JSON_INVALID_UTF8_SUBSTITUTE'];

View File

@ -108,7 +108,7 @@ final class EregToPcreTransformer
/**
* Recursively converts ERE into PCRE, starting at the position $i.
*
* @return mixed[]
* @return float[]|int[]|string[]
*/
private function _ere2pcre(string $content, int $i): array
{
@ -223,11 +223,14 @@ final class EregToPcreTransformer
$i = $ii;
}
// retype
$i = (int) $i;
return $i;
}
/**
* @return mixed[]
* @return float[]|int[]|string[]
*/
private function processSquareBracket(string $s, int $i, int $l, string $cls, bool $start): array
{

View File

@ -119,7 +119,7 @@ CODE_SAMPLE
}
/**
* @return array<int, Assign|FuncCall>
* @return array<Assign|FuncCall>
*/
private function createNewNodes(Expr $assignVariable, Expr $eachedVariable): array
{

View File

@ -83,7 +83,6 @@ CODE_SAMPLE
return null;
}
/** @var FuncCall $node */
$node->args = $this->composeNewArgs($node);
return $node;

View File

@ -20,6 +20,8 @@ final class ArgumentSorter
$newArgsOrParams = [];
foreach (array_keys($argOrParams) as $position) {
assert(is_int($position));
$newPosition = $oldToNewPositions[$position] ?? null;
if ($newPosition === null) {
continue;

View File

@ -28,6 +28,8 @@ final class SwitchExprsResolver
$this->moveDefaultCaseToLast($switch);
foreach ($switch->cases as $key => $case) {
assert(is_int($key));
if (! $this->isValidCase($case)) {
return [];
}

View File

@ -163,8 +163,8 @@ CODE_SAMPLE
}
/**
* @param Param[] $currentClassMethodParams
* @param Param[] $parentClassMethodParams
* @param array<int, Param> $currentClassMethodParams
* @param array<int, Param> $parentClassMethodParams
*/
private function processReplaceClassMethodParams(
ClassMethod $node,

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\NeverType;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\BetterPhpDocParser\ValueObject\Type\SpacingAwareArrayTypeNode;
use Rector\Core\Rector\AbstractRector;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Symplify\Astral\TypeAnalyzer\ClassMethodReturnTypeResolver;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
* @see \Rector\Tests\TypeDeclaration\Rector\ClassMethod\ArrayShapeFromConstantArrayReturnRector\ArrayShapeFromConstantArrayReturnRectorTest
*/
final class ArrayShapeFromConstantArrayReturnRector extends AbstractRector
{
public function __construct(
private readonly ClassMethodReturnTypeResolver $classMethodReturnTypeResolver,
private readonly PhpDocTypeChanger $phpDocTypeChanger
) {
}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Add array shape exact types based on constant keys of array', [
new CodeSample(
<<<'CODE_SAMPLE'
final class SomeClass
{
public function run(string $name)
{
return ['name' => $name];
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
final class SomeClass
{
/**
* @return array{name: string}
*/
public function run(string $name)
{
return ['name' => $name];
}
}
CODE_SAMPLE
),
]);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
/** @var Return_[] $returns */
$returns = $this->betterNodeFinder->findInstanceOf($node, Return_::class);
// exact one shape only
if (count($returns) !== 1) {
return null;
}
$return = $returns[0];
if (! $return->expr instanceof Expr) {
return null;
}
$returnExprType = $this->getType($return->expr);
if (! $returnExprType instanceof ConstantArrayType) {
return null;
}
// empty array
if ($returnExprType->getKeyType() instanceof NeverType) {
return null;
}
$returnType = $this->classMethodReturnTypeResolver->resolve($node, $node->getAttribute(AttributeKey::SCOPE));
if ($returnType instanceof ConstantArrayType) {
return null;
}
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
$returnExprTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode(
$returnExprType,
TypeKind::RETURN()
);
if ($returnExprTypeNode instanceof GenericTypeNode) {
return null;
}
if ($returnExprTypeNode instanceof SpacingAwareArrayTypeNode) {
return null;
}
$hasChanged = $this->phpDocTypeChanger->changeReturnType($phpDocInfo, $returnExprType);
if (! $hasChanged) {
return null;
}
return $node;
}
}

View File

@ -70,6 +70,10 @@ final class TypeNormalizer
return $type;
}
if ($type instanceof ConstantArrayType && $arrayNesting === 1) {
return $type;
}
// first collection of types
if ($arrayNesting === 1) {
$this->collectedNestedArrayTypes = [];

View File

@ -10,7 +10,7 @@ use Rector\Core\Exception\Configuration\InvalidConfigurationException;
final class OutputFormatterCollector
{
/**
* @var OutputFormatterInterface[]
* @var array<string, OutputFormatterInterface>
*/
private array $outputFormatters = [];
@ -32,7 +32,7 @@ final class OutputFormatterCollector
}
/**
* @return int[]|string[]
* @return string[]
*/
public function getNames(): array
{

View File

@ -142,12 +142,9 @@ final class NodeFactory
*/
public function createArgs(array $values): array
{
$normalizedValues = [];
foreach ($values as $key => $value) {
$normalizedValues[$key] = $this->normalizeArgValue($value);
}
array_walk($values, fn ($value) => $this->normalizeArgValue($value));
return $this->builderFactory->args($normalizedValues);
return $this->builderFactory->args($values);
}
/**

View File

@ -34,6 +34,8 @@ use Rector\NodeTypeResolver\Node\AttributeKey;
/**
* @see \Rector\Core\Tests\PhpParser\Printer\BetterStandardPrinterTest
*
* @property array<string, array{string, bool, string, null}> $insertionMap
*/
final class BetterStandardPrinter extends Standard
{

View File

@ -105,7 +105,7 @@ final class FileDiff implements SerializableInterface
}
/**
* @return array<string, mixed>
* @return array{relative_file_path: string, diff: string, diff_console_formatted: string, rectors_with_line_changes: RectorWithLineChange[]}
*/
public function jsonSerialize(): array
{