Skip to content
Permalink
Browse files

Fix #2026 - add support for calling __toString implicitly after metho…

…d_exists
  • Loading branch information...
muglug committed Aug 16, 2019
1 parent 236a7ff commit 43041836dd44ef21f146ced1d1f4d07c38d0b013
@@ -1724,6 +1724,14 @@ public static function processFunctionCall(
if ($first_var_name) {
$if_types[$first_var_name] = [[$prefix . 'callable-string']];
}
} elseif ($expr->name instanceof PhpParser\Node\Name
&& strtolower($expr->name->parts[0]) === 'method_exists'
&& isset($expr->args[1])
&& $expr->args[1]->value instanceof PhpParser\Node\Scalar\String_
) {
if ($first_var_name) {
$if_types[$first_var_name] = [[$prefix . 'hasmethod-' . $expr->args[1]->value->value]];
}
} elseif (self::hasInArrayCheck($expr)) {
if ($first_var_name && isset($expr->args[1]->value->inferredType)) {
foreach ($expr->args[1]->value->inferredType->getTypes() as $atomic_type) {
@@ -1299,6 +1299,17 @@ public static function isAtomicContainedBy(
}
}
if (($container_type_part instanceof TString || $container_type_part instanceof TScalar)
&& $input_type_part instanceof TObjectWithProperties
&& isset($input_type_part->methods['__toString'])
) {
if ($atomic_comparison_result) {
$atomic_comparison_result->to_string_cast = true;
}
return true;
}
if ($container_type_part instanceof Type\Atomic\TFn && $input_type_part instanceof TCallable) {
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced = true;
@@ -344,6 +344,18 @@ public static function reconcile(
);
}
if (substr($assertion, 0, 10) === 'hasmethod-') {
return self::reconcileHasMethod(
$codebase,
substr($assertion, 10),
$existing_var_type,
$key,
$code_location,
$suppressed_issues,
$failed_reconciliation
);
}
if (($assertion === 'int' || $assertion === 'float')
&& $existing_var_type->from_calculation
&& $existing_var_type->hasInt()
@@ -782,6 +794,65 @@ private static function reconcileNonEmptyCountable(
return $existing_var_type;
}
/**
* @param string[] $suppressed_issues
* @param 0|1|2 $failed_reconciliation
*/
private static function reconcileHasMethod(
Codebase $codebase,
string $method_id,
Union $existing_var_type,
?string $key,
?CodeLocation $code_location,
array $suppressed_issues,
int &$failed_reconciliation
) : Union {
$old_var_type_string = $existing_var_type->getId();
$existing_var_atomic_types = $existing_var_type->getTypes();
$object_types = [];
$did_remove_type = false;
foreach ($existing_var_atomic_types as $type) {
if ($type instanceof TNamedObject
&& $codebase->classOrInterfaceExists($type->value)
&& $codebase->methodExists($type->value . '::__toString')
) {
$object_types[] = $type;
} elseif ($type instanceof TObject || $type instanceof TMixed) {
$object_types[] = new Atomic\TObjectWithProperties([], ['__toString' => true]);
$did_remove_type = true;
} elseif ($type instanceof TTemplateParam) {
$object_types[] = $type;
$did_remove_type = true;
} else {
$did_remove_type = true;
}
}
if (!$object_types || !$did_remove_type) {
if ($key && $code_location) {
self::triggerIssueForImpossible(
$existing_var_type,
$old_var_type_string,
$key,
'object',
!$did_remove_type,
$code_location,
$suppressed_issues
);
}
}
if ($object_types) {
return new Type\Union($object_types);
}
$failed_reconciliation = 2;
return Type::getMixed();
}
/**
* @param string[] $suppressed_issues
* @param 0|1|2 $failed_reconciliation
@@ -17,14 +17,21 @@ class TObjectWithProperties extends TObject
*/
public $properties;
/**
* @var array<string, bool>
*/
public $methods;
/**
* Constructs a new instance of a generic type
*
* @param array<string|int, Union> $properties
* @param array<string, bool> $methods
*/
public function __construct(array $properties)
public function __construct(array $properties, array $methods = [])
{
$this->properties = $properties;
$this->methods = $methods;
}
public function __toString()
@@ -35,24 +42,37 @@ public function __toString()
$extra_types = '&' . implode('&', $this->extra_types);
}
return 'object{' .
implode(
', ',
array_map(
/**
* @param string|int $name
* @param Union $type
*
* @return string
*/
function ($name, Union $type) {
return $name . ($type->possibly_undefined ? '?' : '') . ':' . $type;
},
array_keys($this->properties),
$this->properties
)
) .
'}' . $extra_types;
$properties_string = implode(
', ',
array_map(
/**
* @param string|int $name
* @param Union $type
*
* @return string
*/
function ($name, Union $type) {
return $name . ($type->possibly_undefined ? '?' : '') . ':' . $type;
},
array_keys($this->properties),
$this->properties
)
);
$methods_string = implode(
', ',
array_map(
function (string $name) {
return $name . '()';
},
array_keys($this->methods)
)
);
return 'object{'
. $properties_string . ($methods_string && $properties_string ? ', ' : '')
. $methods_string
. '}' . $extra_types;
}
public function getId()
@@ -63,24 +83,37 @@ public function getId()
$extra_types = '&' . implode('&', $this->extra_types);
}
return 'object{' .
implode(
', ',
array_map(
/**
* @param string|int $name
* @param Union $type
*
* @return string
*/
function ($name, Union $type) {
return $name . ($type->possibly_undefined ? '?' : '') . ':' . $type->getId();
},
array_keys($this->properties),
$this->properties
)
) .
'}' . $extra_types;
$properties_string = implode(
', ',
array_map(
/**
* @param string|int $name
* @param Union $type
*
* @return string
*/
function ($name, Union $type) {
return $name . ($type->possibly_undefined ? '?' : '') . ':' . $type->getId();
},
array_keys($this->properties),
$this->properties
)
);
$methods_string = implode(
', ',
array_map(
function (string $name) {
return $name . '()';
},
array_keys($this->methods)
)
);
return 'object{'
. $properties_string . ($methods_string && $properties_string ? ', ' : '')
. $methods_string
. '}' . $extra_types;
}
/**
@@ -185,6 +218,10 @@ public function equals(Atomic $other_type)
return false;
}
if ($this->methods !== $other_type->methods) {
return false;
}
foreach ($this->properties as $property_name => $property_type) {
if (!isset($other_type->properties[$property_name])) {
return false;
@@ -90,6 +90,16 @@ function foo(array $arr) : void {
echo (string) $i;
}',
],
'allowToStringAfterMethodExistsCheck' => [
'<?php
function getString(object $value) : ?string {
if (method_exists($value, "__toString")) {
return (string) $value;
}
return null;
}'
],
];
}
@@ -265,6 +275,17 @@ function bar($a) : ?string {
}',
'error_message' => 'PossiblyInvalidOperand',
],
'allowToStringAfterMethodExistsCheckWithTypo' => [
'<?php
function getString(object $value) : ?string {
if (method_exists($value, "__toStrong")) {
return (string) $value;
}
return null;
}',
'error_message' => 'InvalidCast',
],
];
}
}

0 comments on commit 4304183

Please sign in to comment.
You can’t perform that action at this time.