Skip to content

Commit

Permalink
WIP(broken) - Support non-null-mixed
Browse files Browse the repository at this point in the history
Update tests and implementation to handle edge cases uncovered by the
previous commit.
  • Loading branch information
TysonAndre committed Dec 4, 2020
1 parent 64eb70b commit b3d7333
Show file tree
Hide file tree
Showing 20 changed files with 130 additions and 58 deletions.
2 changes: 1 addition & 1 deletion src/Phan/AST/ContextNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ public function getMethod(
|| (!$union_type->isEmpty()
&& $union_type->isNativeType()
&& !$union_type->hasTypeMatchingCallback(static function (Type $type): bool {
return !$type->isNullable() && ($type instanceof MixedType || $type instanceof ObjectType);
return !$type->isNullableLabeled() && ($type instanceof MixedType || $type instanceof ObjectType);
})
// reject `$stringVar->method()` but not `$stringVar::method()` and not (`new $stringVar()`
&& !(($is_static || $is_new_expression) && $union_type->hasNonNullStringType())
Expand Down
30 changes: 28 additions & 2 deletions src/Phan/AST/UnionTypeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -1988,11 +1988,37 @@ private function resolveArrayShapeElementTypes(Node $node, UnionType $union_type
}
// $union_type is exclusively array shape types, but those don't contain the field $dim_value.
// It's undefined (which becomes null)
return NullType::instance(false)->asPHPDocUnionType();
if (self::couldRealTypesHaveKey($union_type->getRealTypeSet(), $dim_value)) {
return NullType::instance(false)->asPHPDocUnionType();
}
return NullType::instance(false)->asRealUnionType();
}
return $resulting_element_type;
}

/**
* @param list<Type> $real_type_set
* @param int|string|float $dim_value
*/
private static function couldRealTypesHaveKey(array $real_type_set, $dim_value): bool
{
foreach ($real_type_set as $type) {
if ($type instanceof ArrayShapeType) {
if (\array_key_exists($dim_value, $type->getFieldTypes())) {
return true;
}
} elseif ($type instanceof ListType) {
$filtered = \is_int($dim_value) ? $dim_value : \filter_var($dim_value, \FILTER_VALIDATE_INT);
if (\is_int($filtered) && $filtered >= 0) {
return true;
}
} else {
return true;
}
}
return \count($real_type_set) === 0;
}

/**
* @param UnionType $union_type a union type with at least one top-level array shape type
* @param int|string|float|bool $dim_value a scalar dimension. TODO: Warn about null?
Expand Down Expand Up @@ -2139,7 +2165,7 @@ private function analyzeUnpack(Node $node, bool $is_array_spread): UnionType
try {
if ($generic_types->isEmpty()) {
if (!$union_type->asExpandedTypes($this->code_base)->hasIterable() && !$union_type->hasTypeMatchingCallback(static function (Type $type): bool {
return !$type->isNullable() && $type instanceof MixedType;
return !$type->isNullableLabeled() && $type instanceof MixedType;
})) {
throw new IssueException(
Issue::fromType(Issue::TypeMismatchUnpackValue)(
Expand Down
3 changes: 3 additions & 0 deletions src/Phan/Analysis/BinaryOperatorFlagVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,9 @@ public function visitBinaryCoalesce(Node $node): UnionType
// On the left side, remove null and replace '?T' with 'T'
// Don't bother if the right side contains null.
if (!$right_type->isEmpty() && $left_type->containsNullable() && !$right_type->containsNullable()) {
if ($left_type->getRealUnionType()->isRealTypeNullOrUndefined()) {
return $right_type;
}
$left_type = $left_type->nonNullableClone();
}

Expand Down
10 changes: 10 additions & 0 deletions src/Phan/Language/AnnotatedUnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ public function isDefinitelyUndefined(): bool
return $this->is_possibly_undefined === self::DEFINITELY_UNDEFINED;
}

public function isNull(): bool
{
return $this->is_possibly_undefined === self::DEFINITELY_UNDEFINED || parent::isNull();
}

public function isRealTypeNullOrUndefined(): bool
{
return $this->is_possibly_undefined === self::DEFINITELY_UNDEFINED || parent::isRealTypeNullOrUndefined();
}

public function __toString(): string
{
$result = parent::__toString();
Expand Down
19 changes: 15 additions & 4 deletions src/Phan/Language/EmptyUnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,14 @@ public function containsNullable(): bool
return false;
}

/**
* @override
*/
public function containsNonMixedNullable(): bool
{
return false;
}

/**
* @return bool - True if not empty and at least one type is NullType or mixed.
*/
Expand All @@ -340,6 +348,11 @@ public function isNull(): bool
return false;
}

public function isRealTypeNullOrUndefined(): bool
{
return false;
}

/**
* @return bool - True if not empty, not possibly undefined, and at least one type is NullType or nullable.
*/
Expand Down Expand Up @@ -553,13 +566,11 @@ public function canCastToUnionTypeWithoutConfig(
* @internal
* @override
*/
/**
* No longer a special case
public function canCastToUnionTypeIfNonNull(UnionType $target): bool
{
return false;
// TODO: Better check for isPossiblyNonNull
return UnionType::fromFullyQualifiedRealString('non-null-mixed')->canCastToUnionType($target);
}
*/

public function canCastToUnionTypeHandlingTemplates(
UnionType $target,
Expand Down
14 changes: 13 additions & 1 deletion src/Phan/Language/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -1793,13 +1793,25 @@ public function getNamespace(): string
/**
* Is this nullable?
*
* E.g. returns true for `?array`, `null`, etc.
* E.g. returns true for `?array`, `null`, `mixed`, etc.
*/
public function isNullable(): bool
{
return $this->is_nullable;
}

/**
* Is this nullable in a way that Phan would emit warnings about nullable?
*
* E.g. returns true for `?array`, `null`, `?mixed` (but not `mixed`), etc.
*
* Currently, the only difference between this and isNullable() is for `?mixed` vs `mixed`
*/
public function isNullableLabeled(): bool
{
return $this->is_nullable;
}

/**
* Returns true if this has some possibly falsey values
*/
Expand Down
5 changes: 5 additions & 0 deletions src/Phan/Language/Type/MixedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ public function isNullable(): bool
return true;
}

public function isNullableLabeled(): bool
{
return $this->is_nullable;
}

/** Overridden by NonEmptyMixedType */
public function __toString(): string
{
Expand Down
29 changes: 10 additions & 19 deletions src/Phan/Language/Type/NullType.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function canCastToNonNullableType(Type $type): bool
*/
public function canCastToDeclaredType(CodeBase $code_base, Context $context, Type $other): bool
{
return $other->isNullable() || $other instanceof MixedType || $other instanceof TemplateType;
return $other->isNullable() || $other instanceof TemplateType;
}

public function isSubtypeOf(Type $type): bool
Expand Down Expand Up @@ -93,7 +93,7 @@ public function canCastToType(Type $type): bool
}

// Null can cast to a nullable type.
if ($type->is_nullable) {
if ($type->isNullable()) {
return true;
}

Expand All @@ -113,9 +113,6 @@ public function canCastToType(Type $type): bool
return true;
}
}
if (\get_class($type) === MixedType::class) {
return true;
}

return false;
}
Expand All @@ -132,16 +129,8 @@ public function canCastToTypeWithoutConfig(Type $type): bool
return true;
}

// Null can cast to a nullable type or mixed.
if ($type->is_nullable) {
return true;
}

if (\get_class($type) === MixedType::class) {
return true;
}

return false;
// Null can cast to a nullable type or mixed (but not non-null-mixed).
return $type->isNullable();
}

/**
Expand All @@ -157,7 +146,7 @@ public function canCastToTypeHandlingTemplates(Type $type, CodeBase $code_base):
}

// Null can cast to a nullable type.
if ($type->is_nullable) {
if ($type->isNullable()) {
return true;
}

Expand All @@ -177,9 +166,6 @@ public function canCastToTypeHandlingTemplates(Type $type, CodeBase $code_base):
return true;
}
}
if ($type instanceof MixedType) {
return $type->isPossiblyFalsey();
}

// Test to see if we can cast to the non-nullable version
// of the target type.
Expand Down Expand Up @@ -210,6 +196,11 @@ public function isNullable(): bool
return true;
}

public function isNullableLabeled(): bool
{
return true;
}

public function isPossiblyFalsey(): bool
{
return true; // Null is always falsey.
Expand Down
26 changes: 10 additions & 16 deletions src/Phan/Language/Type/VoidType.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ protected function __construct(
*/
public function canCastToDeclaredType(CodeBase $code_base, Context $context, Type $other): bool
{
return $other->isNullable() || $other instanceof MixedType || $other instanceof TemplateType;
return $other->isNullable() || $other instanceof TemplateType;
}

public function isSubtypeOf(Type $type): bool
Expand Down Expand Up @@ -96,7 +96,7 @@ public function canCastToType(Type $type): bool
}

// Null(void) can cast to a nullable type.
if ($type->is_nullable) {
if ($type->isNullable()) {
return true;
}

Expand All @@ -116,9 +116,6 @@ public function canCastToType(Type $type): bool
return true;
}
}
if (\get_class($type) === MixedType::class) {
return true;
}

return false;
}
Expand All @@ -130,16 +127,8 @@ public function canCastToTypeWithoutConfig(Type $type): bool
return true;
}

// Null(void) can cast to a nullable type or mixed.
if ($type->is_nullable) {
return true;
}

if (\get_class($type) === MixedType::class) {
return true;
}

return false;
// Null(void) can cast to a nullable type or mixed (but not non-null-mixed).
return $type->isNullable();
}

/**
Expand Down Expand Up @@ -186,7 +175,7 @@ public function canCastToTypeHandlingTemplates(Type $type, CodeBase $code_base):
}
}
if ($type instanceof MixedType) {
return !$type instanceof NonEmptyMixedType;
return $type->isNullable();
}

// Test to see if we can cast to the non-nullable version
Expand Down Expand Up @@ -233,6 +222,11 @@ public function isNullable(): bool
return true;
}

public function isNullableLabeled(): bool
{
return true;
}

public function isPossiblyFalsey(): bool
{
return true; // Null is always falsey.
Expand Down
16 changes: 15 additions & 1 deletion src/Phan/Language/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,19 @@ public function isNull(): bool
return \count($this->type_set) !== 0;
}

/**
* Returns true if !isset(expr) is unconditionally true for the real types
*/
public function isRealTypeNullOrUndefined(): bool
{
foreach ($this->real_type_set as $type) {
if (!($type instanceof NullType) && !($type instanceof VoidType)) {
return false;
}
}
return \count($this->type_set) !== 0;
}

/**
* @return UnionType a clone of this that does not include null,
* and has the non-null equivalents of any nullable types in this UnionType
Expand Down Expand Up @@ -1505,7 +1518,7 @@ private static function toNullableTypeList(array $type_list): array
{
$result = [];
foreach ($type_list as $type) {
if ($type->isNullable()) {
if ($type->isNullableLabeled()) {
$result[] = $type;
} else {
$result[] = $type->withIsNullable(true);
Expand Down Expand Up @@ -3601,6 +3614,7 @@ private static function castTypeListToClassStringOrObject(array $type_set): arra
*/
public function numericTypes(): UnionType
{
// TODO: Replace mixed with int|string|float in ConditionVisitor
return $this->makeFromFilter(static function (Type $type): bool {
// IntType and LiteralStringType
return $type->isPossiblyNumeric();
Expand Down
9 changes: 6 additions & 3 deletions src/Phan/Plugin/Internal/RedundantConditionCallPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,11 @@ static function (UnionType $type) use ($checker): bool {
$method = new ReflectionMethod(UnionType::class, $extract_types_method);
/** @suppress PhanPluginUnknownObjectMethodCall ReflectionMethod cannot be analyzed */
return $make_first_arg_checker(static function (UnionType $type) use ($method): int {
$new_real_type = $method->invoke($type)->nonNullableClone();
$new_real_type = $method->invoke($type);
if ($new_real_type->isEmpty()) {
return self::_IS_IMPOSSIBLE;
}
$new_real_type = $new_real_type->nonNullableClone();
if ($new_real_type->isEqualTo($type)) {
return self::_IS_REDUNDANT;
}
Expand All @@ -144,10 +145,11 @@ static function (UnionType $type) use ($checker): bool {
$resource_callback = $make_first_arg_checker(static function (UnionType $type): int {
$new_real_type = $type->makeFromFilter(static function (Type $type): bool {
return $type instanceof ResourceType;
})->nonNullableClone();
});
if ($new_real_type->isEmpty()) {
return self::_IS_IMPOSSIBLE;
}
$new_real_type = $new_real_type->nonNullableClone();
if ($new_real_type->isEqualTo($type)) {
return self::_IS_REDUNDANT;
}
Expand Down Expand Up @@ -199,10 +201,11 @@ static function (UnionType $type) use ($checker): bool {
}, $expected_type);
};
$callable_callback = $make_first_arg_checker(static function (UnionType $type): int {
$new_real_type = $type->callableTypes()->nonNullableClone();
$new_real_type = $type->callableTypes();
if ($new_real_type->isEmpty()) {
return self::_IS_IMPOSSIBLE;
}
$new_real_type = $new_real_type->nonNullableClone();
if ($new_real_type->isEqualTo($type)) {
if (!$new_real_type->hasTypeMatchingCallback(static function (Type $type): bool {
return $type instanceof ArrayShapeType;
Expand Down

0 comments on commit b3d7333

Please sign in to comment.