mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
objects even with __toString methods cannot be cast to int/float
This commit is contained in:
parent
bf1c0320fd
commit
d69be4b9a2
@ -54,9 +54,18 @@ use function array_merge;
|
||||
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,
|
||||
@ -78,7 +87,6 @@ class CastAnalyzer
|
||||
|
||||
$type = self::castIntAttempt(
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$maybe_type,
|
||||
$stmt->expr,
|
||||
true
|
||||
@ -106,7 +114,6 @@ class CastAnalyzer
|
||||
|
||||
$type = self::castFloatAttempt(
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$maybe_type,
|
||||
$stmt->expr,
|
||||
true
|
||||
@ -292,7 +299,6 @@ class CastAnalyzer
|
||||
|
||||
public static function castIntAttempt(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Context $context,
|
||||
Union $stmt_type,
|
||||
PhpParser\Node\Expr $stmt,
|
||||
bool $explicit_cast = false
|
||||
@ -373,9 +379,7 @@ class CastAnalyzer
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($atomic_type instanceof TNamedObject
|
||||
|| $atomic_type instanceof TObjectWithProperties
|
||||
) {
|
||||
if ($atomic_type instanceof TNamedObject) {
|
||||
$intersection_types = [$atomic_type];
|
||||
|
||||
if ($atomic_type->extra_types) {
|
||||
@ -383,62 +387,25 @@ class CastAnalyzer
|
||||
}
|
||||
|
||||
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();
|
||||
// prevent "Could not get class storage for mixed"
|
||||
if (!$codebase->classExists($intersection_type->value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
continue 2;
|
||||
foreach (self::PSEUDO_CASTABLE_CLASSES as $pseudo_castable_class) {
|
||||
if (strtolower($intersection_type->value) === strtolower($pseudo_castable_class)
|
||||
|| $codebase->classExtends(
|
||||
$intersection_type->value,
|
||||
$pseudo_castable_class
|
||||
)
|
||||
) {
|
||||
$castable_types[] = new TInt();
|
||||
continue 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -519,7 +486,6 @@ class CastAnalyzer
|
||||
|
||||
public static function castFloatAttempt(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Context $context,
|
||||
Union $stmt_type,
|
||||
PhpParser\Node\Expr $stmt,
|
||||
bool $explicit_cast = false
|
||||
@ -599,9 +565,7 @@ class CastAnalyzer
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($atomic_type instanceof TNamedObject
|
||||
|| $atomic_type instanceof TObjectWithProperties
|
||||
) {
|
||||
if ($atomic_type instanceof TNamedObject) {
|
||||
$intersection_types = [$atomic_type];
|
||||
|
||||
if ($atomic_type->extra_types) {
|
||||
@ -609,62 +573,25 @@ class CastAnalyzer
|
||||
}
|
||||
|
||||
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();
|
||||
// prevent "Could not get class storage for mixed"
|
||||
if (!$codebase->classExists($intersection_type->value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
continue 2;
|
||||
foreach (self::PSEUDO_CASTABLE_CLASSES as $pseudo_castable_class) {
|
||||
if (strtolower($intersection_type->value) === strtolower($pseudo_castable_class)
|
||||
|| $codebase->classExtends(
|
||||
$intersection_type->value,
|
||||
$pseudo_castable_class
|
||||
)
|
||||
) {
|
||||
$castable_types[] = new TFloat();
|
||||
continue 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -505,6 +505,18 @@ class ToStringTest extends TestCase
|
||||
',
|
||||
'error_message' => 'ImplicitToStringCast'
|
||||
],
|
||||
'toStringTypecastNonString' => [
|
||||
'<?php
|
||||
class A {
|
||||
function __toString(): string {
|
||||
return "ha";
|
||||
}
|
||||
}
|
||||
|
||||
$foo = new A();
|
||||
echo (int) $foo;',
|
||||
'error_message' => 'InvalidCast',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user