From e4b7cdf26ff0f444ed1277ca2d090d0805a81e16 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:43:37 +0200 Subject: [PATCH 01/10] fix type for (string) true --- .../Statements/Expression/CastAnalyzer.php | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 439ee3bf19e..b75176e2b41 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -23,6 +23,7 @@ use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TBool; +use Psalm\Type\Atomic\TClosedResource; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; @@ -33,6 +34,9 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNonEmptyList; +use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; @@ -42,6 +46,7 @@ use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Atomic\TTrue; use Psalm\Type\Union; use function array_merge; @@ -352,8 +357,28 @@ public static function castStringAttempt( continue; } + if ($atomic_type instanceof TTrue + ) { + $valid_strings[] = new TLiteralString('1'); + continue; + } + + if ($atomic_type instanceof TBool + ) { + $valid_strings[] = new TLiteralString('1'); + $valid_strings[] = new TLiteralString(''); + continue; + } + + if ($atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + ) { + $castable_types[] = new TNonEmptyString(); + + continue; + } + if ($atomic_type instanceof TMixed - || $atomic_type instanceof TResource || $atomic_type instanceof Scalar ) { $castable_types[] = new TString(); From 3bec76acb8c89637d3c6c4817f6a9f18ec49f3e4 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 22:15:41 +0200 Subject: [PATCH 02/10] fix invalid casts for int https://psalm.dev/r/0d284c6f48 --- .../Statements/Expression/CastAnalyzer.php | 272 ++++++++++++++++-- 1 file changed, 243 insertions(+), 29 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index b75176e2b41..4d45367cad5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -52,7 +52,6 @@ use function array_merge; use function array_pop; use function array_values; -use function count; use function get_class; class CastAnalyzer @@ -67,45 +66,27 @@ public static function analyze( return false; } - $as_int = true; - $valid_int_type = null; $maybe_type = $statements_analyzer->node_data->getType($stmt->expr); if ($maybe_type) { if ($maybe_type->isInt()) { - $valid_int_type = $maybe_type; if (!$maybe_type->from_calculation) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } } - if (count($maybe_type->getAtomicTypes()) === 1 - && $maybe_type->getSingleAtomic() instanceof TBool) { - $as_int = false; - $type = new Union([ - new TLiteralInt(0), - new TLiteralInt(1), - ]); - - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes; - } - - $statements_analyzer->node_data->setType($stmt, $type); - } + $type = self::castIntAttempt( + $statements_analyzer, + $context, + $maybe_type, + $stmt->expr, + true + ); + } else { + $type = Type::getInt(); } - if ($as_int) { - $type = $valid_int_type ?? Type::getInt(); - - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes ?? []; - } - - $statements_analyzer->node_data->setType($stmt, $type); - } + $statements_analyzer->node_data->setType($stmt, $type); return true; } @@ -305,6 +286,239 @@ public static function analyze( return false; } + public static function castIntAttempt( + StatementsAnalyzer $statements_analyzer, + Context $context, + Union $stmt_type, + PhpParser\Node\Expr $stmt, + bool $explicit_cast = false + ): Union { + $codebase = $statements_analyzer->getCodebase(); + + $possibly_unwanted_cast = []; + $invalid_casts = []; + $valid_ints = []; + $castable_types = []; + + $atomic_types = $stmt_type->getAtomicTypes(); + + $parent_nodes = []; + + if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { + $parent_nodes = $stmt_type->parent_nodes; + } + + while ($atomic_types) { + $atomic_type = array_pop($atomic_types); + + if ($atomic_type instanceof TInt) { + $valid_ints[] = $atomic_type; + + continue; + } + + if ($atomic_type instanceof TFloat) { + if ($atomic_type instanceof TLiteralFloat) { + $valid_ints[] = new TLiteralInt((int) $atomic_type->value); + } else { + $castable_types[] = new TInt(); + } + + continue; + } + + if ($atomic_type instanceof TString) { + if ($atomic_type instanceof TLiteralString && (int) $atomic_type->value !== 0) { + $valid_ints[] = new TLiteralInt((int) $atomic_type->value); + } elseif ($atomic_type instanceof TNumericString) { + $castable_types[] = new TInt(); + } else { + // any normal string + $valid_ints[] = new TLiteralInt(0); + } + + continue; + } + + if ($atomic_type instanceof TNull || $atomic_type instanceof TFalse) { + $valid_ints[] = new TLiteralInt(0); + continue; + } + + if ($atomic_type instanceof TTrue) { + $valid_ints[] = new TLiteralInt(1); + continue; + } + + if ($atomic_type instanceof TBool) { + // do NOT use TIntRange here, as it will cause invalid behavior, e.g. bitwiseAssignment + $valid_ints[] = new TLiteralInt(0); + $valid_ints[] = new TLiteralInt(1); + continue; + } + + // could be invalid, but allow it, as it is allowed for TString below too + if ($atomic_type instanceof TMixed + || $atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + || $atomic_type instanceof Scalar + ) { + $castable_types[] = new TInt(); + + continue; + } + + if ($atomic_type instanceof TNamedObject + || $atomic_type instanceof TObjectWithProperties + ) { + $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 + && (int) $sub_atomic_type->value !== 0 + ) { + $valid_ints[] = new TLiteralInt((int) $sub_atomic_type->value); + } elseif ($sub_atomic_type instanceof TNumericString) { + $castable_types[] = new TInt(); + } else { + $valid_ints[] = new TLiteralInt(0); + } + } + + continue 2; + } + } + + if ($intersection_type instanceof TObjectWithProperties + && isset($intersection_type->methods['__toString']) + ) { + $castable_types[] = new TInt(); + + continue 2; + } + } + } + + if ($atomic_type instanceof TNonEmptyArray + || $atomic_type instanceof TNonEmptyList + ) { + $possibly_unwanted_cast[] = $atomic_type->getId(); + + $valid_ints[] = new TLiteralInt(1); + + continue; + } + + if ($atomic_type instanceof TArray + || $atomic_type instanceof TList + || $atomic_type instanceof TKeyedArray + ) { + // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not + // welcome to off-by-one hell if that happens :-) + $possibly_unwanted_cast[] = $atomic_type->getId(); + + $valid_ints[] = new TLiteralInt(0); + $valid_ints[] = new TLiteralInt(1); + + continue; + } + + if ($atomic_type instanceof TTemplateParam) { + $atomic_types = array_merge($atomic_types, $atomic_type->as->getAtomicTypes()); + + continue; + } + + $invalid_casts[] = $atomic_type->getId(); + } + + if ($invalid_casts) { + if ( $valid_ints || $castable_types ) { + IssueBuffer::maybeAdd( + new PossiblyInvalidCast( + $invalid_casts[0] . ' cannot be cast to int', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } else { + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to int', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } + } elseif (!empty($possibly_unwanted_cast)) { + IssueBuffer::maybeAdd( + new PossiblyInvalidCast( + 'Casting ' . $possibly_unwanted_cast[0] . ' to int has possibly unintended value of 1', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } elseif ($explicit_cast && !$castable_types) { + // todo: emit error here + } + + $valid_types = array_merge($valid_ints, $castable_types); + + if (!$valid_types) { + $int_type = Type::getInt(); + } else { + $int_type = TypeCombiner::combine( + $valid_types, + $codebase + ); + } + + if ($statements_analyzer->data_flow_graph) { + $int_type->parent_nodes = $parent_nodes; + } + + return $int_type; + } + public static function castStringAttempt( StatementsAnalyzer $statements_analyzer, Context $context, From 39ec75523e2aa7134b0e785cf99bb626df8b5ca8 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 22:30:45 +0200 Subject: [PATCH 03/10] same for float --- .../Statements/Expression/CastAnalyzer.php | 247 +++++++++++++++++- 1 file changed, 241 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 4d45367cad5..3d26e272e95 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -102,13 +102,16 @@ public static function analyze( if ($maybe_type->isFloat()) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } - } - - $type = Type::getFloat(); - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes ?? []; + $type = self::castFloatAttempt( + $statements_analyzer, + $context, + $maybe_type, + $stmt->expr, + true + ); + } else { + $type = Type::getFloat(); } $statements_analyzer->node_data->setType($stmt, $type); @@ -519,6 +522,238 @@ public static function castIntAttempt( return $int_type; } + public static function castFloatAttempt( + StatementsAnalyzer $statements_analyzer, + Context $context, + Union $stmt_type, + PhpParser\Node\Expr $stmt, + bool $explicit_cast = false + ): Union { + $codebase = $statements_analyzer->getCodebase(); + + $possibly_unwanted_cast = []; + $invalid_casts = []; + $valid_floats = []; + $castable_types = []; + + $atomic_types = $stmt_type->getAtomicTypes(); + + $parent_nodes = []; + + if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { + $parent_nodes = $stmt_type->parent_nodes; + } + + while ($atomic_types) { + $atomic_type = array_pop($atomic_types); + + if ($atomic_type instanceof TFloat) { + $valid_floats[] = $atomic_type; + + continue; + } + + if ($atomic_type instanceof TInt) { + if ($atomic_type instanceof TLiteralInt) { + $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); + } else { + $castable_types[] = new TFloat(); + } + + continue; + } + + if ($atomic_type instanceof TString) { + if ($atomic_type instanceof TLiteralString && (float) $atomic_type->value !== 0.0) { + $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); + } elseif ($atomic_type instanceof TNumericString) { + $castable_types[] = new TFloat(); + } else { + // any normal string + $valid_floats[] = new TLiteralFloat(0.0); + } + + continue; + } + + if ($atomic_type instanceof TNull || $atomic_type instanceof TFalse) { + $valid_floats[] = new TLiteralFloat(0.0); + continue; + } + + if ($atomic_type instanceof TTrue) { + $valid_floats[] = new TLiteralFloat(1.0); + continue; + } + + if ($atomic_type instanceof TBool) { + $valid_floats[] = new TLiteralFloat(0.0); + $valid_floats[] = new TLiteralFloat(1.0); + continue; + } + + // could be invalid, but allow it, as it is allowed for TString below too + if ($atomic_type instanceof TMixed + || $atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + || $atomic_type instanceof Scalar + ) { + $castable_types[] = new TFloat(); + + continue; + } + + if ($atomic_type instanceof TNamedObject + || $atomic_type instanceof TObjectWithProperties + ) { + $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 + && (float) $sub_atomic_type->value !== 0.0 + ) { + $valid_floats[] = new TLiteralFloat((float) $sub_atomic_type->value); + } elseif ($sub_atomic_type instanceof TNumericString) { + $castable_types[] = new TFloat(); + } else { + $valid_floats[] = new TLiteralFloat(0.0); + } + } + + continue 2; + } + } + + if ($intersection_type instanceof TObjectWithProperties + && isset($intersection_type->methods['__toString']) + ) { + $castable_types[] = new TFloat(); + + continue 2; + } + } + } + + if ($atomic_type instanceof TNonEmptyArray + || $atomic_type instanceof TNonEmptyList + ) { + $possibly_unwanted_cast[] = $atomic_type->getId(); + + $valid_floats[] = new TLiteralFloat(1.0); + + continue; + } + + if ($atomic_type instanceof TArray + || $atomic_type instanceof TList + || $atomic_type instanceof TKeyedArray + ) { + // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not + // welcome to off-by-one hell if that happens :-) + $possibly_unwanted_cast[] = $atomic_type->getId(); + + $valid_floats[] = new TLiteralFloat(0.0); + $valid_floats[] = new TLiteralFloat(1.0); + + continue; + } + + if ($atomic_type instanceof TTemplateParam) { + $atomic_types = array_merge($atomic_types, $atomic_type->as->getAtomicTypes()); + + continue; + } + + $invalid_casts[] = $atomic_type->getId(); + } + + if ($invalid_casts) { + if ( $valid_floats || $castable_types ) { + IssueBuffer::maybeAdd( + new PossiblyInvalidCast( + $invalid_casts[0] . ' cannot be cast to float', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } else { + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to float', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } + } elseif (!empty($possibly_unwanted_cast)) { + IssueBuffer::maybeAdd( + new PossiblyInvalidCast( + 'Casting ' . $possibly_unwanted_cast[0] . ' to float has possibly unintended value of 1.0', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } elseif ($explicit_cast && !$castable_types) { + // todo: emit error here + } + + $valid_types = array_merge($valid_floats, $castable_types); + + if (!$valid_types) { + $float_type = Type::getFloat(); + } else { + $float_type = TypeCombiner::combine( + $valid_types, + $codebase + ); + } + + if ($statements_analyzer->data_flow_graph) { + $float_type->parent_nodes = $parent_nodes; + } + + return $float_type; + } + public static function castStringAttempt( StatementsAnalyzer $statements_analyzer, Context $context, From d32efb0619dbc2ac277d3283a04c13f55c7a76e0 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 23:48:49 +0200 Subject: [PATCH 04/10] float/int always 1 on "error", no PossiblyInvalidCasts by default --- .../Statements/Expression/CastAnalyzer.php | 58 +++++++------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 3d26e272e95..f5af439823b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -471,32 +471,25 @@ public static function castIntAttempt( continue; } + // always 1 for "error" cases + $valid_ints[] = new TLiteralInt(1); + $invalid_casts[] = $atomic_type->getId(); } if ($invalid_casts) { - if ( $valid_ints || $castable_types ) { - IssueBuffer::maybeAdd( - new PossiblyInvalidCast( - $invalid_casts[0] . ' cannot be cast to int', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) - ), - $statements_analyzer->getSuppressedIssues() - ); - } else { - IssueBuffer::maybeAdd( - new InvalidCast( - $invalid_casts[0] . ' cannot be cast to int', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) - ), - $statements_analyzer->getSuppressedIssues() - ); - } + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to int', + new CodeLocation($statements_analyzer->getSource(), $stmt) + ), + $statements_analyzer->getSuppressedIssues() + ); } elseif (!empty($possibly_unwanted_cast)) { IssueBuffer::maybeAdd( new PossiblyInvalidCast( 'Casting ' . $possibly_unwanted_cast[0] . ' to int has possibly unintended value of 1', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) + new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); @@ -703,32 +696,25 @@ public static function castFloatAttempt( continue; } + // always 1.0 for "error" cases + $valid_floats[] = new TLiteralFloat(1.0); + $invalid_casts[] = $atomic_type->getId(); } if ($invalid_casts) { - if ( $valid_floats || $castable_types ) { - IssueBuffer::maybeAdd( - new PossiblyInvalidCast( - $invalid_casts[0] . ' cannot be cast to float', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) - ), - $statements_analyzer->getSuppressedIssues() - ); - } else { - IssueBuffer::maybeAdd( - new InvalidCast( - $invalid_casts[0] . ' cannot be cast to float', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) - ), - $statements_analyzer->getSuppressedIssues() - ); - } + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to float', + new CodeLocation($statements_analyzer->getSource(), $stmt) + ), + $statements_analyzer->getSuppressedIssues() + ); } elseif (!empty($possibly_unwanted_cast)) { IssueBuffer::maybeAdd( new PossiblyInvalidCast( 'Casting ' . $possibly_unwanted_cast[0] . ' to float has possibly unintended value of 1.0', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) + new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); From c3eebe25792c6a1a6b2ac6f23e1c258fa280ff36 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 4 Aug 2022 00:14:06 +0200 Subject: [PATCH 05/10] be less strict for generic string type --- .../Statements/Expression/CastAnalyzer.php | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index f5af439823b..61c28c0a727 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -331,13 +331,14 @@ public static function castIntAttempt( } if ($atomic_type instanceof TString) { - if ($atomic_type instanceof TLiteralString && (int) $atomic_type->value !== 0) { + if ($atomic_type instanceof TLiteralString) { $valid_ints[] = new TLiteralInt((int) $atomic_type->value); } elseif ($atomic_type instanceof TNumericString) { $castable_types[] = new TInt(); } else { - // any normal string - $valid_ints[] = new TLiteralInt(0); + // 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; @@ -416,14 +417,14 @@ public static function castIntAttempt( } foreach ($return_type->getAtomicTypes() as $sub_atomic_type) { - if ($sub_atomic_type instanceof TLiteralString - && (int) $sub_atomic_type->value !== 0 - ) { + 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 { - $valid_ints[] = new TLiteralInt(0); + // 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(); } } @@ -557,13 +558,14 @@ public static function castFloatAttempt( } if ($atomic_type instanceof TString) { - if ($atomic_type instanceof TLiteralString && (float) $atomic_type->value !== 0.0) { + if ($atomic_type instanceof TLiteralString) { $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); } elseif ($atomic_type instanceof TNumericString) { $castable_types[] = new TFloat(); } else { - // any normal string - $valid_floats[] = new TLiteralFloat(0.0); + // any normal string is technically $valid_floats[] = new TLiteralFloat(0.0); + // however we cannot be certain that it's not inferred, therefore less strict + $castable_types[] = new TFloat(); } continue; @@ -641,14 +643,14 @@ public static function castFloatAttempt( } foreach ($return_type->getAtomicTypes() as $sub_atomic_type) { - if ($sub_atomic_type instanceof TLiteralString - && (float) $sub_atomic_type->value !== 0.0 - ) { + 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 { - $valid_floats[] = new TLiteralFloat(0.0); + // 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(); } } From 7cdad99645dd48dab1d639f97f89949e3155a02b Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Fri, 9 Sep 2022 03:04:14 +0200 Subject: [PATCH 06/10] add RiskyCast --- config.xsd | 1 + docs/running_psalm/error_levels.md | 1 + docs/running_psalm/issues.md | 1 + docs/running_psalm/issues/RiskyCast.md | 17 +++++++++++++ .../Statements/Expression/CastAnalyzer.php | 25 ++++++++++--------- src/Psalm/Issue/RiskyCast.php | 9 +++++++ 6 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 docs/running_psalm/issues/RiskyCast.md create mode 100644 src/Psalm/Issue/RiskyCast.php diff --git a/config.xsd b/config.xsd index c9a4cd88895..5a0dab58d68 100644 --- a/config.xsd +++ b/config.xsd @@ -462,6 +462,7 @@ + diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index c58cca026ff..38e628bfd93 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -187,6 +187,7 @@ These issues are treated as errors at level 3 and below. - [PossiblyUndefinedMethod](issues/PossiblyUndefinedMethod.md) - [PossiblyUndefinedVariable](issues/PossiblyUndefinedVariable.md) - [PropertyTypeCoercion](issues/PropertyTypeCoercion.md) + - [RiskyCast](issues/RiskyCast.md) ## Errors ignored at level 5 and higher diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index 50385f60e31..20ddd0eca76 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -214,6 +214,7 @@ - [RedundantPropertyInitializationCheck](issues/RedundantPropertyInitializationCheck.md) - [ReferenceConstraintViolation](issues/ReferenceConstraintViolation.md) - [ReservedWord](issues/ReservedWord.md) + - [RiskyCast](issues/RiskyCast.md) - [StringIncrement](issues/StringIncrement.md) - [TaintedCallable](issues/TaintedCallable.md) - [TaintedCookie](issues/TaintedCookie.md) diff --git a/docs/running_psalm/issues/RiskyCast.md b/docs/running_psalm/issues/RiskyCast.md new file mode 100644 index 00000000000..ef90d6e37b8 --- /dev/null +++ b/docs/running_psalm/issues/RiskyCast.md @@ -0,0 +1,17 @@ +# RiskyCast + +Emitted when attempting to cast an array to int or float + +```php +getCodebase(); - $possibly_unwanted_cast = []; + $risky_cast = []; $invalid_casts = []; $valid_ints = []; $castable_types = []; @@ -445,7 +446,7 @@ public static function castIntAttempt( if ($atomic_type instanceof TNonEmptyArray || $atomic_type instanceof TNonEmptyList ) { - $possibly_unwanted_cast[] = $atomic_type->getId(); + $risky_cast[] = $atomic_type->getId(); $valid_ints[] = new TLiteralInt(1); @@ -458,7 +459,7 @@ public static function castIntAttempt( ) { // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not // welcome to off-by-one hell if that happens :-) - $possibly_unwanted_cast[] = $atomic_type->getId(); + $risky_cast[] = $atomic_type->getId(); $valid_ints[] = new TLiteralInt(0); $valid_ints[] = new TLiteralInt(1); @@ -486,10 +487,10 @@ public static function castIntAttempt( ), $statements_analyzer->getSuppressedIssues() ); - } elseif (!empty($possibly_unwanted_cast)) { + } elseif ($risky_cast) { IssueBuffer::maybeAdd( - new PossiblyInvalidCast( - 'Casting ' . $possibly_unwanted_cast[0] . ' to int has possibly unintended value of 1', + new RiskyCast( + 'Casting ' . $risky_cast[0] . ' to int has possibly unintended value of 0/1', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() @@ -525,7 +526,7 @@ public static function castFloatAttempt( ): Union { $codebase = $statements_analyzer->getCodebase(); - $possibly_unwanted_cast = []; + $risky_cast = []; $invalid_casts = []; $valid_floats = []; $castable_types = []; @@ -671,7 +672,7 @@ public static function castFloatAttempt( if ($atomic_type instanceof TNonEmptyArray || $atomic_type instanceof TNonEmptyList ) { - $possibly_unwanted_cast[] = $atomic_type->getId(); + $risky_cast[] = $atomic_type->getId(); $valid_floats[] = new TLiteralFloat(1.0); @@ -684,7 +685,7 @@ public static function castFloatAttempt( ) { // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not // welcome to off-by-one hell if that happens :-) - $possibly_unwanted_cast[] = $atomic_type->getId(); + $risky_cast[] = $atomic_type->getId(); $valid_floats[] = new TLiteralFloat(0.0); $valid_floats[] = new TLiteralFloat(1.0); @@ -712,10 +713,10 @@ public static function castFloatAttempt( ), $statements_analyzer->getSuppressedIssues() ); - } elseif (!empty($possibly_unwanted_cast)) { + } elseif ($risky_cast) { IssueBuffer::maybeAdd( - new PossiblyInvalidCast( - 'Casting ' . $possibly_unwanted_cast[0] . ' to float has possibly unintended value of 1.0', + new RiskyCast( + 'Casting ' . $risky_cast[0] . ' to float has possibly unintended value of 0.0/1.0', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() diff --git a/src/Psalm/Issue/RiskyCast.php b/src/Psalm/Issue/RiskyCast.php new file mode 100644 index 00000000000..6576d430aeb --- /dev/null +++ b/src/Psalm/Issue/RiskyCast.php @@ -0,0 +1,9 @@ + Date: Fri, 9 Sep 2022 04:22:54 +0200 Subject: [PATCH 07/10] fix psalm-internal risky casts --- src/Psalm/Internal/Cli/LanguageServer.php | 3 ++- src/Psalm/Internal/Cli/Psalter.php | 3 ++- src/Psalm/Internal/Cli/Refactor.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 634cdee16a4..1a0144def8c 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -32,6 +32,7 @@ use function in_array; use function ini_set; use function is_array; +use function is_numeric; use function is_string; use function preg_replace; use function realpath; @@ -298,7 +299,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class $find_unused_code = 'auto'; } - if (isset($options['disable-on-change'])) { + if (isset($options['disable-on-change']) && is_numeric($options['disable-on-change'])) { $project_analyzer->onchange_line_limit = (int) $options['disable-on-change']; } diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index 7db204c5f61..12e025fce7e 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -45,6 +45,7 @@ use function ini_set; use function is_array; use function is_dir; +use function is_numeric; use function is_string; use function microtime; use function pathinfo; @@ -230,7 +231,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class chdir($current_dir); } - $threads = isset($options['threads']) ? (int)$options['threads'] : 1; + $threads = isset($options['threads']) && is_numeric($options['threads']) ? (int)$options['threads'] : 1; if (isset($options['no-cache'])) { $providers = new Providers( diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 864e9d4aebd..a3b5a114b37 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -35,6 +35,7 @@ use function in_array; use function ini_set; use function is_array; +use function is_numeric; use function is_string; use function max; use function microtime; @@ -284,7 +285,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class chdir($current_dir); } - $threads = isset($options['threads']) + $threads = isset($options['threads']) && is_numeric($options['threads']) ? (int)$options['threads'] : max(1, ProjectAnalyzer::getCpuCount() - 2); From bf1c0320fd7584aa6f287946e44b34f3f2c1872e Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Sun, 16 Jan 2022 21:33:04 +0100 Subject: [PATCH 08/10] Cherry-pick: Try to provide literal int types when possible (fixes #6966) (#7071) * Fixed vimeo/psalm#6966 * Only accept >= 0 values for mode argument in round() * Made round() only return float or literal float values and remove unneeded test * Registered RoundReturnTypeProvider * Updated cast analyzer to handle single string literal int values as literal ints * Fixed psalm errors * Fix invalid property accesses * Addressed comments * Added Tests * Marked RoundReturnTypeProvider as internal * Fixed CS --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- .../Provider/FunctionReturnTypeProvider.php | 2 + .../RoundReturnTypeProvider.php | 78 +++++++++++++++++++ tests/FunctionCallTest.php | 9 ++- tests/TypeReconciliation/ValueTest.php | 8 ++ 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index df6b3456d8a..df1739c6609 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -11714,7 +11714,7 @@ 'rewind' => ['bool', 'stream'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'directory'=>'string', 'context='=>'resource'], -'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'int'], +'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'0|positive-int'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 036c5c710c7..a8ad6acd48e 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14751,7 +14751,7 @@ 'rewind' => ['bool', 'stream'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'directory'=>'string', 'context='=>'resource'], - 'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'int'], + 'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'0|positive-int'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 8b124ea5532..e4636d5c3a0 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -35,6 +35,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\MktimeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ParseUrlReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\RandReturnTypeProvider; +use Psalm\Internal\Provider\ReturnTypeProvider\RoundReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\StrReplaceReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\StrTrReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\TriggerErrorReturnTypeProvider; @@ -109,6 +110,7 @@ public function __construct() $this->registerClass(TriggerErrorReturnTypeProvider::class); $this->registerClass(RandReturnTypeProvider::class); $this->registerClass(InArrayReturnTypeProvider::class); + $this->registerClass(RoundReturnTypeProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php new file mode 100644 index 00000000000..eb8ee561f54 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php @@ -0,0 +1,78 @@ + + */ + public static function getFunctionIds(): array + { + return ['round']; + } + + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Type\Union + { + $call_args = $event->getCallArgs(); + if (count($call_args) === 0) { + return null; + } + + $statements_source = $event->getStatementsSource(); + $nodeTypeProvider = $statements_source->getNodeTypeProvider(); + + $num_arg = $nodeTypeProvider->getType($call_args[0]->value); + + $precision_val = 0; + if ($statements_source instanceof StatementsAnalyzer && count($call_args) > 1) { + $type = $statements_source->node_data->getType($call_args[1]->value); + + if ($type !== null && $type->isSingle()) { + $atomic_type = array_values($type->getAtomicTypes())[0]; + if ($atomic_type instanceof Type\Atomic\TLiteralInt) { + $precision_val = $atomic_type->value; + } + } + } + + $mode_val = PHP_ROUND_HALF_UP; + if ($statements_source instanceof StatementsAnalyzer && count($call_args) > 2) { + $type = $statements_source->node_data->getType($call_args[2]->value); + + if ($type !== null && $type->isSingle()) { + $atomic_type = array_values($type->getAtomicTypes())[0]; + if ($atomic_type instanceof Type\Atomic\TLiteralInt) { + /** @var positive-int|0 $mode_val */ + $mode_val = $atomic_type->value; + } + } + } + + if ($num_arg !== null && $num_arg->isSingle()) { + $num_type = array_values($num_arg->getAtomicTypes())[0]; + if ($num_type instanceof Type\Atomic\TLiteralFloat || $num_type instanceof Type\Atomic\TLiteralInt) { + $rounded_val = round($num_type->value, $precision_val, $mode_val); + return new Type\Union([new Type\Atomic\TLiteralFloat($rounded_val)]); + } + } + + return new Type\Union([new Type\Atomic\TFloat()]); + } +} diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index e57a1d432b9..9b457ce600d 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -31,7 +31,6 @@ function filter(array $strings): array { } ' ], - 'typedArrayWithDefault' => [ ' 'lowercase-string', ], ], + 'round_literalValue' => [ + ' [ + '$a===' => 'float(10.36)', + ], + ], ]; } diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index b5963ac4125..cda14767394 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -909,6 +909,14 @@ function foo(string $s) : void { $interval = \DateInterval::createFromDateString("30 дней"); if ($interval === false) {}', ], + 'literalInt' => [ + ' [ + '$a===' => '5', + ], + ], ]; } From d69be4b9a271446f1295a0c89b528c5eafdf5a12 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:19:26 +0200 Subject: [PATCH 09/10] objects even with __toString methods cannot be cast to int/float --- .../Statements/Expression/CastAnalyzer.php | 159 +++++------------- tests/ToStringTest.php | 12 ++ 2 files changed, 55 insertions(+), 116 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 039243fd00a..334a466e9bd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -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, @@ -78,7 +87,6 @@ public static function analyze( $type = self::castIntAttempt( $statements_analyzer, - $context, $maybe_type, $stmt->expr, true @@ -106,7 +114,6 @@ public static function analyze( $type = self::castFloatAttempt( $statements_analyzer, - $context, $maybe_type, $stmt->expr, true @@ -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 @@ -373,9 +379,7 @@ 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) { @@ -383,62 +387,25 @@ public static function castIntAttempt( } 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 @@ public static function castIntAttempt( public static function castFloatAttempt( StatementsAnalyzer $statements_analyzer, - Context $context, Union $stmt_type, PhpParser\Node\Expr $stmt, bool $explicit_cast = false @@ -599,9 +565,7 @@ 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) { @@ -609,62 +573,25 @@ public static function castFloatAttempt( } 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; + } } } } diff --git a/tests/ToStringTest.php b/tests/ToStringTest.php index 1a35aae8b73..40874ca4e94 100644 --- a/tests/ToStringTest.php +++ b/tests/ToStringTest.php @@ -505,6 +505,18 @@ public function __toString(): string ', 'error_message' => 'ImplicitToStringCast' ], + 'toStringTypecastNonString' => [ + ' 'InvalidCast', + ], ]; } } From d69e06242617d89a66bcc30843acdd30af1ce23c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 20 Sep 2022 10:59:46 +0200 Subject: [PATCH 10/10] add RiskyCast tests --- tests/ToStringTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/ToStringTest.php b/tests/ToStringTest.php index 40874ca4e94..f9101fa9cbb 100644 --- a/tests/ToStringTest.php +++ b/tests/ToStringTest.php @@ -517,6 +517,16 @@ function __toString(): string { echo (int) $foo;', 'error_message' => 'InvalidCast', ], + 'riskyArrayToIntCast' => [ + ' 'RiskyCast', + ], + 'riskyArrayToFloatCast' => [ + ' 'RiskyCast', + ], ]; } }