1
0
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:
Matt Brown 2021-02-03 17:19:48 -05:00
parent 03665b9646
commit a0420fb704
10 changed files with 136 additions and 13 deletions

View File

@ -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;
}

View File

@ -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'));
}
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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,

View File

@ -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();

View File

@ -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
{

View 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';
}
}

View File

@ -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'
]
],
];
}

View File

@ -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',
],
];
}
}