Skip to content

Commit d5284ce

Browse files
clxmstaabondrejmirtes
authored andcommitted
A bit more precise concatenation of unions of literal strings
1 parent 492dede commit d5284ce

File tree

3 files changed

+164
-44
lines changed

3 files changed

+164
-44
lines changed

src/Analyser/MutatingScope.php

Lines changed: 81 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,50 +1072,7 @@ private function resolveType(Expr $node): Type
10721072
}
10731073

10741074
if ($node instanceof Expr\BinaryOp\Concat || $node instanceof Expr\AssignOp\Concat) {
1075-
if ($node instanceof Node\Expr\AssignOp) {
1076-
$left = $node->var;
1077-
$right = $node->expr;
1078-
} else {
1079-
$left = $node->left;
1080-
$right = $node->right;
1081-
}
1082-
1083-
$leftStringType = $this->getType($left)->toString();
1084-
$rightStringType = $this->getType($right)->toString();
1085-
if (TypeCombinator::union(
1086-
$leftStringType,
1087-
$rightStringType,
1088-
) instanceof ErrorType) {
1089-
return new ErrorType();
1090-
}
1091-
1092-
if ($leftStringType instanceof ConstantStringType && $leftStringType->getValue() === '') {
1093-
return $rightStringType;
1094-
}
1095-
1096-
if ($rightStringType instanceof ConstantStringType && $rightStringType->getValue() === '') {
1097-
return $leftStringType;
1098-
}
1099-
1100-
if ($leftStringType instanceof ConstantStringType && $rightStringType instanceof ConstantStringType) {
1101-
return $leftStringType->append($rightStringType);
1102-
}
1103-
1104-
$accessoryTypes = [];
1105-
if ($leftStringType->isNonEmptyString()->or($rightStringType->isNonEmptyString())->yes()) {
1106-
$accessoryTypes[] = new AccessoryNonEmptyStringType();
1107-
}
1108-
1109-
if ($leftStringType->isLiteralString()->and($rightStringType->isLiteralString())->yes()) {
1110-
$accessoryTypes[] = new AccessoryLiteralStringType();
1111-
}
1112-
1113-
if (count($accessoryTypes) > 0) {
1114-
$accessoryTypes[] = new StringType();
1115-
return new IntersectionType($accessoryTypes);
1116-
}
1117-
1118-
return new StringType();
1075+
return $this->resolveConcatType($node);
11191076
}
11201077

11211078
if (
@@ -2680,6 +2637,86 @@ private function resolveType(Expr $node): Type
26802637
return new MixedType();
26812638
}
26822639

2640+
private function resolveConcatType(Expr\BinaryOp\Concat|Expr\AssignOp\Concat $node): Type
2641+
{
2642+
if ($node instanceof Node\Expr\AssignOp) {
2643+
$left = $node->var;
2644+
$right = $node->expr;
2645+
} else {
2646+
$left = $node->left;
2647+
$right = $node->right;
2648+
}
2649+
2650+
$leftStringType = $this->getType($left)->toString();
2651+
$rightStringType = $this->getType($right)->toString();
2652+
if (TypeCombinator::union(
2653+
$leftStringType,
2654+
$rightStringType,
2655+
) instanceof ErrorType) {
2656+
return new ErrorType();
2657+
}
2658+
2659+
if ($leftStringType instanceof ConstantStringType && $leftStringType->getValue() === '') {
2660+
return $rightStringType;
2661+
}
2662+
2663+
if ($rightStringType instanceof ConstantStringType && $rightStringType->getValue() === '') {
2664+
return $leftStringType;
2665+
}
2666+
2667+
if ($leftStringType instanceof ConstantStringType && $rightStringType instanceof ConstantStringType) {
2668+
return $leftStringType->append($rightStringType);
2669+
}
2670+
2671+
// we limit the number of union-types for performance reasons
2672+
if ($leftStringType instanceof UnionType && count($leftStringType->getTypes()) <= 16 && $rightStringType instanceof ConstantStringType) {
2673+
$constantStrings = TypeUtils::getConstantStrings($leftStringType);
2674+
if (count($constantStrings) > 0) {
2675+
$strings = [];
2676+
foreach ($constantStrings as $constantString) {
2677+
if ($constantString->getValue() === '') {
2678+
$strings[] = $rightStringType;
2679+
2680+
continue;
2681+
}
2682+
$strings[] = $constantString->append($rightStringType);
2683+
}
2684+
return TypeCombinator::union(...$strings);
2685+
}
2686+
}
2687+
if ($rightStringType instanceof UnionType && count($rightStringType->getTypes()) <= 16 && $leftStringType instanceof ConstantStringType) {
2688+
$constantStrings = TypeUtils::getConstantStrings($rightStringType);
2689+
if (count($constantStrings) > 0) {
2690+
$strings = [];
2691+
foreach ($constantStrings as $constantString) {
2692+
if ($constantString->getValue() === '') {
2693+
$strings[] = $leftStringType;
2694+
2695+
continue;
2696+
}
2697+
$strings[] = $leftStringType->append($constantString);
2698+
}
2699+
return TypeCombinator::union(...$strings);
2700+
}
2701+
}
2702+
2703+
$accessoryTypes = [];
2704+
if ($leftStringType->isNonEmptyString()->or($rightStringType->isNonEmptyString())->yes()) {
2705+
$accessoryTypes[] = new AccessoryNonEmptyStringType();
2706+
}
2707+
2708+
if ($leftStringType->isLiteralString()->and($rightStringType->isLiteralString())->yes()) {
2709+
$accessoryTypes[] = new AccessoryLiteralStringType();
2710+
}
2711+
2712+
if (count($accessoryTypes) > 0) {
2713+
$accessoryTypes[] = new StringType();
2714+
return new IntersectionType($accessoryTypes);
2715+
}
2716+
2717+
return new StringType();
2718+
}
2719+
26832720
private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type
26842721
{
26852722
if ($expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) {

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,7 @@ public function dataFileAsserts(): iterable
791791
}
792792

793793
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php');
794+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6439.php');
794795
}
795796

796797
/**
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Bug6439;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
8+
class HelloWorld
9+
{
10+
public function unionOnLeft(string $name, ?int $gesperrt = null, ?int $adaid = null):void
11+
{
12+
$string = 'general';
13+
if (null !== $gesperrt) {
14+
$string = $string . ' branch-a';
15+
}
16+
assertType("'general'|'general branch-a'", $string);
17+
if (null !== $adaid) {
18+
$string = $string . ' branch-b';
19+
}
20+
assertType("'general'|'general branch-a'|'general branch-a branch-b'|'general branch-b'", $string);
21+
}
22+
23+
public function unionOnRight(string $name, ?int $gesperrt = null, ?int $adaid = null):void
24+
{
25+
$string = 'general';
26+
if (null !== $gesperrt) {
27+
$string = 'branch-a ' . $string;
28+
}
29+
assertType("'branch-a general'|'general'", $string);
30+
if (null !== $adaid) {
31+
$string = 'branch-b ' . $string;
32+
}
33+
assertType("'branch-a general'|'branch-b branch-a general'|'branch-b general'|'general'", $string);
34+
}
35+
36+
/**
37+
* @param '1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'10'|'11'|'12'|'13'|'14'|'15' $s15
38+
* @param '1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'10'|'11'|'12'|'13'|'14'|'15'|'16' $s16
39+
* @param '1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'10'|'11'|'12'|'13'|'14'|'15'|'16'|'17' $s17
40+
*/
41+
public function testLimit(string $s15, string $s16, string $s17) {
42+
if (rand(0,1)) {
43+
// doubles the number of elements
44+
$s15 .= 'a';
45+
$s16 .= 'a';
46+
$s17 .= 'a';
47+
}
48+
// union should contain 30 elements
49+
assertType("'1'|'10'|'10a'|'11'|'11a'|'12'|'12a'|'13'|'13a'|'14'|'14a'|'15'|'15a'|'1a'|'2'|'2a'|'3'|'3a'|'4'|'4a'|'5'|'5a'|'6'|'6a'|'7'|'7a'|'8'|'8a'|'9'|'9a'", $s15);
50+
// union should contain 32 elements
51+
assertType("'1'|'10'|'10a'|'11'|'11a'|'12'|'12a'|'13'|'13a'|'14'|'14a'|'15'|'15a'|'16'|'16a'|'1a'|'2'|'2a'|'3'|'3a'|'4'|'4a'|'5'|'5a'|'6'|'6a'|'7'|'7a'|'8'|'8a'|'9'|'9a'", $s16);
52+
// fallback to the more general form
53+
assertType("literal-string&non-empty-string", $s17);
54+
}
55+
56+
/**
57+
* @param '1'|'2' $s2
58+
*/
59+
public function appendEmpty($s2) {
60+
if (rand(0,1)) {
61+
$s2 .= '';
62+
}
63+
assertType("'1'|'2'", $s2);
64+
65+
if (rand(0,1)) {
66+
$s2 = '';
67+
}
68+
assertType("''|'1'|'2'", $s2);
69+
}
70+
71+
public function concatCase() {
72+
$extra = '';
73+
if (rand(0,1)) {
74+
$extra = '[0-9]';
75+
}
76+
77+
assertType("''|'[0-9]'", $extra);
78+
79+
$regex = '~[A-Z]' . $extra . '~';
80+
assertType("'~[A-Z][0-9]~'|'~[A-Z]~'", $regex);
81+
}
82+
}

0 commit comments

Comments
 (0)