Skip to content

Commit

Permalink
Merge pull request #8688 from weirdan/fix-stringable-object
Browse files Browse the repository at this point in the history
Fixes #8575
  • Loading branch information
weirdan committed Nov 11, 2022
2 parents 55933a5 + 80750fd commit f49ff60
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 18 deletions.
Expand Up @@ -190,7 +190,7 @@ public static function analyze(
&& !$atomic_key_type instanceof TTemplateParam
&& !(
$atomic_key_type instanceof TObjectWithProperties
&& isset($atomic_key_type->methods['__toString'])
&& isset($atomic_key_type->methods['__tostring'])
)
) {
IssueBuffer::maybeAdd(
Expand Down
Expand Up @@ -632,9 +632,9 @@ private static function handleInvalidClass(

if ($lhs_type_part instanceof TObjectWithProperties
&& $stmt->name instanceof PhpParser\Node\Identifier
&& isset($lhs_type_part->methods[$stmt->name->name])
&& isset($lhs_type_part->methods[strtolower($stmt->name->name)])
) {
$result->existent_method_ids[] = $lhs_type_part->methods[$stmt->name->name];
$result->existent_method_ids[] = $lhs_type_part->methods[strtolower($stmt->name->name)];
} elseif (!$is_intersection) {
if ($stmt->name instanceof PhpParser\Node\Identifier) {
$codebase->analyzer->addMixedMemberName(
Expand Down
Expand Up @@ -808,7 +808,7 @@ public static function castStringAttempt(
}

if ($intersection_type instanceof TObjectWithProperties
&& isset($intersection_type->methods['__toString'])
&& isset($intersection_type->methods['__tostring'])
) {
$castable_types[] = new TString();

Expand Down
Expand Up @@ -832,7 +832,7 @@ public static function getArrayAccessTypeGivenOffset(
&& !$atomic_key_type instanceof TTemplateParam
&& !(
$atomic_key_type instanceof TObjectWithProperties
&& isset($atomic_key_type->methods['__toString'])
&& isset($atomic_key_type->methods['__tostring'])
)
) {
$bad_types[] = $atomic_key_type;
Expand Down
Expand Up @@ -66,7 +66,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
}

if ($atomic_type instanceof Type\Atomic\TObjectWithProperties
&& isset($atomic_type->methods['__toString'])
&& isset($atomic_type->methods['__tostring'])
) {
$has_tostring = true;
continue;
Expand Down
24 changes: 23 additions & 1 deletion src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php
Expand Up @@ -215,6 +215,28 @@ public static function isContainedBy(
return true;
}

if ($container_type_part instanceof TObjectWithProperties
&& $container_type_part->is_stringable_object_only
) {
if (($input_type_part instanceof TObjectWithProperties
&& $input_type_part->is_stringable_object_only)
|| ($input_type_part instanceof TNamedObject
&& $codebase->methodExists(new MethodIdentifier($input_type_part->value, '__tostring')))
) {
return true;
}
return false;
}

if ($container_type_part instanceof TNamedObject
&& $container_type_part->value === 'Stringable'
&& $codebase->analysis_php_version_id >= 8_00_00
&& $input_type_part instanceof TObjectWithProperties
&& $input_type_part->is_stringable_object_only
) {
return true;
}

if (($container_type_part instanceof TKeyedArray
&& $input_type_part instanceof TKeyedArray)
|| ($container_type_part instanceof TObjectWithProperties
Expand Down Expand Up @@ -615,7 +637,7 @@ public static function isContainedBy(
return true;
}
} elseif ($input_type_part instanceof TObjectWithProperties
&& isset($input_type_part->methods['__toString'])
&& isset($input_type_part->methods['__tostring'])
) {
if ($atomic_comparison_result) {
$atomic_comparison_result->to_string_cast = true;
Expand Down
13 changes: 7 additions & 6 deletions src/Psalm/Internal/Type/SimpleAssertionReconciler.php
Expand Up @@ -82,6 +82,7 @@
use function get_class;
use function min;
use function strpos;
use function strtolower;

/**
* This class receives a known type and an assertion (probably coming from AssertionFinder). The goal is to refine
Expand Down Expand Up @@ -802,10 +803,10 @@ private static function reconcileHasMethod(
} elseif ($extra_type instanceof TObjectWithProperties) {
$match_found = true;

if (!isset($extra_type->methods[$method_name])) {
if (!isset($extra_type->methods[strtolower($method_name)])) {
unset($extra_types[$k]);
$extra_type = $extra_type->setMethods(array_merge($extra_type->methods, [
$method_name => 'object::' . $method_name
strtolower($method_name) => 'object::' . $method_name
]));
$extra_types[$extra_type->getKey()] = $extra_type;
$did_remove_type = true;
Expand All @@ -816,7 +817,7 @@ private static function reconcileHasMethod(
if (!$match_found) {
$extra_type = new TObjectWithProperties(
[],
[$method_name => $type->value . '::' . $method_name]
[strtolower($method_name) => $type->value . '::' . $method_name]
);
$extra_types[$extra_type->getKey()] = $extra_type;
$did_remove_type = true;
Expand All @@ -826,17 +827,17 @@ private static function reconcileHasMethod(
}
$object_types[] = $type;
} elseif ($type instanceof TObjectWithProperties) {
if (!isset($type->methods[$method_name])) {
if (!isset($type->methods[strtolower($method_name)])) {
$type = $type->setMethods(array_merge($type->methods, [
$method_name => 'object::' . $method_name
strtolower($method_name) => 'object::' . $method_name
]));
$did_remove_type = true;
}
$object_types[] = $type;
} elseif ($type instanceof TObject || $type instanceof TMixed) {
$object_types[] = new TObjectWithProperties(
[],
[$method_name => 'object::' . $method_name]
[strtolower($method_name) => 'object::' . $method_name]
);
$did_remove_type = true;
} elseif ($type instanceof TString) {
Expand Down
20 changes: 17 additions & 3 deletions src/Psalm/Type/Atomic/TObjectWithProperties.php
Expand Up @@ -29,15 +29,18 @@ final class TObjectWithProperties extends TObject
public $properties;

/**
* @var array<string, string>
* @var array<lowercase-string, string>
*/
public $methods;

/** @var bool */
public $is_stringable_object_only = false;

/**
* Constructs a new instance of a generic type
*
* @param array<string|int, Union> $properties
* @param array<string, string> $methods
* @param array<lowercase-string, string> $methods
* @param array<string, TNamedObject|TTemplateParam|TIterable|TObjectWithProperties> $extra_types
*/
public function __construct(
Expand All @@ -50,6 +53,9 @@ public function __construct(
$this->methods = $methods;
$this->extra_types = $extra_types;
$this->from_docblock = $from_docblock;

$this->is_stringable_object_only =
$this->properties === [] && $this->methods === ['__tostring' => 'string'];
}

/**
Expand All @@ -62,11 +68,15 @@ public function setProperties(array $properties): self
}
$cloned = clone $this;
$cloned->properties = $properties;

$cloned->is_stringable_object_only =
$cloned->properties === [] && $cloned->methods === ['__tostring' => 'string'];

return $cloned;
}

/**
* @param array<string, string> $methods
* @param array<lowercase-string, string> $methods
*/
public function setMethods(array $methods): self
{
Expand All @@ -75,6 +85,10 @@ public function setMethods(array $methods): self
}
$cloned = clone $this;
$cloned->methods = $methods;

$cloned->is_stringable_object_only =
$cloned->properties === [] && $cloned->methods === ['__tostring' => 'string'];

return $cloned;
}

Expand Down
6 changes: 6 additions & 0 deletions tests/ArrayFunctionCallTest.php
Expand Up @@ -2536,6 +2536,12 @@ function merger(array $a, array $b) : array {
',
'error_message' => 'RawObjectIteration',
],
'implodeWithNonStringableArgs' => [
'code' => '<?php
implode(",", [new stdClass]);
',
'error_message' => 'InvalidArgument',
],
];
}
}
80 changes: 79 additions & 1 deletion tests/FunctionCallTest.php
Expand Up @@ -1907,6 +1907,56 @@ function baz(string $s) : void {
'$b===' => 'lowercase-string',
],
],
'passingStringableObjectToStringableParam' => [
'code' => '<?php
function acceptsStringable(Stringable $_p): void {}
/** @param stringable-object $p */
function f(object $p): void
{
f($p);
}
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'passingStringableToStringableObjectParam' => [
'code' => '<?php
/** @param stringable-object $_o */
function acceptsStringableObject(object $_o): void {}
function f(Stringable $o): void
{
acceptsStringableObject($o);
}
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'passingImplicitStringableObjectToStringableObjectParam' => [
'code' => '<?php
/** @param stringable-object $o */
function acceptsStringableObject(object $o): void {}
class C { public function __toString(): string { return __CLASS__; }}
acceptsStringableObject(new C);
',
],
'passingExplicitStringableObjectToStringableObjectParam' => [
'code' => '<?php
/** @param stringable-object $o */
function acceptsStringableObject(object $o): void {}
class C implements Stringable { public function __toString(): string { return __CLASS__; }}
acceptsStringableObject(new C);
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
];
}

Expand Down Expand Up @@ -2457,7 +2507,35 @@ function scope(int $mode){
'error_message' => 'TypeDoesNotContainType',
'ignored_issues' => [],
'php_version' => '8.1',
]
],
'passingObjectToStringableObjectParam' => [
'code' => '<?php
/** @param stringable-object $o */
function acceptsStringableObject(object $o): void {}
acceptsStringableObject((object)[]);
',
'error_message' => 'InvalidArgument',
],
'passingNonStringableObjectToStringableObjectParam' => [
'code' => '<?php
/** @param stringable-object $o */
function acceptsStringableObject(object $o): void {}
class C {}
acceptsStringableObject(new C);
',
'error_message' => 'InvalidArgument',
],
'passingStdClassToStringableObjectParam' => [
'code' => '<?php
/** @param stringable-object $o */
function acceptsStringableObject(object $o): void {}
acceptsStringableObject(new stdClass);
',
'error_message' => 'InvalidArgument',
],
];
}

Expand Down
2 changes: 1 addition & 1 deletion tests/MethodCallTest.php
Expand Up @@ -454,7 +454,7 @@ public function getId(): string;
function foo(ReturnsString $user, $a): string {
strlen($user->getId());
(is_object($a) && method_exists($a, "getS")) ? (string)$a->getS() : "";
(is_object($a) && method_exists($a, "getS")) ? (string)$a->GETS() : "";
return $user->getId();
}',
Expand Down
36 changes: 36 additions & 0 deletions tests/ReturnTypeTest.php
Expand Up @@ -1118,6 +1118,42 @@ function foo(array $x): array {
}
',
],
'returningExplicitStringableForStringableObjectReturnType' => [
'code' => '<?php
class C implements Stringable { public function __toString(): string { return __CLASS__; } }
/** @return stringable-object */
function f(): object {
return new C;
}
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'returningImplicitStringableForStringableObjectReturnType' => [
'code' => '<?php
class C { public function __toString(): string { return __CLASS__; } }
/** @return stringable-object */
function f(): object {
return new C;
}
',
],
'returningStringableObjectForStringableReturnType' => [
'code' => '<?php
class C implements Stringable { public function __toString(): string { return __CLASS__; } }
/** @param stringable-object $p */
function f(object $p): Stringable {
return $p;
}
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
];
}

Expand Down

0 comments on commit f49ff60

Please sign in to comment.