Skip to content
10 changes: 0 additions & 10 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ parameters:
count: 1
path: src/Collectors/Collector.php

-
message: "#^Binary operation \"\\+\" between array\\{class\\-string\\<TNodeType of PhpParser\\\\Node\\>\\} and array\\<string, class\\-string\\>\\|false results in an error\\.$#"
count: 1
path: src/Collectors/Registry.php

-
message: "#^Method PHPStan\\\\Collectors\\\\Registry\\:\\:__construct\\(\\) has parameter \\$collectors with generic interface PHPStan\\\\Collectors\\\\Collector but does not specify its types\\: TNodeType, TValue$#"
count: 1
Expand Down Expand Up @@ -307,11 +302,6 @@ parameters:
count: 1
path: src/Reflection/SignatureMap/Php8SignatureMapProvider.php

-
message: "#^Binary operation \"\\+\" between array\\{class\\-string\\<TNodeType of PhpParser\\\\Node\\>\\} and array\\<string, class\\-string\\>\\|false results in an error\\.$#"
count: 1
path: src/Rules/Registry.php

-
message: "#^Method PHPStan\\\\Rules\\\\Registry\\:\\:__construct\\(\\) has parameter \\$rules with generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#"
count: 1
Expand Down
41 changes: 40 additions & 1 deletion src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,10 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback):
$leftType = $getTypeCallback($left);
$rightType = $getTypeCallback($right);

if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
return new NeverType();
}

$leftTypes = TypeUtils::getConstantScalars($leftType);
$rightTypes = TypeUtils::getConstantScalars($rightType);
$leftTypesCount = count($leftTypes);
Expand Down Expand Up @@ -921,7 +925,9 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback):
return TypeCombinator::union(...$resultTypes);
}

if ($leftType->isArray()->yes() && $rightType->isArray()->yes()) {
$leftIsArray = $leftType->isArray();
$rightIsArray = $rightType->isArray();
if ($leftIsArray->yes() && $rightIsArray->yes()) {
if ($leftType->getIterableKeyType()->equals($rightType->getIterableKeyType())) {
// to preserve BenevolentUnionType
$keyType = $leftType->getIterableKeyType();
Expand Down Expand Up @@ -955,6 +961,39 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback):
]);
}

if (
($leftIsArray->yes() && $rightIsArray->no())
|| ($leftIsArray->no() && $rightIsArray->yes())
) {
return new ErrorType();
}

if (
($leftIsArray->yes() && $rightIsArray->maybe())
|| ($leftIsArray->maybe() && $rightIsArray->yes())
) {
$resultType = new ArrayType(new MixedType(), new MixedType());
if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) {
return TypeCombinator::intersect($resultType, new NonEmptyArrayType());
}

return $resultType;
}

if ($leftIsArray->maybe() && $rightIsArray->maybe()) {
$plusable = new UnionType([
new StringType(),
new FloatType(),
new IntegerType(),
new ArrayType(new MixedType(), new MixedType()),
new BooleanType(),
]);

if ($plusable->isSuperTypeOf($leftType)->yes() && $plusable->isSuperTypeOf($rightType)->yes()) {
return TypeCombinator::union($leftType, $rightType);
}
}

return $this->resolveCommonMath(new BinaryOp\Plus($left, $right), $leftType, $rightType);
}

Expand Down
42 changes: 41 additions & 1 deletion tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2696,9 +2696,49 @@ public function dataBinaryOperations(): array
'$mixed - $mixed',
],
[
'*ERROR*',
'array',
'$mixed + []',
],
[
'array|int',
'$intOrArray + $intOrArray',
],
[
'float|int',
'$intOrFloat + $intOrFloat',
],
[
'array|float',
'$floatOrArray + $floatOrArray',
],
[
'array|bool|float|int|string',
'$plusable + $plusable',
],
[
'array',
'$mixedNoFloat + []',
],
[
'(float|int)',
'$mixedNoFloat + 5',
],
[
'(float|int)',
'$mixedNoInt + 5',
],
[
'*ERROR*',
'$mixedNoArray + []',
],
[
'*ERROR*',
'$mixedNoArrayOrInt + []',
],
[
'*ERROR*',
'$integer + []',
],
[
'124',
'1 + "123"',
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-constant.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-array-bug.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/tagged-unions.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7492.php');
}

/**
Expand Down
27 changes: 27 additions & 0 deletions tests/PHPStan/Analyser/data/binary.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,33 @@ public function doFoo(array $generalArray)
$severalSumWithStaticConst2 = 1 + static::INT_CONST + 1;
$severalSumWithStaticConst3 = 1 + 1 + static::INT_CONST;

if (!is_array($mixed)) {
$mixedNoArray = $mixed;
}
if (!is_int($mixed)) {
$mixedNoInt = $mixed;
}
if (!is_float($mixed)) {
$mixedNoFloat = $mixed;
}
if (!is_array($mixed)) {
if (!is_int($mixed)) {
$mixedNoArrayOrInt = $mixed;
}
}

/** @var int|array $intOrArray */
$intOrArray = doFoo();

/** @var array|float $floatOrArray */
$floatOrArray = doFoo();

/** @var int|float $intOrFloat */
$intOrFloat = doFoo();

/** @var array|float|int|string|bool $plusable */
$plusable = doFoo();

die;
}

Expand Down
14 changes: 14 additions & 0 deletions tests/PHPStan/Analyser/data/bug-7492.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1);

namespace Bug7492;

use function PHPStan\Testing\assertType;

class HelloWorld
{
public function sayHello(array $config): void
{
$x = ($config['db'][1] ?? []) + ['host' => '', 'login' => '', 'password' => '', 'name' => ''];
assertType('non-empty-array', $x);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ public function testRule(): void
'Binary operation "/" between 10 and literal-string results in an error.',
222,
],
[
'Binary operation "+" between int and array{} results in an error.',
259,
],
]);
}

Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Rules/Operators/data/invalid-binary.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,7 @@ function benevolentPlus(array $a, int $i): void {
echo $k + $i;
}
};

function (int $int) {
$int + [];
};