Skip to content

Commit

Permalink
Add non-falsy-string to allow more accurate checks
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed Feb 3, 2021
1 parent 03665b9 commit a0420fb
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 13 deletions.
37 changes: 33 additions & 4 deletions src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -258,15 +259,39 @@ 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 === ''
) {
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
) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -378,15 +405,17 @@ 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;
}

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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Psalm/Internal/Type/SimpleAssertionReconciler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'));
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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
) {
Expand All @@ -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) {
Expand Down
20 changes: 18 additions & 2 deletions src/Psalm/Internal/Type/TypeCombiner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Type/TypeTokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/Psalm/Type/Atomic.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php
Original file line number Diff line number Diff line change
Expand Up @@ -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

This comment has been minimized.

Copy link
@orklah

orklah Feb 3, 2021

Collaborator

Why? strtolower('0') will still be falsy?

This comment has been minimized.

Copy link
@muglug

muglug Feb 3, 2021

Author Collaborator

Yeah, but my hunch is that nobody's calling strtolower on 0

This comment has been minimized.

Copy link
@orklah

orklah Feb 3, 2021

Collaborator

ouch, this hurts my sense of perfection :D But I guess you would know better :)

{
public function getKey(bool $include_extra = true): string
{
Expand Down
13 changes: 13 additions & 0 deletions src/Psalm/Type/Atomic/TNonFalsyString.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
namespace Psalm\Type\Atomic;

/**
* Denotes a string, that is also non-empty

This comment has been minimized.

Copy link
@orklah

orklah Feb 3, 2021

Collaborator

maybe fix the documentation from non-empty to non-falsy

This comment has been minimized.

Copy link
@muglug

muglug Feb 3, 2021

Author Collaborator

Yeah, when it comes time to release

*/
class TNonFalsyString extends TNonEmptyString
{
public function getId(bool $nested = false): string
{
return 'non-falsy-string';
}
}
20 changes: 17 additions & 3 deletions tests/TypeCombinationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -637,27 +637,41 @@ public function providerTestValidTypeCombination(): array
'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'
]
],
];
}

Expand Down
44 changes: 44 additions & 0 deletions tests/TypeReconciliation/ValueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,38 @@ class A {
[],
'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)) {}
}',
],
];
}

Expand Down Expand Up @@ -1053,6 +1085,18 @@ function bar(string $s) : void {
}',
'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',
],
];
}
}

0 comments on commit a0420fb

Please sign in to comment.