Skip to content

Commit 3d11be9

Browse files
committed
Do not parenthesize a benevolent union that describes as a single type
The members of a benevolent union can describe the same at a verbosity level, like decimal-int-string and non-empty-string&uppercase-string as string at the type-only level. The union then described as "(string)", for example in "Parameter #1 $num of function ceil expects float|int, (string) given." for a string cast of a benevolent (float|int). The parentheses now only wrap a description that has a "|". Amp-Thread-ID: https://ampcode.com/threads/T-01a19770-5331-74b2-963f-e324ae0f7635
1 parent f839004 commit 3d11be9

5 files changed

Lines changed: 53 additions & 5 deletions

File tree

‎src/Type/BenevolentUnionType.php‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Turbo\ShadowedByTurboExtension;
77
use PHPStan\Type\Generic\TemplateTypeMap;
88
use function count;
9+
use function str_contains;
910

1011
/** @api */
1112
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/BenevolentUnionType.cpp')]
@@ -43,7 +44,14 @@ public function tryRemove(Type $typeToRemove): ?Type
4344

4445
public function describe(VerbosityLevel $level): string
4546
{
46-
return '(' . parent::describe($level) . ')';
47+
$description = parent::describe($level);
48+
49+
// the members can describe the same at this level, like decimal-int-string|non-empty-string as string
50+
if (!str_contains($description, '|')) {
51+
return $description;
52+
}
53+
54+
return '(' . $description . ')';
4755
}
4856

4957
protected function unionTypes(callable $getType): Type

‎tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public function testCallToFunctionWithDynamicIterableParameters(): void
186186
$this->analyse([__DIR__ . '/data/function-with-variadic-parameters-7.1.php'], [
187187
[
188188
'Parameter #2 ...$foo of function FunctionWithVariadicParameters\foo expects int, string given.',
189-
16,
189+
18,
190190
],
191191
]);
192192
}
@@ -1989,7 +1989,7 @@ public function testBug10626(): void
19891989
$this->analyse([__DIR__ . '/data/bug-10626.php'], [
19901990
[
19911991
'Parameter #1 $value of function Bug10626\intByValue expects int, string given.',
1992-
16,
1992+
18,
19931993
],
19941994
[
19951995
'Parameter #1 $value of function Bug10626\intByReference expects int, string given.',
@@ -3450,4 +3450,19 @@ public function testFloatRangeArgument(): void
34503450
]);
34513451
}
34523452

3453+
#[RequiresPhp('>= 8.0.0')]
3454+
public function testBenevolentUnionDescription(): void
3455+
{
3456+
$this->analyse([__DIR__ . '/data/benevolent-union-description.php'], [
3457+
[
3458+
'Parameter #1 $num of function BenevolentUnionDescription\\takesNumber expects float|int, string given.',
3459+
18,
3460+
],
3461+
[
3462+
'Parameter #1 $s of function BenevolentUnionDescription\\takesString expects string, (float|int) given.',
3463+
19,
3464+
],
3465+
]);
3466+
}
3467+
34533468
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace BenevolentUnionDescription;
6+
7+
function takesNumber(float|int $num): void
8+
{
9+
}
10+
11+
function takesString(string $s): void
12+
{
13+
}
14+
15+
function doFoo(int|float $number, int $scale): void
16+
{
17+
$coefficient = 10 ** $scale;
18+
takesNumber((string) ($number * $coefficient));
19+
takesString($coefficient);
20+
}

‎turbo-ext/src/BenevolentUnionType.cpp‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,17 @@ class BenevolentUnionType
108108
return toBenevolent(result.raw());
109109
}
110110

111-
/* '(' . parent::describe($level) . ')' */
111+
/* '(' . parent::describe($level) . ')', unwrapped when it has no '|'
112+
* because the members describe the same at this level */
112113
zv::Val describe(zval *level) const
113114
{
114115
zv::Val inner = pt_union_type_describe(self, level);
115116
if (UNEXPECTED(inner.isUndef())) return zv::Val();
117+
zend_string *innerString = zv::Ref(inner.raw()).asString();
118+
if (memchr(ZSTR_VAL(innerString), '|', ZSTR_LEN(innerString)) == NULL) return inner;
116119
smart_str description = {NULL, 0};
117120
smart_str_appendc(&description, '(');
118-
smart_str_append(&description, zv::Ref(inner.raw()).asString());
121+
smart_str_append(&description, innerString);
119122
smart_str_appendc(&description, ')');
120123
smart_str_0(&description);
121124
return zv::Val::adoptString(description.s);

‎turbo-ext/tests/type-family.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3645,6 +3645,8 @@ public function getValuesArray(): \PHPStan\Type\Constant\ConstantArrayType
36453645
'benevolentArrays' => new $benevolentClass([new \PHPStan\Type\ArrayType(new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()), $compoundConstShape(['a' => new \PHPStan\Type\IntegerType()])]),
36463646
'benevolentConsts' => new $benevolentClass([new \PHPStan\Type\Constant\ConstantIntegerType(1), new \PHPStan\Type\Constant\ConstantStringType('a')]),
36473647
'benevolentObjects' => new $benevolentClass([new \PHPStan\Type\ObjectType(\stdClass::class), new \PHPStan\Type\StringType()]),
3648+
// both members describe as string at the type-only level
3649+
'benevolentStrings' => new $benevolentClass([new $intersectionClass([new \PHPStan\Type\StringType(), new \PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType()]), new $intersectionClass([new \PHPStan\Type\StringType(), new \PHPStan\Type\Accessory\AccessoryNonEmptyStringType(), new \PHPStan\Type\Accessory\AccessoryUppercaseStringType()])]),
36483650
'templateUnion' => \PHPStan\Type\Generic\TemplateTypeFactory::create($compoundScope, 'U', new $unionClass([new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()]), \PHPStan\Type\Generic\TemplateTypeVariance::createInvariant()),
36493651
'templateBenevolentUnion' => \PHPStan\Type\Generic\TemplateTypeFactory::create($compoundScope, 'B', new $benevolentClass([new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()]), \PHPStan\Type\Generic\TemplateTypeVariance::createInvariant()),
36503652
'nonEmptyString' => new $intersectionClass([new \PHPStan\Type\StringType(), new \PHPStan\Type\Accessory\AccessoryNonEmptyStringType()]),

0 commit comments

Comments
 (0)