diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 3a3ac6ae110..541b6b1e370 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -27,6 +27,7 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyString; +use Psalm\Type\Atomic\TNonFalsyString; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; use Psalm\Type\Atomic\TPositiveInt; @@ -258,6 +259,22 @@ public static function isContainedBy( return true; } + if ($container_type_part instanceof TNonEmptyString + && $input_type_part instanceof TNonFalsyString + ) { + return true; + } + + if ($container_type_part instanceof TNonFalsyString + && get_class($input_type_part) === TNonEmptyString::class + ) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; + } + + return false; + } + if ($container_type_part instanceof TNonEmptyString && $input_type_part instanceof TLiteralString && $input_type_part->value === '' @@ -265,8 +282,16 @@ public static function isContainedBy( return false; } + if ($container_type_part instanceof TNonFalsyString + && $input_type_part instanceof TLiteralString + && $input_type_part->value === '0' + ) { + return false; + } + if ((get_class($container_type_part) === TString::class || get_class($container_type_part) === TNonEmptyString::class + || get_class($container_type_part) === TNonFalsyString::class || get_class($container_type_part) === TSingleLetter::class) && $input_type_part instanceof TLiteralString ) { @@ -321,7 +346,8 @@ public static function isContainedBy( if ((get_class($input_type_part) === TString::class || get_class($input_type_part) === TSingleLetter::class - || get_class($input_type_part) === TNonEmptyString::class) + || get_class($input_type_part) === TNonEmptyString::class + || get_class($input_type_part) === TNonFalsyString::class) && $container_type_part instanceof TLiteralString ) { if ($atomic_comparison_result) { @@ -365,7 +391,8 @@ public static function isContainedBy( if ($container_type_part instanceof TTraitString && (get_class($input_type_part) === TString::class - || get_class($input_type_part) === TNonEmptyString::class) + || get_class($input_type_part) === TNonEmptyString::class + || get_class($input_type_part) === TNonFalsyString::class) ) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; @@ -378,7 +405,8 @@ public static function isContainedBy( || $input_type_part instanceof TLiteralClassString) && (get_class($container_type_part) === TString::class || get_class($container_type_part) === TSingleLetter::class - || get_class($container_type_part) === TNonEmptyString::class) + || get_class($container_type_part) === TNonEmptyString::class + || get_class($container_type_part) === TNonFalsyString::class) ) { return true; } @@ -386,7 +414,8 @@ public static function isContainedBy( if ($input_type_part instanceof TCallableString && (get_class($container_type_part) === TString::class || get_class($container_type_part) === TSingleLetter::class - || get_class($container_type_part) === TNonEmptyString::class) + || get_class($container_type_part) === TNonEmptyString::class + || get_class($container_type_part) === TNonFalsyString::class) ) { return true; } diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index c012dad30d4..0f1e424a13d 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -1971,6 +1971,7 @@ private static function reconcileCallable( $callable_types[] = $type; } elseif (get_class($type) === TString::class || get_class($type) === Type\Atomic\TNonEmptyString::class + || get_class($type) === Type\Atomic\TNonFalsyString::class ) { $callable_types[] = new Type\Atomic\TCallableString(); $did_remove_type = true; @@ -2203,6 +2204,8 @@ private static function reconcileFalsyOrEmpty( if (!$existing_var_atomic_types['string'] instanceof Type\Atomic\TNonEmptyString) { $existing_var_type->addType(new Type\Atomic\TLiteralString('')); $existing_var_type->addType(new Type\Atomic\TLiteralString('0')); + } elseif (!$existing_var_atomic_types['string'] instanceof Type\Atomic\TNonFalsyString) { + $existing_var_type->addType(new Type\Atomic\TLiteralString('0')); } } } diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 0d2ff8f0121..878111b3210 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -582,7 +582,7 @@ private static function reconcileFalsyOrEmpty( if ($existing_var_atomic_types['string'] instanceof Type\Atomic\TLowercaseString) { $existing_var_type->addType(new Type\Atomic\TNonEmptyLowercaseString); } else { - $existing_var_type->addType(new Type\Atomic\TNonEmptyString); + $existing_var_type->addType(new Type\Atomic\TNonFalsyString); } } @@ -707,7 +707,7 @@ private static function reconcileFalsyOrEmpty( } if (isset($existing_var_atomic_types['string'])) { - if (!$existing_var_atomic_types['string'] instanceof Type\Atomic\TNonEmptyString + if (!$existing_var_atomic_types['string'] instanceof Type\Atomic\TNonFalsyString && !$existing_var_atomic_types['string'] instanceof Type\Atomic\TClassString && !$existing_var_atomic_types['string'] instanceof Type\Atomic\TDependentGetClass ) { @@ -718,7 +718,7 @@ private static function reconcileFalsyOrEmpty( if ($existing_var_atomic_types['string'] instanceof Type\Atomic\TLowercaseString) { $existing_var_type->addType(new Type\Atomic\TNonEmptyLowercaseString); } else { - $existing_var_type->addType(new Type\Atomic\TNonEmptyString); + $existing_var_type->addType(new Type\Atomic\TNonFalsyString); } } elseif ($existing_var_type->isSingle() && !$is_equality) { if ($code_location && $key) { diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 151824d0cfe..d95873e1773 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -36,6 +36,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\TNull; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TPositiveInt; @@ -932,10 +933,15 @@ private static function scrapeTypeProperties( ) { // do nothing } elseif (isset($combination->value_types['string']) - && $combination->value_types['string'] instanceof Type\Atomic\TNonEmptyString + && $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(); } @@ -1036,10 +1042,20 @@ private static function scrapeTypeProperties( 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']) === TNonFalsyString::class + ) { + $combination->value_types['string'] = $type; + } elseif (get_class($type) === TNonFalsyString::class + && get_class($combination->value_types['string']) === TNonEmptyString::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'] = $type; - } elseif (get_class($combination->value_types['string']) === TNonEmptyString::class + } elseif ((get_class($combination->value_types['string']) === TNonEmptyString::class + || get_class($combination->value_types['string']) === TNonFalsyString::class) && get_class($type) === TNonEmptyLowercaseString::class ) { //no-change diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index 2a7ad7f3e99..25a5a920099 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -34,6 +34,7 @@ class TypeTokenizer 'array' => true, 'non-empty-array' => true, 'non-empty-string' => true, + 'non-falsy-string' => true, 'iterable' => true, 'null' => true, 'mixed' => true, diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index c002d091db6..a326db6dba1 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -177,6 +177,9 @@ public static function create( case 'non-empty-string': return new Type\Atomic\TNonEmptyString(); + case 'non-falsy-string': + return new Type\Atomic\TNonFalsyString(); + case 'lowercase-string': return new Type\Atomic\TLowercaseString(); diff --git a/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php b/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php index d0132401372..d24ab9701c9 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php @@ -4,7 +4,7 @@ /** * Denotes a non-empty-string where every character is lowercased. (which can also result from a `strtolower` call). */ -class TNonEmptyLowercaseString extends TNonEmptyString +class TNonEmptyLowercaseString extends TNonFalsyString { public function getKey(bool $include_extra = true): string { diff --git a/src/Psalm/Type/Atomic/TNonFalsyString.php b/src/Psalm/Type/Atomic/TNonFalsyString.php new file mode 100644 index 00000000000..daa49dece21 --- /dev/null +++ b/src/Psalm/Type/Atomic/TNonFalsyString.php @@ -0,0 +1,13 @@ + [ + 'combineNonEmptyArrayAndKeyedArray' => [ 'array', [ 'non-empty-array', 'array{0?:int}', ] ], - 'combinNonEmptyStringAndLiteral' => [ + 'combineNonEmptyStringAndLiteral' => [ 'non-empty-string', [ 'non-empty-string', '"foo"', ] ], - 'combinLiteralAndNonEmptyString' => [ + 'combineLiteralAndNonEmptyString' => [ 'non-empty-string', [ '"foo"', 'non-empty-string' ] ], + 'combineNonFalsyNonEmptyString' => [ + 'non-empty-string', + [ + 'non-falsy-string', + 'non-empty-string' + ] + ], + 'combineNonEmptyNonFalsyString' => [ + 'non-empty-string', + [ + 'non-empty-string', + 'non-falsy-string' + ] + ], ]; } diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index 7f7268a8b17..b577a344a3c 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -854,6 +854,38 @@ class A { [], '7.4' ], + 'zeroIsNonEmptyString' => [ + ' [ + ' [ + ' 'ArgumentTypeCoercion' ], + 'stringCoercedToNonEmptyString' => [ + ' 'ArgumentTypeCoercion', + ], ]; } }