diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index b38c19e95..a5c2ba130 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -146,10 +146,8 @@ class ConcatAnalyzer // type is all string or int literals, combine them into new literal(s). $literal_concat = false; - if ((($left_type->isSingleStringLiteral() || $left_type->isSingleIntLiteral()) - && ($right_type->allStringLiterals() || $right_type->allIntLiterals())) - || (($right_type->isSingleStringLiteral() || $right_type->isSingleIntLiteral()) - && ($left_type->allStringLiterals() || $left_type->allIntLiterals())) + if (($left_type->allStringLiterals() || $left_type->allIntLiterals()) + && ($right_type->allStringLiterals() || $right_type->allIntLiterals()) ) { $literal_concat = true; $result_type_parts = []; @@ -170,7 +168,7 @@ class ConcatAnalyzer } if (!empty($result_type_parts)) { - if ($literal_concat) { + if ($literal_concat && count($result_type_parts) < 64) { $result_type = new Type\Union($result_type_parts); } else { $result_type = new Type\Union([new Type\Atomic\TNonEmptyNonspecificLiteralString]); @@ -189,27 +187,30 @@ class ConcatAnalyzer $left_type, $numeric_type ); - $right_uint = Type::getPositiveInt(); - $right_uint->addType(new Type\Atomic\TLiteralInt(0)); - $right_is_uint = UnionTypeComparator::isContainedBy( - $codebase, - $right_type, - $right_uint - ); - if ($left_is_numeric && $right_is_uint) { - $result_type = Type::getNumericString(); - return; + if ($left_is_numeric) { + $right_uint = Type::getPositiveInt(); + $right_uint->addType(new Type\Atomic\TLiteralInt(0)); + $right_is_uint = UnionTypeComparator::isContainedBy( + $codebase, + $right_type, + $right_uint + ); + + if ($right_is_uint) { + $result_type = Type::getNumericString(); + return; + } } $lowercase_type = clone $numeric_type; $lowercase_type->addType(new Type\Atomic\TLowercaseString()); - $left_is_lowercase = UnionTypeComparator::isContainedBy( + + $all_lowercase = UnionTypeComparator::isContainedBy( $codebase, $left_type, $lowercase_type - ); - $right_is_lowercase = UnionTypeComparator::isContainedBy( + ) && UnionTypeComparator::isContainedBy( $codebase, $right_type, $lowercase_type @@ -217,34 +218,31 @@ class ConcatAnalyzer $non_empty_string = Type::getNonEmptyString(); - $left_is_non_empty = UnionTypeComparator::isContainedBy( + $has_non_empty = UnionTypeComparator::isContainedBy( $codebase, $left_type, $non_empty_string - ); - $right_is_non_empty = UnionTypeComparator::isContainedBy( + ) || UnionTypeComparator::isContainedBy( $codebase, $right_type, $non_empty_string ); - if ($left_is_lowercase && $right_is_lowercase) { - $result_type = $left_is_non_empty || $right_is_non_empty - ? Type::getNonEmptyLowercaseString() - : Type::getLowercaseString(); + $all_literals = $left_type->allLiterals() && $right_type->allLiterals(); - return; - } - - if ($left_is_non_empty || $right_is_non_empty) { - if ($left_type->allLiterals() && $right_type->allLiterals()) { + if ($has_non_empty) { + if ($all_literals) { $result_type = new Type\Union([new Type\Atomic\TNonEmptyNonspecificLiteralString]); + } elseif ($all_lowercase) { + $result_type = Type::getNonEmptyLowercaseString(); } else { $result_type = Type::getNonEmptyString(); } } else { - if ($left_type->allLiterals() && $right_type->allLiterals()) { + if ($all_literals) { $result_type = new Type\Union([new Type\Atomic\TNonspecificLiteralString]); + } elseif ($all_lowercase) { + $result_type = Type::getLowercaseString(); } else { $result_type = Type::getString(); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 3a5f0c659..4930f654d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -372,6 +372,8 @@ class CastAnalyzer ) { if ($atomic_type instanceof Type\Atomic\TLiteralInt) { $castable_types[] = new Type\Atomic\TLiteralString((string) $atomic_type->value); + } elseif ($atomic_type instanceof Type\Atomic\TNonspecificLiteralInt) { + $castable_types[] = new Type\Atomic\TNonspecificLiteralString(); } else { $castable_types[] = new Type\Atomic\TNumericString(); } diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 7261dae50..c5737e76f 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -28,6 +28,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString; use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonFalsyString; +use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; @@ -94,8 +95,27 @@ class ScalarTypeComparator } if ($container_type_part instanceof TNonspecificLiteralString) { - if ($atomic_comparison_result) { - $atomic_comparison_result->type_coerced = true; + if ($input_type_part instanceof TString) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; + } + } + + return false; + } + + if ($container_type_part instanceof TNonspecificLiteralInt + && ($input_type_part instanceof TLiteralInt + || $input_type_part instanceof TNonspecificLiteralInt) + ) { + return true; + } + + if ($container_type_part instanceof TNonspecificLiteralInt) { + if ($input_type_part instanceof TInt) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; + } } return false; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index eb103eac8..da115c2a8 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -38,6 +38,7 @@ use Psalm\Type\Atomic\TNonEmptyLowercaseString; use Psalm\Type\Atomic\TNonEmptyMixed; use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonFalsyString; +use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumericString; @@ -896,292 +897,24 @@ class TypeCombiner } if ($type instanceof TString) { - if ($type instanceof TCallableString && isset($combination->value_types['callable'])) { - return null; - } - - if (isset($combination->value_types['array-key'])) { - return null; - } - - if ($type instanceof Type\Atomic\TTemplateParamClass) { - $combination->value_types[$type_key] = $type; - } elseif ($type instanceof Type\Atomic\TClassString) { - if (!$type->as_type) { - $combination->class_string_types['object'] = new TObject(); - } else { - $combination->class_string_types[$type->as] = $type->as_type; - } - } elseif ($type instanceof TLiteralString) { - if ($combination->strings !== null && count($combination->strings) < $literal_limit) { - $combination->strings[$type_key] = $type; - } else { - $shared_classlikes = $codebase ? self::getSharedTypes($combination, $codebase) : []; - - $combination->strings = null; - - if (isset($combination->value_types['string']) - && $combination->value_types['string'] instanceof TNumericString - && \is_numeric($type->value) - ) { - // do nothing - } elseif (isset($combination->value_types['class-string']) - && $type instanceof TLiteralClassString - ) { - // do nothing - } elseif ($type instanceof TLiteralClassString) { - $type_classlikes = $codebase - ? self::getClassLikes($codebase, $type->value) - : []; - - $mutual = array_intersect_key($type_classlikes, $shared_classlikes); - - if ($mutual) { - $first_class = array_keys($mutual)[0]; - - $combination->class_string_types[$first_class] = new TNamedObject($first_class); - } else { - $combination->class_string_types['object'] = new TObject(); - } - } elseif (isset($combination->value_types['string']) - && $combination->value_types['string'] instanceof TNonspecificLiteralString - ) { - // do nothing - } elseif (isset($combination->value_types['string']) - && $combination->value_types['string'] instanceof Type\Atomic\TLowercaseString - && \strtolower($type->value) === $type->value - ) { - // do nothing - } elseif (isset($combination->value_types['string']) - && $combination->value_types['string'] instanceof Type\Atomic\TNonFalsyString - && $type->value - ) { - // do nothing - } elseif (isset($combination->value_types['string']) - && $combination->value_types['string'] instanceof Type\Atomic\TNonEmptyString - && $type->value !== '' - ) { - // do nothing - } else { - $combination->value_types['string'] = new TString(); - } - } - } else { - $type_key = 'string'; - - if (!isset($combination->value_types['string'])) { - if ($combination->strings) { - if ($type instanceof TNumericString) { - $has_non_numeric_string = false; - - foreach ($combination->strings as $string_type) { - if (!\is_numeric($string_type->value)) { - $has_non_numeric_string = true; - break; - } - } - - if ($has_non_numeric_string) { - $combination->value_types['string'] = new TString(); - } else { - $combination->value_types['string'] = $type; - } - - $combination->strings = null; - } elseif ($type instanceof Type\Atomic\TLowercaseString) { - $has_non_lowercase_string = false; - - foreach ($combination->strings as $string_type) { - if (\strtolower($string_type->value) !== $string_type->value) { - $has_non_lowercase_string = true; - break; - } - } - - if ($has_non_lowercase_string) { - $combination->value_types['string'] = new TString(); - } else { - $combination->value_types['string'] = $type; - } - - $combination->strings = null; - } elseif ($type instanceof Type\Atomic\TNonEmptyString) { - $has_empty_string = false; - - foreach ($combination->strings as $string_type) { - if (!$string_type->value) { - $has_empty_string = true; - break; - } - } - - if ($has_empty_string) { - $combination->value_types['string'] = new TString(); - } else { - $combination->value_types['string'] = $type; - } - - $combination->strings = null; - } elseif ($type instanceof TNonspecificLiteralString) { - $combination->value_types['string'] = $type; - - $combination->strings = null; - } else { - $has_non_literal_class_string = false; - - $shared_classlikes = $codebase ? self::getSharedTypes($combination, $codebase) : []; - - foreach ($combination->strings as $string_type) { - if (!$string_type instanceof TLiteralClassString) { - $has_non_literal_class_string = true; - break; - } - } - - if ($has_non_literal_class_string || - !$type instanceof TClassString - ) { - $combination->value_types[$type_key] = new TString(); - } else { - if (isset($shared_classlikes[$type->as]) && $type->as_type) { - $combination->class_string_types[$type->as] = $type->as_type; - } else { - $combination->class_string_types['object'] = new TObject(); - } - } - } - } else { - $combination->value_types[$type_key] = $type; - } - } elseif (get_class($combination->value_types['string']) !== TString::class) { - if (get_class($type) === TString::class) { - $combination->value_types['string'] = $type; - } elseif ($combination->value_types['string'] instanceof TTraitString - && $type instanceof TClassString - ) { - $combination->value_types['trait-string'] = $combination->value_types['string']; - $combination->value_types['class-string'] = $type; - - unset($combination->value_types['string']); - } elseif (get_class($combination->value_types['string']) !== get_class($type)) { - if (get_class($type) === TNonEmptyString::class - && get_class($combination->value_types['string']) === TNumericString::class - ) { - $combination->value_types['string'] = $type; - } elseif (get_class($type) === TNumericString::class - && get_class($combination->value_types['string']) === TNonEmptyString::class - ) { - // do nothing - } elseif ((get_class($type) === TNonEmptyString::class - || get_class($type) === TNumericString::class) - && get_class($combination->value_types['string']) === TNonFalsyString::class - ) { - $combination->value_types['string'] = $type; - } elseif (get_class($type) === TNonFalsyString::class - && (get_class($combination->value_types['string']) === TNonEmptyString::class - || get_class($combination->value_types['string']) === TNumericString::class) - ) { - // do nothing - } elseif ((get_class($type) === TNonEmptyString::class - || get_class($type) === TNonFalsyString::class) - && get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class - ) { - $combination->value_types['string'] = new TNonEmptyString(); - } elseif ((get_class($combination->value_types['string']) === TNonEmptyString::class - || get_class($combination->value_types['string']) === TNonFalsyString::class) - && get_class($type) === TNonEmptyLowercaseString::class - ) { - $combination->value_types['string'] = new TNonEmptyString(); - } elseif (get_class($type) === TLowercaseString::class - && get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class - ) { - $combination->value_types['string'] = $type; - } elseif (get_class($combination->value_types['string']) === TLowercaseString::class - && get_class($type) === TNonEmptyLowercaseString::class - ) { - //no-change - } else { - $combination->value_types['string'] = new TString(); - } - } - } - - $combination->strings = null; - } + self::scrapeStringProperties( + $type_key, + $type, + $combination, + $codebase, + $literal_limit + ); return null; } if ($type instanceof TInt) { - if (isset($combination->value_types['array-key'])) { - return null; - } - - $had_zero = isset($combination->ints['int(0)']); - - if ($type instanceof TLiteralInt) { - if ($type->value === 0) { - $had_zero = true; - } - - if ($combination->ints !== null && count($combination->ints) < $literal_limit) { - $combination->ints[$type_key] = $type; - } else { - $combination->ints[$type_key] = $type; - - $all_nonnegative = !array_filter( - $combination->ints, - function ($int): bool { - return $int->value < 0; - } - ); - - $combination->ints = null; - - if (!isset($combination->value_types['int'])) { - $combination->value_types['int'] = $all_nonnegative ? new TPositiveInt() : new TInt(); - } elseif ($combination->value_types['int'] instanceof TPositiveInt - && !$all_nonnegative - ) { - $combination->value_types['int'] = new TInt(); - } - } - } else { - if ($type instanceof TPositiveInt) { - if ($combination->ints) { - $all_nonnegative = !array_filter( - $combination->ints, - function ($int): bool { - return $int->value < 0; - } - ); - - if ($all_nonnegative) { - $combination->value_types['int'] = $type; - } else { - $combination->value_types['int'] = new TInt(); - } - } elseif (!isset($combination->value_types['int'])) { - $combination->value_types['int'] = $type; - } - } else { - $combination->value_types['int'] = $type; - } - - $combination->ints = null; - } - - if ($had_zero - && isset($combination->value_types['int']) - && $combination->value_types['int'] instanceof TPositiveInt - ) { - if ($combination->ints === null) { - $combination->ints = ['int(0)' => new TLiteralInt(0)]; - } elseif ($type instanceof TLiteralInt && $type->value < 0) { - $combination->ints = null; - $combination->value_types['int'] = new TInt(); - } - } + self::scrapeIntProperties( + $type_key, + $type, + $combination, + $literal_limit + ); return null; } @@ -1216,6 +949,317 @@ class TypeCombiner return null; } + private static function scrapeStringProperties( + string $type_key, + Atomic $type, + TypeCombination $combination, + ?Codebase $codebase, + int $literal_limit + ): void { + if ($type instanceof TCallableString && isset($combination->value_types['callable'])) { + return; + } + + if (isset($combination->value_types['array-key'])) { + return; + } + + if ($type instanceof Type\Atomic\TTemplateParamClass) { + $combination->value_types[$type_key] = $type; + } elseif ($type instanceof Type\Atomic\TClassString) { + if (!$type->as_type) { + $combination->class_string_types['object'] = new TObject(); + } else { + $combination->class_string_types[$type->as] = $type->as_type; + } + } elseif ($type instanceof TLiteralString) { + if ($combination->strings !== null && count($combination->strings) < $literal_limit) { + $combination->strings[$type_key] = $type; + } else { + $shared_classlikes = $codebase ? self::getSharedTypes($combination, $codebase) : []; + + $combination->strings = null; + + if (isset($combination->value_types['string']) + && $combination->value_types['string'] instanceof TNumericString + && \is_numeric($type->value) + ) { + // do nothing + } elseif (isset($combination->value_types['class-string']) + && $type instanceof TLiteralClassString + ) { + // do nothing + } elseif ($type instanceof TLiteralClassString) { + $type_classlikes = $codebase + ? self::getClassLikes($codebase, $type->value) + : []; + + $mutual = array_intersect_key($type_classlikes, $shared_classlikes); + + if ($mutual) { + $first_class = array_keys($mutual)[0]; + + $combination->class_string_types[$first_class] = new TNamedObject($first_class); + } else { + $combination->class_string_types['object'] = new TObject(); + } + } elseif (isset($combination->value_types['string']) + && $combination->value_types['string'] instanceof TNonspecificLiteralString + ) { + // do nothing + } elseif (isset($combination->value_types['string']) + && $combination->value_types['string'] instanceof Type\Atomic\TLowercaseString + && \strtolower($type->value) === $type->value + ) { + // do nothing + } elseif (isset($combination->value_types['string']) + && $combination->value_types['string'] instanceof Type\Atomic\TNonFalsyString + && $type->value + ) { + // do nothing + } elseif (isset($combination->value_types['string']) + && $combination->value_types['string'] instanceof Type\Atomic\TNonEmptyString + && $type->value !== '' + ) { + // do nothing + } else { + $combination->value_types['string'] = new TString(); + } + } + } else { + $type_key = 'string'; + + if (!isset($combination->value_types['string'])) { + if ($combination->strings) { + if ($type instanceof TNumericString) { + $has_non_numeric_string = false; + + foreach ($combination->strings as $string_type) { + if (!\is_numeric($string_type->value)) { + $has_non_numeric_string = true; + break; + } + } + + if ($has_non_numeric_string) { + $combination->value_types['string'] = new TString(); + } else { + $combination->value_types['string'] = $type; + } + + $combination->strings = null; + } elseif ($type instanceof Type\Atomic\TLowercaseString) { + $has_non_lowercase_string = false; + + foreach ($combination->strings as $string_type) { + if (\strtolower($string_type->value) !== $string_type->value) { + $has_non_lowercase_string = true; + break; + } + } + + if ($has_non_lowercase_string) { + $combination->value_types['string'] = new TString(); + } else { + $combination->value_types['string'] = $type; + } + + $combination->strings = null; + } elseif ($type instanceof Type\Atomic\TNonEmptyString) { + $has_empty_string = false; + + foreach ($combination->strings as $string_type) { + if (!$string_type->value) { + $has_empty_string = true; + break; + } + } + + if ($has_empty_string) { + $combination->value_types['string'] = new TString(); + } else { + $combination->value_types['string'] = $type; + } + + $combination->strings = null; + } elseif ($type instanceof TNonspecificLiteralString) { + $combination->value_types['string'] = $type; + + $combination->strings = null; + } else { + $has_non_literal_class_string = false; + + $shared_classlikes = $codebase ? self::getSharedTypes($combination, $codebase) : []; + + foreach ($combination->strings as $string_type) { + if (!$string_type instanceof TLiteralClassString) { + $has_non_literal_class_string = true; + break; + } + } + + if ($has_non_literal_class_string || + !$type instanceof TClassString + ) { + $combination->value_types[$type_key] = new TString(); + } else { + if (isset($shared_classlikes[$type->as]) && $type->as_type) { + $combination->class_string_types[$type->as] = $type->as_type; + } else { + $combination->class_string_types['object'] = new TObject(); + } + } + } + } else { + $combination->value_types[$type_key] = $type; + } + } elseif (get_class($combination->value_types['string']) !== TString::class) { + if (get_class($type) === TString::class) { + $combination->value_types['string'] = $type; + } elseif ($combination->value_types['string'] instanceof TTraitString + && $type instanceof TClassString + ) { + $combination->value_types['trait-string'] = $combination->value_types['string']; + $combination->value_types['class-string'] = $type; + + unset($combination->value_types['string']); + } elseif (get_class($combination->value_types['string']) !== get_class($type)) { + if (get_class($type) === TNonEmptyString::class + && get_class($combination->value_types['string']) === TNumericString::class + ) { + $combination->value_types['string'] = $type; + } elseif (get_class($type) === TNumericString::class + && get_class($combination->value_types['string']) === TNonEmptyString::class + ) { + // do nothing + } elseif ((get_class($type) === TNonEmptyString::class + || get_class($type) === TNumericString::class) + && get_class($combination->value_types['string']) === TNonFalsyString::class + ) { + $combination->value_types['string'] = $type; + } elseif (get_class($type) === TNonFalsyString::class + && (get_class($combination->value_types['string']) === TNonEmptyString::class + || get_class($combination->value_types['string']) === TNumericString::class) + ) { + // do nothing + } elseif ((get_class($type) === TNonEmptyString::class + || get_class($type) === TNonFalsyString::class) + && get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class + ) { + $combination->value_types['string'] = new TNonEmptyString(); + } elseif ((get_class($combination->value_types['string']) === TNonEmptyString::class + || get_class($combination->value_types['string']) === TNonFalsyString::class) + && get_class($type) === TNonEmptyLowercaseString::class + ) { + $combination->value_types['string'] = new TNonEmptyString(); + } elseif (get_class($type) === TLowercaseString::class + && get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class + ) { + $combination->value_types['string'] = $type; + } elseif (get_class($combination->value_types['string']) === TLowercaseString::class + && get_class($type) === TNonEmptyLowercaseString::class + ) { + //no-change + } else { + $combination->value_types['string'] = new TString(); + } + } + } + + $combination->strings = null; + } + } + + private static function scrapeIntProperties( + string $type_key, + Atomic $type, + TypeCombination $combination, + int $literal_limit + ): void { + if (isset($combination->value_types['array-key'])) { + return; + } + + $had_zero = isset($combination->ints['int(0)']); + + if ($type instanceof TLiteralInt) { + if ($type->value === 0) { + $had_zero = true; + } + + if ($combination->ints !== null && count($combination->ints) < $literal_limit) { + $combination->ints[$type_key] = $type; + } else { + $combination->ints[$type_key] = $type; + + $all_nonnegative = !array_filter( + $combination->ints, + function ($int): bool { + return $int->value < 0; + } + ); + + $combination->ints = null; + + if (!isset($combination->value_types['int'])) { + $combination->value_types['int'] = $all_nonnegative + ? new TPositiveInt() + : new TNonspecificLiteralInt(); + } elseif ($combination->value_types['int'] instanceof TPositiveInt + && !$all_nonnegative + ) { + $combination->value_types['int'] = new TInt(); + } + } + } else { + if ($type instanceof TPositiveInt) { + if ($combination->ints) { + $all_nonnegative = !array_filter( + $combination->ints, + function ($int): bool { + return $int->value < 0; + } + ); + + if ($all_nonnegative) { + $combination->value_types['int'] = $type; + } else { + $combination->value_types['int'] = new TInt(); + } + } elseif (!isset($combination->value_types['int'])) { + $combination->value_types['int'] = $type; + } elseif (get_class($combination->value_types['int']) !== get_class($type)) { + $combination->value_types['int'] = new TInt(); + } + } elseif ($type instanceof TNonspecificLiteralInt) { + if ($combination->ints || !isset($combination->value_types['int'])) { + $combination->value_types['int'] = $type; + } elseif (isset($combination->value_types['int']) + && get_class($combination->value_types['int']) + !== get_class($type) + ) { + $combination->value_types['int'] = new TInt(); + } + } else { + $combination->value_types['int'] = $type; + } + + $combination->ints = null; + } + + if ($had_zero + && isset($combination->value_types['int']) + && $combination->value_types['int'] instanceof TPositiveInt + ) { + if ($combination->ints === null) { + $combination->ints = ['int(0)' => new TLiteralInt(0)]; + } elseif ($type instanceof TLiteralInt && $type->value < 0) { + $combination->ints = null; + $combination->value_types['int'] = new TInt(); + } + } + } + /** * @return array */ diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index cea1bf687..72194cf5e 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -53,6 +53,7 @@ class TypeTokenizer 'lowercase-string' => true, 'non-empty-lowercase-string' => true, 'positive-int' => true, + 'literal-int' => true, 'boolean' => true, 'integer' => true, 'double' => true, diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index b8186f5ac..10350d5e3 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -266,6 +266,9 @@ abstract class Atomic implements TypeNode case 'non-empty-literal-string': return new Type\Atomic\TNonEmptyNonspecificLiteralString(); + case 'literal-int': + return new Type\Atomic\TNonspecificLiteralInt(); + case 'false-y': return new TAssertionFalsy(); diff --git a/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php b/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php new file mode 100644 index 000000000..99e20ef04 --- /dev/null +++ b/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php @@ -0,0 +1,29 @@ + ? non-empty-literal-string : non-empty-string) + * ? ($pieces is array + * ? ($glue is literal-string ? non-empty-literal-string : non-empty-string) + * : non-empty-string + * ) * : string) * : string * ) diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 30d655fe7..be479a702 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -920,9 +920,12 @@ class ArrayFunctionCallTest extends TestCase 'implodeNonEmptyArrayAndString' => [ ' 'non-empty-literal-string', + '$b===' => 'non-empty-literal-string', ] ], 'key' => [ diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index dc96d98be..76a3b2fa6 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -201,10 +201,8 @@ class BinaryOperationTest extends TestCase return $arg; } - /** @var "a"|"b" */ - $foo = "a"; - /** @var "c"|"d" */ - $bar = "c"; + $foo = rand(0, 1) ? "a" : "b"; + $bar = rand(0, 1) ? "c" : "d"; $baz = $foo . $bar; foobar($baz); ', @@ -598,6 +596,49 @@ class BinaryOperationTest extends TestCase return "Hello $s1 $s2"; }', ], + 'literalIntConcatCreatesLiteral' => [ + ' [ + ' [ + ' [ + ' [ + 'literal-int', + [ + 'literal-int', + '5', + ] + ], + 'combineNonspecificLiteralAndLiteralInt' => [ + 'literal-int', + [ + '5', + 'literal-int', + ] + ], + 'combineNonspecificLiteralAndPositiveInt' => [ + 'int', + [ + 'positive-int', + 'literal-int', + ] + ], + 'combinePositiveAndLiteralInt' => [ + 'int', + [ + 'literal-int', + 'positive-int', + ] + ], ]; }