arrayTypeAnalyzer = $arrayTypeAnalyzer;
$this->phpVersionProvider = $phpVersionProvider;
}
public function getRuleDefinition() : RuleDefinition
{
return new RuleDefinition('Change array_merge() to spread operator', [new CodeSample(<<<'CODE_SAMPLE'
class SomeClass
{
public function run($iter1, $iter2)
{
$values = array_merge(iterator_to_array($iter1), iterator_to_array($iter2));
// Or to generalize to all iterables
$anotherValues = array_merge(
is_array($iter1) ? $iter1 : iterator_to_array($iter1),
is_array($iter2) ? $iter2 : iterator_to_array($iter2)
);
}
}
CODE_SAMPLE
, <<<'CODE_SAMPLE'
class SomeClass
{
public function run($iter1, $iter2)
{
$values = [...$iter1, ...$iter2];
// Or to generalize to all iterables
$anotherValues = [...$iter1, ...$iter2];
}
}
CODE_SAMPLE
)]);
}
/**
* @return array>
*/
public function getNodeTypes() : array
{
return [FuncCall::class];
}
/**
* @param FuncCall $node
*/
public function refactor(Node $node) : ?Node
{
if ($this->isName($node, 'array_merge')) {
return $this->refactorArray($node);
}
return null;
}
public function provideMinPhpVersion() : int
{
return PhpVersionFeature::ARRAY_SPREAD;
}
private function refactorArray(FuncCall $funcCall) : ?Array_
{
if ($funcCall->isFirstClassCallable()) {
return null;
}
$array = new Array_();
foreach ($funcCall->args as $arg) {
if (!$arg instanceof Arg) {
continue;
}
// cannot handle unpacked arguments
if ($arg->unpack) {
return null;
}
$value = $arg->value;
if ($this->shouldSkipArrayForInvalidTypeOrKeys($value)) {
return null;
}
if ($value instanceof Array_) {
$array->items = \array_merge($array->items, $value->items);
continue;
}
$value = $this->resolveValue($value);
$array->items[] = $this->createUnpackedArrayItem($value);
}
return $array;
}
private function shouldSkipArrayForInvalidTypeOrKeys(Expr $expr) : bool
{
// we have no idea what it is → cannot change it
if (!$this->arrayTypeAnalyzer->isArrayType($expr)) {
return \true;
}
$arrayStaticType = $this->getType($expr);
if (!$arrayStaticType instanceof ArrayType) {
return \true;
}
return !$this->isArrayKeyTypeAllowed($arrayStaticType);
}
private function isArrayKeyTypeAllowed(ArrayType $arrayType) : bool
{
if ($arrayType->getKeyType()->isInteger()->yes()) {
return \true;
}
// php 8.1+ allow mixed key: int, string, and null
return $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::ARRAY_SPREAD_STRING_KEYS);
}
private function resolveValue(Expr $expr) : Expr
{
if ($expr instanceof FuncCall && $this->isIteratorToArrayFuncCall($expr)) {
/** @var Arg $arg */
$arg = $expr->args[0];
/** @var FuncCall $expr */
$expr = $arg->value;
}
if (!$expr instanceof Ternary) {
return $expr;
}
if (!$expr->cond instanceof FuncCall) {
return $expr;
}
if (!$this->isName($expr->cond, 'is_array')) {
return $expr;
}
if ($expr->if instanceof Variable && $this->isIteratorToArrayFuncCall($expr->else)) {
return $expr->if;
}
return $expr;
}
private function createUnpackedArrayItem(Expr $expr) : ArrayItem
{
return new ArrayItem($expr, null, \false, [], \true);
}
private function isIteratorToArrayFuncCall(Expr $expr) : bool
{
if (!$expr instanceof FuncCall) {
return \false;
}
if (!$this->nodeNameResolver->isName($expr, 'iterator_to_array')) {
return \false;
}
if ($expr->isFirstClassCallable()) {
return \false;
}
return isset($expr->getArgs()[0]);
}
}