mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Add non-falsy-string to allow more accurate checks
This commit is contained in:
parent
03665b9646
commit
a0420fb704
@ -27,6 +27,7 @@ use Psalm\Type\Atomic\TLiteralInt;
|
||||
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 @@ class ScalarTypeComparator
|
||||
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 @@ class ScalarTypeComparator
|
||||
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 @@ class ScalarTypeComparator
|
||||
|
||||
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 @@ class ScalarTypeComparator
|
||||
|
||||
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 @@ class ScalarTypeComparator
|
||||
|| $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 @@ class ScalarTypeComparator
|
||||
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;
|
||||
}
|
||||
|
@ -1971,6 +1971,7 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler
|
||||
$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 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -582,7 +582,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler
|
||||
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 @@ class SimpleNegatedAssertionReconciler extends Reconciler
|
||||
}
|
||||
|
||||
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 @@ class SimpleNegatedAssertionReconciler extends Reconciler
|
||||
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) {
|
||||
|
@ -36,6 +36,7 @@ use Psalm\Type\Atomic\TNonEmptyList;
|
||||
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 @@ class TypeCombiner
|
||||
) {
|
||||
// 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 @@ class TypeCombiner
|
||||
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
|
||||
|
@ -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,
|
||||
|
@ -177,6 +177,9 @@ abstract class Atomic implements TypeNode
|
||||
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();
|
||||
|
||||
|
@ -4,7 +4,7 @@ namespace Psalm\Type\Atomic;
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
|
13
src/Psalm/Type/Atomic/TNonFalsyString.php
Normal file
13
src/Psalm/Type/Atomic/TNonFalsyString.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Psalm\Type\Atomic;
|
||||
|
||||
/**
|
||||
* Denotes a string, that is also non-empty
|
||||
*/
|
||||
class TNonFalsyString extends TNonEmptyString
|
||||
{
|
||||
public function getId(bool $nested = false): string
|
||||
{
|
||||
return 'non-falsy-string';
|
||||
}
|
||||
}
|
@ -637,27 +637,41 @@ class TypeCombinationTest extends TestCase
|
||||
'positive-int',
|
||||
],
|
||||
],
|
||||
'combinNonEmptyArrayAndKeyedArray' => [
|
||||
'combineNonEmptyArrayAndKeyedArray' => [
|
||||
'array<int, int>',
|
||||
[
|
||||
'non-empty-array<int, int>',
|
||||
'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'
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -854,6 +854,38 @@ class ValueTest extends \Psalm\Tests\TestCase
|
||||
[],
|
||||
'7.4'
|
||||
],
|
||||
'zeroIsNonEmptyString' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param non-empty-string $s
|
||||
*/
|
||||
function foo(string $s) : void {}
|
||||
|
||||
foo("0");',
|
||||
],
|
||||
'notLiteralEmptyCanBeNotEmptyString' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param non-empty-string $s
|
||||
*/
|
||||
function foo(string $s) : void {}
|
||||
|
||||
function takesString(string $s) : void {
|
||||
if ($s !== "") {
|
||||
foo($s);
|
||||
}
|
||||
}',
|
||||
],
|
||||
'nonEmptyStringCanBeStringZero' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param non-empty-string $s
|
||||
*/
|
||||
function foo(string $s) : void {
|
||||
if ($s === "0") {}
|
||||
if (empty($s)) {}
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -1053,6 +1085,18 @@ class ValueTest extends \Psalm\Tests\TestCase
|
||||
}',
|
||||
'error_message' => 'ArgumentTypeCoercion'
|
||||
],
|
||||
'stringCoercedToNonEmptyString' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param non-empty-string $name
|
||||
*/
|
||||
function sayHello(string $name) : void {}
|
||||
|
||||
function takeInput(string $name) : void {
|
||||
sayHello($name);
|
||||
}',
|
||||
'error_message' => 'ArgumentTypeCoercion',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user