classMethodReturnTypeResolver = $classMethodReturnTypeResolver; $this->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> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->isInTestCase($scope)) { return null; } /** @var Return_[] $returns */ $returns = $this->betterNodeFinder->findInstancesOfInFunctionLikeScoped($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; } if ($this->shouldSkip($returnExprType)) { return null; } $returnType = $this->classMethodReturnTypeResolver->resolve($node, $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; } private function shouldSkip(ConstantArrayType $constantArrayType) : bool { $keyType = $constantArrayType->getKeyType(); // empty array if ($keyType instanceof NeverType) { return \true; } $types = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; foreach ($types as $type) { if (!$type instanceof ConstantStringType) { continue; } $value = $type->getValue(); if (\trim($value) === '') { return \true; } if (StringUtils::isMatch($value, self::SKIPPED_CHAR_REGEX)) { return \true; } } $itemType = $constantArrayType->getItemType(); if ($itemType instanceof ConstantArrayType) { return $this->shouldSkip($itemType); } return \false; } /** * Skip test case, as return methods there are usually with test data only. * Those arrays are hand made and return types are getting complex and messy, so this rule should skip it. */ private function isInTestCase(Scope $scope) : bool { $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return \false; } return $classReflection->isSubclassOf('PHPUnit\\Framework\\TestCase'); } }