From dd092dd413e5399888795c6920f4de03e5b0eb63 Mon Sep 17 00:00:00 2001 From: schlndh Date: Tue, 6 Dec 2022 18:34:38 +0100 Subject: [PATCH] Improve constant string union handling for concat and encapsed string --- src/Analyser/MutatingScope.php | 62 ++++--------------- .../InitializerExprTypeResolver.php | 54 +++++++++------- .../Analyser/LegacyNodeScopeResolverTest.php | 6 +- .../Analyser/NodeScopeResolverTest.php | 2 +- ...ug-6439.php => constant-string-unions.php} | 26 +++++++- 5 files changed, 70 insertions(+), 80 deletions(-) rename tests/PHPStan/Analyser/data/{bug-6439.php => constant-string-unions.php} (76%) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f1ea0a4e7ca..5d825f00a7c 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -66,8 +66,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -1091,63 +1089,25 @@ private function resolveType(Expr $node): Type } elseif ($node instanceof String_) { return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Node\Scalar\Encapsed) { - $parts = []; - foreach ($node->parts as $part) { - if ($part instanceof EncapsedStringPart) { - $parts[] = new ConstantStringType($part->value); - continue; - } + $resultType = null; - $partStringType = $this->getType($part)->toString(); - if ($partStringType instanceof ErrorType) { - return new ErrorType(); - } - - $parts[] = $partStringType; - } + foreach ($node->parts as $part) { + $partType = $part instanceof EncapsedStringPart + ? new ConstantStringType($part->value) + : $this->getType($part); + if ($resultType === null) { + $resultType = $partType; - $constantString = new ConstantStringType(''); - foreach ($parts as $part) { - if ($part instanceof ConstantStringType) { - $constantString = $constantString->append($part); continue; } - $isNonEmpty = false; - $isNonFalsy = false; - $isLiteralString = true; - foreach ($parts as $partType) { - if ($partType->isNonFalsyString()->yes()) { - $isNonFalsy = true; - } - if ($partType->isNonEmptyString()->yes()) { - $isNonEmpty = true; - } - if ($partType->isLiteralString()->yes()) { - continue; - } - $isLiteralString = false; - } - - $accessoryTypes = []; - if ($isNonFalsy === true) { - $accessoryTypes[] = new AccessoryNonFalsyStringType(); - } elseif ($isNonEmpty === true) { - $accessoryTypes[] = new AccessoryNonEmptyStringType(); + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); + if (! $resultType instanceof ConstantStringType && ! $resultType instanceof UnionType) { + return $resultType; } - - if ($isLiteralString === true) { - $accessoryTypes[] = new AccessoryLiteralStringType(); - } - if (count($accessoryTypes) > 0) { - $accessoryTypes[] = new StringType(); - return new IntersectionType($accessoryTypes); - } - - return new StringType(); } - return $constantString; + return $resultType ?? new ConstantStringType(''); } elseif ($node instanceof DNumber) { return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index b3d49df16e1..0314fb3e4c5 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -62,6 +62,7 @@ use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use function array_keys; +use function array_merge; use function count; use function dirname; use function in_array; @@ -353,8 +354,16 @@ public function getType(Expr $expr, InitializerExprContext $context): Type */ public function getConcatType(Expr $left, Expr $right, callable $getTypeCallback): Type { - $leftStringType = $getTypeCallback($left)->toString(); - $rightStringType = $getTypeCallback($right)->toString(); + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + return $this->resolveConcatType($leftType, $rightType); + } + + public function resolveConcatType(Type $left, Type $right): Type + { + $leftStringType = $left->toString(); + $rightStringType = $right->toString(); if (TypeCombinator::union( $leftStringType, $rightStringType, @@ -374,34 +383,33 @@ public function getConcatType(Expr $left, Expr $right, callable $getTypeCallback return $leftStringType->append($rightStringType); } + $leftConstantStrings = TypeUtils::getConstantStrings($leftStringType); + $rightConstantStrings = TypeUtils::getConstantStrings($rightStringType); + $combinedConstantStringsCount = count($leftConstantStrings) * count($rightConstantStrings); + // we limit the number of union-types for performance reasons - if ($leftStringType instanceof UnionType && count($leftStringType->getTypes()) <= 16 && $rightStringType instanceof ConstantStringType) { - $constantStrings = TypeUtils::getConstantStrings($leftStringType); - if (count($constantStrings) > 0) { - $strings = []; - foreach ($constantStrings as $constantString) { - if ($constantString->getValue() === '') { - $strings[] = $rightStringType; + if ($combinedConstantStringsCount > 0 && $combinedConstantStringsCount <= 16) { + $strings = []; - continue; - } - $strings[] = $constantString->append($rightStringType); + foreach ($leftConstantStrings as $leftConstantString) { + if ($leftConstantString->getValue() === '') { + $strings = array_merge($strings, $rightConstantStrings); + + continue; } - return TypeCombinator::union(...$strings); - } - } - if ($rightStringType instanceof UnionType && count($rightStringType->getTypes()) <= 16 && $leftStringType instanceof ConstantStringType) { - $constantStrings = TypeUtils::getConstantStrings($rightStringType); - if (count($constantStrings) > 0) { - $strings = []; - foreach ($constantStrings as $constantString) { - if ($constantString->getValue() === '') { - $strings[] = $leftStringType; + + foreach ($rightConstantStrings as $rightConstantString) { + if ($rightConstantString->getValue() === '') { + $strings[] = $leftConstantString; continue; } - $strings[] = $leftStringType->append($constantString); + + $strings[] = $leftConstantString->append($rightConstantString); } + } + + if (count($strings) > 0) { return TypeCombinator::union(...$strings); } } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 57d4637bc32..a9381a6dbd3 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2996,15 +2996,15 @@ public function dataBinaryOperations(): array '$decrementedFooString', ], [ - 'literal-string&non-falsy-string', + "'barbar'|'barfoo'|'foobar'|'foofoo'", '$conditionalString . $conditionalString', ], [ - 'literal-string&non-falsy-string', + "'baripsum'|'barlorem'|'fooipsum'|'foolorem'", '$conditionalString . $anotherConditionalString', ], [ - 'literal-string&non-falsy-string', + "'ipsumbar'|'ipsumfoo'|'lorembar'|'loremfoo'", '$anotherConditionalString . $conditionalString', ], [ diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index e7b187dbce8..8c16e69bccd 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -810,7 +810,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/weird-strlen-cases.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6439.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-string-unions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6748.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys.php'); diff --git a/tests/PHPStan/Analyser/data/bug-6439.php b/tests/PHPStan/Analyser/data/constant-string-unions.php similarity index 76% rename from tests/PHPStan/Analyser/data/bug-6439.php rename to tests/PHPStan/Analyser/data/constant-string-unions.php index ed72a284d10..a8db3d1dc6e 100644 --- a/tests/PHPStan/Analyser/data/bug-6439.php +++ b/tests/PHPStan/Analyser/data/constant-string-unions.php @@ -1,10 +1,10 @@