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',
+ ],
];
}
}