expr, $context) === false) { return false; } $maybe_type = $statements_analyzer->node_data->getType($stmt->expr); if ($maybe_type) { if ($maybe_type->isInt()) { if (!$maybe_type->from_calculation) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } } $type = self::castIntAttempt( $statements_analyzer, $maybe_type, $stmt->expr, true ); } else { $type = Type::getInt(); } $statements_analyzer->node_data->setType($stmt, $type); return true; } if ($stmt instanceof PhpParser\Node\Expr\Cast\Double) { if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) { return false; } $maybe_type = $statements_analyzer->node_data->getType($stmt->expr); if ($maybe_type) { if ($maybe_type->isFloat()) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } $type = self::castFloatAttempt( $statements_analyzer, $maybe_type, $stmt->expr, true ); } else { $type = Type::getFloat(); } $statements_analyzer->node_data->setType($stmt, $type); return true; } if ($stmt instanceof PhpParser\Node\Expr\Cast\Bool_) { if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) { return false; } $maybe_type = $statements_analyzer->node_data->getType($stmt->expr); if ($maybe_type) { if ($maybe_type->isBool()) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } } if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph ) { $type = new Union([new TBool()], [ 'parent_nodes' => $maybe_type->parent_nodes ?? [] ]); } else { $type = Type::getBool(); } $statements_analyzer->node_data->setType($stmt, $type); return true; } if ($stmt instanceof PhpParser\Node\Expr\Cast\String_) { if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) { return false; } $stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr); if ($stmt_expr_type) { if ($stmt_expr_type->isString()) { self::handleRedundantCast($stmt_expr_type, $statements_analyzer, $stmt); } $stmt_type = self::castStringAttempt( $statements_analyzer, $context, $stmt_expr_type, $stmt->expr, true ); } else { $stmt_type = Type::getString(); } $statements_analyzer->node_data->setType($stmt, $stmt_type); return true; } if ($stmt instanceof PhpParser\Node\Expr\Cast\Object_) { if (!self::checkExprGeneralUse($statements_analyzer, $stmt, $context)) { return false; } $permissible_atomic_types = []; $all_permissible = false; if ($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr)) { if ($stmt_expr_type->isObjectType()) { self::handleRedundantCast($stmt_expr_type, $statements_analyzer, $stmt); } $all_permissible = true; foreach ($stmt_expr_type->getAtomicTypes() as $type) { if ($type instanceof TList) { $type = $type->getKeyedArray(); } if ($type instanceof Scalar) { $objWithProps = new TObjectWithProperties(['scalar' => new Union([$type])]); $permissible_atomic_types[] = $objWithProps; } elseif ($type instanceof TKeyedArray) { $permissible_atomic_types[] = new TObjectWithProperties($type->properties); } else { $all_permissible = false; break; } } } if ($permissible_atomic_types && $all_permissible) { $type = TypeCombiner::combine($permissible_atomic_types); } else { $type = Type::getObject(); } if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph ) { $type = $type->setParentNodes($stmt_expr_type->parent_nodes ?? []); } $statements_analyzer->node_data->setType($stmt, $type); return true; } if ($stmt instanceof PhpParser\Node\Expr\Cast\Array_) { if (!self::checkExprGeneralUse($statements_analyzer, $stmt, $context)) { return false; } $permissible_atomic_types = []; $all_permissible = false; if ($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr)) { if ($stmt_expr_type->isArray()) { self::handleRedundantCast($stmt_expr_type, $statements_analyzer, $stmt); } $all_permissible = true; foreach ($stmt_expr_type->getAtomicTypes() as $type) { if ($type instanceof TList) { $type = $type->getKeyedArray(); } if ($type instanceof Scalar) { $keyed_array = new TKeyedArray([new Union([$type])], null, null, true); $permissible_atomic_types[] = $keyed_array; } elseif ($type instanceof TNull) { $permissible_atomic_types[] = new TArray([Type::getNever(), Type::getNever()]); } elseif ($type instanceof TArray || $type instanceof TKeyedArray ) { $permissible_atomic_types[] = $type; } elseif ($type instanceof TObjectWithProperties) { $array_type = $type->properties === [] ? new TArray([Type::getArrayKey(), Type::getMixed()]) : new TKeyedArray( $type->properties, null, [Type::getArrayKey(), Type::getMixed()] ); $permissible_atomic_types[] = $array_type; } else { $all_permissible = false; break; } } } if ($permissible_atomic_types && $all_permissible) { $type = TypeCombiner::combine($permissible_atomic_types); } else { $type = Type::getArray(); } if ($statements_analyzer->data_flow_graph) { $type = $type->setParentNodes($stmt_expr_type->parent_nodes ?? []); } $statements_analyzer->node_data->setType($stmt, $type); return true; } if ($stmt instanceof PhpParser\Node\Expr\Cast\Unset_ && $statements_analyzer->getCodebase()->analysis_php_version_id <= 7_04_00 ) { if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) { return false; } $statements_analyzer->node_data->setType($stmt, Type::getNull()); return true; } IssueBuffer::maybeAdd( new UnrecognizedExpression( 'Psalm does not understand the cast ' . get_class($stmt), new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); return false; } public static function castIntAttempt( StatementsAnalyzer $statements_analyzer, Union $stmt_type, PhpParser\Node\Expr $stmt, bool $explicit_cast = false ): Union { $codebase = $statements_analyzer->getCodebase(); $risky_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 TList) { $atomic_type = $atomic_type->getKeyedArray(); } 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) { $valid_ints[] = new TLiteralInt((int) $atomic_type->value); } elseif ($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; } 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) { $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) { continue; } // prevent "Could not get class storage for mixed" if (!$codebase->classExists($intersection_type->value)) { continue; } 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; } } } } if ($atomic_type instanceof TNonEmptyArray || ($atomic_type instanceof TKeyedArray && $atomic_type->isNonEmpty()) ) { $risky_cast[] = $atomic_type->getId(); $valid_ints[] = new TLiteralInt(1); continue; } if ($atomic_type instanceof TArray || $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 :-) $risky_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; } // always 1 for "error" cases $valid_ints[] = new TLiteralInt(1); $invalid_casts[] = $atomic_type->getId(); } if ($invalid_casts) { IssueBuffer::maybeAdd( new InvalidCast( $invalid_casts[0] . ' cannot be cast to int', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); } elseif ($risky_cast) { IssueBuffer::maybeAdd( new RiskyCast( 'Casting ' . $risky_cast[0] . ' to int has possibly unintended value of 0/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 = $int_type->setParentNodes($parent_nodes); } return $int_type; } public static function castFloatAttempt( StatementsAnalyzer $statements_analyzer, Union $stmt_type, PhpParser\Node\Expr $stmt, bool $explicit_cast = false ): Union { $codebase = $statements_analyzer->getCodebase(); $risky_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 TList) { $atomic_type = $atomic_type->getKeyedArray(); } 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) { $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); } elseif ($atomic_type instanceof TNumericString) { $castable_types[] = new TFloat(); } else { // 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; } 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) { $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) { continue; } // prevent "Could not get class storage for mixed" if (!$codebase->classExists($intersection_type->value)) { continue; } 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; } } } } if ($atomic_type instanceof TNonEmptyArray || ($atomic_type instanceof TKeyedArray && $atomic_type->isNonEmpty()) ) { $risky_cast[] = $atomic_type->getId(); $valid_floats[] = new TLiteralFloat(1.0); continue; } if ($atomic_type instanceof TArray || $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 :-) $risky_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; } // always 1.0 for "error" cases $valid_floats[] = new TLiteralFloat(1.0); $invalid_casts[] = $atomic_type->getId(); } if ($invalid_casts) { IssueBuffer::maybeAdd( new InvalidCast( $invalid_casts[0] . ' cannot be cast to float', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); } elseif ($risky_cast) { IssueBuffer::maybeAdd( 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() ); } 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 = $float_type->setParentNodes($parent_nodes); } return $float_type; } public static function castStringAttempt( StatementsAnalyzer $statements_analyzer, Context $context, Union $stmt_type, PhpParser\Node\Expr $stmt, bool $explicit_cast = false ): Union { $codebase = $statements_analyzer->getCodebase(); $invalid_casts = []; $valid_strings = []; $castable_types = []; $atomic_types = $stmt_type->getAtomicTypes(); $parent_nodes = []; if ($statements_analyzer->data_flow_graph) { $parent_nodes = $stmt_type->parent_nodes; } while ($atomic_types) { $atomic_type = array_pop($atomic_types); if ($atomic_type instanceof TFloat || $atomic_type instanceof TInt || $atomic_type instanceof TNumeric ) { if ($atomic_type instanceof TLiteralInt || $atomic_type instanceof TLiteralFloat) { $castable_types[] = new TLiteralString((string) $atomic_type->value); } elseif ($atomic_type instanceof TNonspecificLiteralInt) { $castable_types[] = new TNonspecificLiteralString(); } else { $castable_types[] = new TNumericString(); } continue; } if ($atomic_type instanceof TString) { $valid_strings[] = $atomic_type; continue; } if ($atomic_type instanceof TNull || $atomic_type instanceof TFalse ) { $valid_strings[] = new TLiteralString(''); 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 Scalar ) { $castable_types[] = new TString(); 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); } $castable_types = array_merge( $castable_types, array_values($return_type->getAtomicTypes()) ); continue 2; } } if ($intersection_type instanceof TObjectWithProperties && isset($intersection_type->methods['__tostring']) ) { $castable_types[] = new TString(); continue 2; } } } 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_strings || $castable_types) { IssueBuffer::maybeAdd( new PossiblyInvalidCast( $invalid_casts[0] . ' cannot be cast to string', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); } else { IssueBuffer::maybeAdd( new InvalidCast( $invalid_casts[0] . ' cannot be cast to string', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); } } elseif ($explicit_cast && !$castable_types) { // todo: emit error here } $valid_types = [...$valid_strings, ...$castable_types]; if (!$valid_types) { $str_type = Type::getString(); } else { $str_type = TypeCombiner::combine( $valid_types, $codebase ); } if ($statements_analyzer->data_flow_graph) { $str_type = $str_type->setParentNodes($parent_nodes); } return $str_type; } private static function checkExprGeneralUse( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Cast $stmt, Context $context ): bool { $was_inside_general_use = $context->inside_general_use; $context->inside_general_use = true; $retVal = ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context); $context->inside_general_use = $was_inside_general_use; return $retVal; } private static function handleRedundantCast( Union $maybe_type, StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Cast $stmt ): void { $codebase = $statements_analyzer->getCodebase(); $project_analyzer = $statements_analyzer->getProjectAnalyzer(); $file_manipulation = null; if ($maybe_type->from_docblock) { $issue = new RedundantCastGivenDocblockType( 'Redundant cast to ' . $maybe_type->getKey() . ' given docblock-provided type', new CodeLocation($statements_analyzer->getSource(), $stmt) ); if ($codebase->alter_code && isset($project_analyzer->getIssuesToFix()['RedundantCastGivenDocblockType']) ) { $file_manipulation = new FileManipulation( (int) $stmt->getAttribute('startFilePos'), (int) $stmt->expr->getAttribute('startFilePos'), '' ); } } else { $issue = new RedundantCast( 'Redundant cast to ' . $maybe_type->getKey(), new CodeLocation($statements_analyzer->getSource(), $stmt) ); if ($codebase->alter_code && isset($project_analyzer->getIssuesToFix()['RedundantCast']) ) { $file_manipulation = new FileManipulation( (int) $stmt->getAttribute('startFilePos'), (int) $stmt->expr->getAttribute('startFilePos'), '' ); } } if ($file_manipulation) { FileManipulationBuffer::add($statements_analyzer->getFilePath(), [$file_manipulation]); } if (IssueBuffer::accepts($issue, $statements_analyzer->getSuppressedIssues())) { // fall through } } }