diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index 6d082088072..c7884921984 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -24,6 +24,10 @@ use Psalm\Type\Atomic\TString; use Psalm\Type\Union; +use function array_map; +use function array_merge; +use function array_unique; +use function count; use function in_array; /** @@ -31,6 +35,8 @@ */ final class EncapsulatedStringAnalyzer { + private const MAX_LITERALS = 500; + public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Scalar\Encapsed $stmt, @@ -43,7 +49,7 @@ public static function analyze( $all_literals = true; - $literal_string = ""; + $literal_strings = []; foreach ($stmt->parts as $part) { if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) { @@ -51,8 +57,8 @@ public static function analyze( } if ($part instanceof EncapsedStringPart) { - if ($literal_string !== null) { - $literal_string .= $part->value; + if ($literal_strings !== null) { + $literal_strings = self::combineLiteral($literal_strings, $part->value); } $non_falsy = $non_falsy || $part->value; $non_empty = $non_empty || $part->value !== ""; @@ -118,11 +124,22 @@ public static function analyze( } } - if ($literal_string !== null) { - if ($casted_part_type->isSingleLiteral()) { - $literal_string .= $casted_part_type->getSingleLiteral()->value; + if ($literal_strings !== null) { + if ($casted_part_type->allSpecificLiterals()) { + $new_literal_strings = []; + foreach ($casted_part_type->getLiteralStrings() as $literal_string_atomic) { + $new_literal_strings = array_merge( + $new_literal_strings, + self::combineLiteral($literal_strings, $literal_string_atomic->value), + ); + } + + $literal_strings = array_unique($new_literal_strings); + if (count($literal_strings) > self::MAX_LITERALS) { + $literal_strings = null; + } } else { - $literal_string = null; + $literal_strings = null; } } @@ -156,14 +173,14 @@ public static function analyze( } } else { $all_literals = false; - $literal_string = null; + $literal_strings = null; } } if ($non_empty || $non_falsy) { - if ($literal_string !== null) { + if ($literal_strings !== null) { $stmt_type = new Union( - [Type::getAtomicStringFromLiteral($literal_string)], + array_map([Type::class, 'getAtomicStringFromLiteral'], $literal_strings), ['parent_nodes' => $parent_nodes], ); } elseif ($all_literals) { @@ -198,4 +215,28 @@ public static function analyze( return true; } + + /** + * @param string[] $literal_strings + * @return non-empty-list + */ + private static function combineLiteral( + array $literal_strings, + string $append + ): array { + if ($literal_strings === []) { + return [$append]; + } + + if ($append === '') { + return $literal_strings; + } + + $new_literal_strings = array(); + foreach ($literal_strings as $literal_string) { + $new_literal_strings[] = "{$literal_string}{$append}"; + } + + return $new_literal_strings; + } } diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 9f5b4171373..d9b97785c5a 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -977,6 +977,39 @@ function foo(int $s1): string { return "Hello $s1 $s2"; }', ], + 'encapsedWithUnionLiteralsKeepsLiteral' => [ + 'code' => ' ['$encapsed===' => "'0'|'2'"], + ], + 'encapsedWithUnionLiteralsKeepsLiteral2' => [ + 'code' => ' ['$encapsed===' => "'XaYbyeZ'|'XaYhelloZ'|'XaYworldZ'|'XbYbyeZ'|'XbYhelloZ'|'XbYworldZ'"], + ], + 'encapsedWithIntsKeepsLiteral' => [ + 'code' => ' ['$encapsed===' => "'a0'|'a1'|'a2'|'b0'|'b1'|'b2'"], + ], + 'encapsedWithIntRangeKeepsLiteral' => [ + 'code' => ' $b + */ + $encapsed = "{$a}{$b}";', + 'assertions' => ['$encapsed===' => "'a0'|'a1'|'a2'|'b0'|'b1'|'b2'"], + ], 'NumericStringIncrement' => [ 'code' => '