Skip to content

Commit

Permalink
objects even with __toString methods cannot be cast to int/float
Browse files Browse the repository at this point in the history
  • Loading branch information
kkmuffme committed Sep 19, 2022
1 parent efab90e commit 94f56a7
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 118 deletions.
153 changes: 35 additions & 118 deletions src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php
Expand Up @@ -54,9 +54,18 @@
use function array_pop;
use function array_values;
use function get_class;
use function strtolower;

class CastAnalyzer
{
/** @var string[] */
private const PSEUDO_CASTABLE_CLASSES = [
'SimpleXMLElement',
'DOMNode',
'GMP',
'Decimal\Decimal',
];

public static function analyze(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\Cast $stmt,
Expand All @@ -78,7 +87,6 @@ public static function analyze(

$type = self::castIntAttempt(
$statements_analyzer,
$context,
$maybe_type,
$stmt->expr,
true
Expand Down Expand Up @@ -106,7 +114,6 @@ public static function analyze(

$type = self::castFloatAttempt(
$statements_analyzer,
$context,
$maybe_type,
$stmt->expr,
true
Expand Down Expand Up @@ -292,7 +299,6 @@ public static function analyze(

public static function castIntAttempt(
StatementsAnalyzer $statements_analyzer,
Context $context,
Union $stmt_type,
PhpParser\Node\Expr $stmt,
bool $explicit_cast = false
Expand Down Expand Up @@ -373,72 +379,28 @@ public static function castIntAttempt(
continue;
}

if ($atomic_type instanceof TNamedObject
|| $atomic_type instanceof TObjectWithProperties
) {
if ($atomic_type instanceof TNamedObject) {
$intersection_types = [$atomic_type];

if ($atomic_type->extra_types) {
$intersection_types = array_merge($intersection_types, $atomic_type->extra_types);
}

foreach ($intersection_types as $intersection_type) {
if ($intersection_type instanceof TNamedObject) {
$intersection_method_id = new MethodIdentifier(
$intersection_type->value,
'__tostring'
);

if ($codebase->methods->methodExists(
$intersection_method_id,
$context->calling_method_id,
new CodeLocation($statements_analyzer->getSource(), $stmt)
)) {
$return_type = $codebase->methods->getMethodReturnType(
$intersection_method_id,
$self_class
) ?? Type::getString();

$declaring_method_id = $codebase->methods->getDeclaringMethodId($intersection_method_id);

MethodCallReturnTypeFetcher::taintMethodCallResult(
$statements_analyzer,
$return_type,
$stmt,
$stmt,
[],
$intersection_method_id,
$declaring_method_id,
$intersection_type->value . '::__toString',
$context
);

if ($statements_analyzer->data_flow_graph) {
$parent_nodes = array_merge($return_type->parent_nodes, $parent_nodes);
}

foreach ($return_type->getAtomicTypes() as $sub_atomic_type) {
if ($sub_atomic_type instanceof TLiteralString) {
$valid_ints[] = new TLiteralInt((int) $sub_atomic_type->value);
} elseif ($sub_atomic_type instanceof TNumericString) {
$castable_types[] = new TInt();
} else {
// any normal string is technically $valid_int[] = new TLiteralInt(0);
// however we cannot be certain that it's not inferred, therefore less strict
$castable_types[] = new TInt();
}
}

continue 2;
}
if (!$intersection_type instanceof TNamedObject) {
continue;
}

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

continue 2;
foreach (self::PSEUDO_CASTABLE_CLASSES as $pseudo_castable_class) {
if (strtolower($intersection_type->value) === strtolower($pseudo_castable_class)
|| $statements_analyzer->getCodebase()->classExtends(
$intersection_type->value,
$pseudo_castable_class
)
) {
$castable_types[] = new TInt();
continue 3;
}
}
}
}
Expand Down Expand Up @@ -519,7 +481,6 @@ public static function castIntAttempt(

public static function castFloatAttempt(
StatementsAnalyzer $statements_analyzer,
Context $context,
Union $stmt_type,
PhpParser\Node\Expr $stmt,
bool $explicit_cast = false
Expand Down Expand Up @@ -599,72 +560,28 @@ public static function castFloatAttempt(
continue;
}

if ($atomic_type instanceof TNamedObject
|| $atomic_type instanceof TObjectWithProperties
) {
if ($atomic_type instanceof TNamedObject) {
$intersection_types = [$atomic_type];

if ($atomic_type->extra_types) {
$intersection_types = array_merge($intersection_types, $atomic_type->extra_types);
}

foreach ($intersection_types as $intersection_type) {
if ($intersection_type instanceof TNamedObject) {
$intersection_method_id = new MethodIdentifier(
$intersection_type->value,
'__tostring'
);

if ($codebase->methods->methodExists(
$intersection_method_id,
$context->calling_method_id,
new CodeLocation($statements_analyzer->getSource(), $stmt)
)) {
$return_type = $codebase->methods->getMethodReturnType(
$intersection_method_id,
$self_class
) ?? Type::getString();

$declaring_method_id = $codebase->methods->getDeclaringMethodId($intersection_method_id);

MethodCallReturnTypeFetcher::taintMethodCallResult(
$statements_analyzer,
$return_type,
$stmt,
$stmt,
[],
$intersection_method_id,
$declaring_method_id,
$intersection_type->value . '::__toString',
$context
);

if ($statements_analyzer->data_flow_graph) {
$parent_nodes = array_merge($return_type->parent_nodes, $parent_nodes);
}

foreach ($return_type->getAtomicTypes() as $sub_atomic_type) {
if ($sub_atomic_type instanceof TLiteralString) {
$valid_floats[] = new TLiteralFloat((float) $sub_atomic_type->value);
} elseif ($sub_atomic_type instanceof TNumericString) {
$castable_types[] = new TFloat();
} else {
// any normal string is technically $valid_int[] = new TLiteralInt(0);
// however we cannot be certain that it's not inferred, therefore less strict
$castable_types[] = new TFloat();
}
}

continue 2;
}
if (!$intersection_type instanceof TNamedObject) {
continue;
}

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

continue 2;
foreach (self::PSEUDO_CASTABLE_CLASSES as $pseudo_castable_class) {
if (strtolower($intersection_type->value) === strtolower($pseudo_castable_class)
|| $statements_analyzer->getCodebase()->classExtends(
$intersection_type->value,
$pseudo_castable_class
)
) {
$castable_types[] = new TFloat();
continue 3;
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions tests/ToStringTest.php
Expand Up @@ -505,6 +505,18 @@ public function __toString(): string
',
'error_message' => 'ImplicitToStringCast'
],
'toStringTypecastNonString' => [
'<?php
class A {
function __toString(): string {
return "ha";
}
}
$foo = new A();
echo (int) $foo;',
'error_message' => 'InvalidCast',
],
];
}
}

0 comments on commit 94f56a7

Please sign in to comment.