Skip to content

Commit

Permalink
Implement subtype checks for stringable-object
Browse files Browse the repository at this point in the history
  • Loading branch information
weirdan committed Nov 10, 2022
1 parent 512ad83 commit 80750fd
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 2 deletions.
22 changes: 22 additions & 0 deletions 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
14 changes: 14 additions & 0 deletions src/Psalm/Type/Atomic/TObjectWithProperties.php
Expand Up @@ -33,6 +33,9 @@ final class TObjectWithProperties extends TObject
*/
public $methods;

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

/**
* Constructs a new instance of a generic type
*
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,6 +68,10 @@ 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;
}

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 80750fd

Please sign in to comment.