Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make stringable-object equivalent to Stringable #8688

Merged
merged 2 commits into from Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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