getCodebase(); if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat && $nesting > 20) { // ignore deeply-nested string concatenation } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd ) { $left_clauses = Algebra::getFormula( $stmt->left, $statements_analyzer->getFQCLN(), $statements_analyzer, $codebase ); $pre_referenced_var_ids = $context->referenced_var_ids; $context->referenced_var_ids = []; $original_vars_in_scope = $context->vars_in_scope; $pre_assigned_var_ids = $context->assigned_var_ids; if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $context) === false) { return false; } /** @var array */ $new_referenced_var_ids = $context->referenced_var_ids; $context->referenced_var_ids = array_merge($pre_referenced_var_ids, $new_referenced_var_ids); $new_assigned_var_ids = array_diff_key($context->assigned_var_ids, $pre_assigned_var_ids); $new_referenced_var_ids = array_diff_key($new_referenced_var_ids, $new_assigned_var_ids); // remove all newly-asserted var ids too $new_referenced_var_ids = array_filter( $new_referenced_var_ids, /** * @param string $var_id * * @return bool */ function ($var_id) use ($original_vars_in_scope) { return isset($original_vars_in_scope[$var_id]); }, ARRAY_FILTER_USE_KEY ); $simplified_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $left_clauses)); $left_type_assertions = Algebra::getTruthsFromFormula($simplified_clauses); $changed_var_ids = []; $op_context = clone $context; if ($left_type_assertions) { // while in an and, we allow scope to boil over to support // statements of the form if ($x && $x->foo()) $op_vars_in_scope = Reconciler::reconcileKeyedTypes( $left_type_assertions, $context->vars_in_scope, $changed_var_ids, $new_referenced_var_ids, $statements_analyzer, [], $context->inside_loop, new CodeLocation($statements_analyzer->getSource(), $stmt) ); $op_context->vars_in_scope = $op_vars_in_scope; } $op_context->removeReconciledClauses($changed_var_ids); if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $op_context) === false) { return false; } $context->referenced_var_ids = array_merge( $op_context->referenced_var_ids, $context->referenced_var_ids ); if ($context->collect_references) { $context->unreferenced_vars = $op_context->unreferenced_vars; } foreach ($op_context->vars_in_scope as $var_id => $type) { if (isset($context->vars_in_scope[$var_id])) { $context->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->vars_in_scope[$var_id], $type, $codebase ); } } if ($context->inside_conditional) { foreach ($op_context->vars_in_scope as $var => $type) { if (!isset($context->vars_in_scope[$var]) && !$type->possibly_undefined) { $context->vars_in_scope[$var] = $type; } } $context->updateChecks($op_context); $context->vars_possibly_in_scope = array_merge( $op_context->vars_possibly_in_scope, $context->vars_possibly_in_scope ); $context->assigned_var_ids = array_merge( $context->assigned_var_ids, $op_context->assigned_var_ids ); } } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr ) { $pre_referenced_var_ids = $context->referenced_var_ids; $context->referenced_var_ids = []; $pre_assigned_var_ids = $context->assigned_var_ids; $pre_op_context = clone $context; $pre_op_context->parent_context = $context; if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $pre_op_context) === false) { return false; } foreach ($pre_op_context->vars_in_scope as $var_id => $type) { if (!isset($context->vars_in_scope[$var_id])) { $context->vars_in_scope[$var_id] = clone $type; } else { $context->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->vars_in_scope[$var_id], $type ); } } $new_referenced_var_ids = $pre_op_context->referenced_var_ids; $pre_op_context->referenced_var_ids = array_merge($pre_referenced_var_ids, $new_referenced_var_ids); $new_assigned_var_ids = array_diff_key($pre_op_context->assigned_var_ids, $pre_assigned_var_ids); $new_referenced_var_ids = array_diff_key($new_referenced_var_ids, $new_assigned_var_ids); $left_clauses = Algebra::getFormula( $stmt->left, $statements_analyzer->getFQCLN(), $statements_analyzer, $codebase ); try { $negated_left_clauses = Algebra::negateFormula($left_clauses); } catch (\Psalm\Exception\ComplicatedExpressionException $e) { return false; } $clauses_for_right_analysis = Algebra::simplifyCNF( array_merge( $pre_op_context->clauses, $negated_left_clauses ) ); $negated_type_assertions = Algebra::getTruthsFromFormula($clauses_for_right_analysis); $changed_var_ids = []; $op_context = clone $pre_op_context; if ($negated_type_assertions) { // while in an or, we allow scope to boil over to support // statements of the form if ($x === null || $x->foo()) $op_vars_in_scope = Reconciler::reconcileKeyedTypes( $negated_type_assertions, $pre_op_context->vars_in_scope, $changed_var_ids, $new_referenced_var_ids, $statements_analyzer, [], $pre_op_context->inside_loop, new CodeLocation($statements_analyzer->getSource(), $stmt) ); $op_context->vars_in_scope = $op_vars_in_scope; } $op_context->clauses = $clauses_for_right_analysis; if ($changed_var_ids) { $op_context->removeReconciledClauses($changed_var_ids); $context->removeReconciledClauses($changed_var_ids); } if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $op_context) === false) { return false; } if (!($stmt->right instanceof PhpParser\Node\Expr\Exit_)) { foreach ($op_context->vars_in_scope as $var_id => $type) { if (isset($context->vars_in_scope[$var_id])) { $context->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->vars_in_scope[$var_id], $type ); } } } elseif ($stmt->left instanceof PhpParser\Node\Expr\Assign) { $var_id = ExpressionAnalyzer::getVarId($stmt->left->var, $context->self); if ($var_id && isset($pre_op_context->vars_in_scope[$var_id])) { $left_inferred_reconciled = AssertionReconciler::reconcile( '!falsy', clone $pre_op_context->vars_in_scope[$var_id], '', $statements_analyzer, $context->inside_loop, [], new CodeLocation($statements_analyzer->getSource(), $stmt->left), $statements_analyzer->getSuppressedIssues() ); $context->vars_in_scope[$var_id] = $left_inferred_reconciled; } } if ($context->inside_conditional) { $context->updateChecks($op_context); } $context->referenced_var_ids = array_merge( $op_context->referenced_var_ids, $context->referenced_var_ids ); $context->assigned_var_ids = array_merge( $context->assigned_var_ids, $pre_op_context->assigned_var_ids ); if ($context->collect_references) { foreach ($op_context->unreferenced_vars as $var_id => $locations) { if (!isset($context->unreferenced_vars[$var_id])) { $context->unreferenced_vars[$var_id] = $locations; } else { $new_locations = array_diff_key( $locations, $context->unreferenced_vars[$var_id] ); if ($new_locations) { $context->unreferenced_vars[$var_id] += $locations; } } } } $context->vars_possibly_in_scope = array_merge( $op_context->vars_possibly_in_scope, $context->vars_possibly_in_scope ); } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) { $stmt->inferredType = Type::getString(); if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $context) === false) { return false; } if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $context) === false) { return false; } if ($codebase->taint) { $sources = []; $either_tainted = 0; if (isset($stmt->left->inferredType)) { $sources = $stmt->left->inferredType->sources ?: []; $either_tainted = $stmt->left->inferredType->tainted; } if (isset($stmt->right->inferredType)) { $sources = array_merge($sources, $stmt->right->inferredType->sources ?: []); $either_tainted = $either_tainted | $stmt->right->inferredType->tainted; } if ($sources) { $stmt->inferredType->sources = $sources; } if ($either_tainted) { $stmt->inferredType->tainted = $either_tainted; } } } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) { $t_if_context = clone $context; $if_clauses = Algebra::getFormula( $stmt, $statements_analyzer->getFQCLN(), $statements_analyzer, $codebase ); $mixed_var_ids = []; foreach ($context->vars_in_scope as $var_id => $type) { if ($type->hasMixed()) { $mixed_var_ids[] = $var_id; } } foreach ($context->vars_possibly_in_scope as $var_id => $_) { if (!isset($context->vars_in_scope[$var_id])) { $mixed_var_ids[] = $var_id; } } $if_clauses = array_values( array_map( /** * @return \Psalm\Internal\Clause */ function (\Psalm\Internal\Clause $c) use ($mixed_var_ids) { $keys = array_keys($c->possibilities); foreach ($keys as $key) { foreach ($mixed_var_ids as $mixed_var_id) { if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) { return new \Psalm\Internal\Clause([], true); } } } return $c; }, $if_clauses ) ); $ternary_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $if_clauses)); $negated_clauses = Algebra::negateFormula($if_clauses); $negated_if_types = Algebra::getTruthsFromFormula($negated_clauses); $reconcilable_if_types = Algebra::getTruthsFromFormula($ternary_clauses); $changed_var_ids = []; if ($reconcilable_if_types) { $t_if_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes( $reconcilable_if_types, $t_if_context->vars_in_scope, $changed_var_ids, [], $statements_analyzer, [], $t_if_context->inside_loop, new CodeLocation($statements_analyzer->getSource(), $stmt->left) ); $t_if_context->vars_in_scope = $t_if_vars_in_scope_reconciled; } $t_if_context->inside_isset = true; if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $t_if_context) === false) { return false; } $t_if_context->inside_isset = false; foreach ($t_if_context->vars_in_scope as $var_id => $type) { if (isset($context->vars_in_scope[$var_id])) { $context->vars_in_scope[$var_id] = Type::combineUnionTypes($context->vars_in_scope[$var_id], $type); } else { $context->vars_in_scope[$var_id] = $type; } } $context->referenced_var_ids = array_merge( $context->referenced_var_ids, $t_if_context->referenced_var_ids ); if ($context->collect_references) { $context->unreferenced_vars = array_intersect_key( $t_if_context->unreferenced_vars, $context->unreferenced_vars ); } $t_else_context = clone $context; if ($negated_if_types) { $t_else_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes( $negated_if_types, $t_else_context->vars_in_scope, $changed_var_ids, [], $statements_analyzer, [], $t_else_context->inside_loop, new CodeLocation($statements_analyzer->getSource(), $stmt->right) ); $t_else_context->vars_in_scope = $t_else_vars_in_scope_reconciled; } if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $t_else_context) === false) { return false; } $context->referenced_var_ids = array_merge( $context->referenced_var_ids, $t_else_context->referenced_var_ids ); if ($context->collect_references) { $context->unreferenced_vars = array_intersect_key( $t_else_context->unreferenced_vars, $context->unreferenced_vars ); } $lhs_type = null; if (isset($stmt->left->inferredType)) { $if_return_type_reconciled = AssertionReconciler::reconcile( '!null', $stmt->left->inferredType, '', $statements_analyzer, $context->inside_loop, [], new CodeLocation($statements_analyzer->getSource(), $stmt), $statements_analyzer->getSuppressedIssues() ); $lhs_type = $if_return_type_reconciled; } if (!$lhs_type || !isset($stmt->right->inferredType)) { $stmt->inferredType = Type::getMixed(); } else { $stmt->inferredType = Type::combineUnionTypes($lhs_type, $stmt->right->inferredType); } } else { if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp) { if (self::analyze($statements_analyzer, $stmt->left, $context, ++$nesting) === false) { return false; } } else { if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $context) === false) { return false; } } if ($stmt->right instanceof PhpParser\Node\Expr\BinaryOp) { if (self::analyze($statements_analyzer, $stmt->right, $context, ++$nesting) === false) { return false; } } else { if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $context) === false) { return false; } } } // let's do some fun type assignment if (isset($stmt->left->inferredType) && isset($stmt->right->inferredType)) { if ($stmt->left->inferredType->hasString() && $stmt->right->inferredType->hasString() && ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr || $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseXor || $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseAnd ) ) { $stmt->inferredType = Type::getString(); } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Plus || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Minus || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Mod || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Mul || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Pow || (($stmt->left->inferredType->hasInt() || $stmt->right->inferredType->hasInt()) && ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr || $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseXor || $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseAnd || $stmt instanceof PhpParser\Node\Expr\BinaryOp\ShiftLeft || $stmt instanceof PhpParser\Node\Expr\BinaryOp\ShiftRight ) ) ) { self::analyzeNonDivArithmeticOp( $statements_analyzer, $stmt->left, $stmt->right, $stmt, $result_type, $context ); if ($result_type) { $stmt->inferredType = $result_type; } } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseXor && ($stmt->left->inferredType->hasBool() || $stmt->right->inferredType->hasBool()) ) { $stmt->inferredType = Type::getInt(); } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor && ($stmt->left->inferredType->hasBool() || $stmt->right->inferredType->hasBool()) ) { $stmt->inferredType = Type::getBool(); } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Div) { self::analyzeNonDivArithmeticOp( $statements_analyzer, $stmt->left, $stmt->right, $stmt, $result_type, $context ); if ($result_type) { if ($result_type->hasInt()) { $result_type->addType(new TFloat); } $stmt->inferredType = $result_type; } } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) { self::analyzeConcatOp( $statements_analyzer, $stmt->left, $stmt->right, $context, $result_type ); if ($result_type) { $stmt->inferredType = $result_type; } if ($codebase->taint && $stmt->inferredType) { $sources = $stmt->left->inferredType->sources ?: []; $either_tainted = $stmt->left->inferredType->tainted; $sources = array_merge($sources, $stmt->right->inferredType->sources ?: []); $either_tainted = $either_tainted | $stmt->right->inferredType->tainted; if ($sources) { $stmt->inferredType->sources = $sources; } if ($either_tainted) { $stmt->inferredType->tainted = $either_tainted; } } } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr) { self::analyzeNonDivArithmeticOp( $statements_analyzer, $stmt->left, $stmt->right, $stmt, $result_type, $context ); } } if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd || $stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal || $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotEqual || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical || $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Greater || $stmt instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Smaller || $stmt instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual ) { $stmt->inferredType = Type::getBool(); } if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Spaceship) { $stmt->inferredType = Type::getInt(); } return null; } /** * @param StatementsSource|null $statements_source * @param PhpParser\Node\Expr $left * @param PhpParser\Node\Expr $right * @param PhpParser\Node $parent * @param Type\Union|null &$result_type * * @return void */ public static function analyzeNonDivArithmeticOp( $statements_source, PhpParser\Node\Expr $left, PhpParser\Node\Expr $right, PhpParser\Node $parent, Type\Union &$result_type = null, Context $context = null ) { $codebase = $statements_source ? $statements_source->getCodebase() : null; $left_type = isset($left->inferredType) ? $left->inferredType : null; $right_type = isset($right->inferredType) ? $right->inferredType : null; $config = Config::getInstance(); if ($left_type && $right_type) { if ($left_type->isNull()) { if ($statements_source && IssueBuffer::accepts( new NullOperand( 'Left operand cannot be null', new CodeLocation($statements_source, $left) ), $statements_source->getSuppressedIssues() )) { // fall through } return; } if ($left_type->isNullable() && !$left_type->ignore_nullable_issues) { if ($statements_source && IssueBuffer::accepts( new PossiblyNullOperand( 'Left operand cannot be nullable, got ' . $left_type, new CodeLocation($statements_source, $left) ), $statements_source->getSuppressedIssues() )) { // fall through } } if ($right_type->isNull()) { if ($statements_source && IssueBuffer::accepts( new NullOperand( 'Right operand cannot be null', new CodeLocation($statements_source, $right) ), $statements_source->getSuppressedIssues() )) { // fall through } return; } if ($right_type->isNullable() && !$right_type->ignore_nullable_issues) { if ($statements_source && IssueBuffer::accepts( new PossiblyNullOperand( 'Right operand cannot be nullable, got ' . $right_type, new CodeLocation($statements_source, $right) ), $statements_source->getSuppressedIssues() )) { // fall through } } if ($left_type->isFalse()) { if ($statements_source && IssueBuffer::accepts( new FalseOperand( 'Left operand cannot be null', new CodeLocation($statements_source, $left) ), $statements_source->getSuppressedIssues() )) { // fall through } return; } if ($left_type->isFalsable() && !$left_type->ignore_falsable_issues) { if ($statements_source && IssueBuffer::accepts( new PossiblyFalseOperand( 'Left operand cannot be falsable, got ' . $left_type, new CodeLocation($statements_source, $left) ), $statements_source->getSuppressedIssues() )) { // fall through } } if ($right_type->isFalse()) { if ($statements_source && IssueBuffer::accepts( new FalseOperand( 'Right operand cannot be false', new CodeLocation($statements_source, $right) ), $statements_source->getSuppressedIssues() )) { // fall through } return; } if ($right_type->isFalsable() && !$right_type->ignore_falsable_issues) { if ($statements_source && IssueBuffer::accepts( new PossiblyFalseOperand( 'Right operand cannot be falsable, got ' . $right_type, new CodeLocation($statements_source, $right) ), $statements_source->getSuppressedIssues() )) { // fall through } } $invalid_left_messages = []; $invalid_right_messages = []; $has_valid_left_operand = false; $has_valid_right_operand = false; foreach ($left_type->getTypes() as $left_type_part) { foreach ($right_type->getTypes() as $right_type_part) { $candidate_result_type = self::analyzeNonDivOperands( $statements_source, $codebase, $config, $context, $left, $right, $parent, $left_type_part, $right_type_part, $invalid_left_messages, $invalid_right_messages, $has_valid_left_operand, $has_valid_right_operand, $result_type ); if ($candidate_result_type) { $result_type = $candidate_result_type; return; } } } if ($invalid_left_messages && $statements_source) { $first_left_message = $invalid_left_messages[0]; if ($has_valid_left_operand) { if (IssueBuffer::accepts( new PossiblyInvalidOperand( $first_left_message, new CodeLocation($statements_source, $left) ), $statements_source->getSuppressedIssues() )) { // fall through } } else { if (IssueBuffer::accepts( new InvalidOperand( $first_left_message, new CodeLocation($statements_source, $left) ), $statements_source->getSuppressedIssues() )) { // fall through } } } if ($invalid_right_messages && $statements_source) { $first_right_message = $invalid_right_messages[0]; if ($has_valid_right_operand) { if (IssueBuffer::accepts( new PossiblyInvalidOperand( $first_right_message, new CodeLocation($statements_source, $right) ), $statements_source->getSuppressedIssues() )) { // fall through } } else { if (IssueBuffer::accepts( new InvalidOperand( $first_right_message, new CodeLocation($statements_source, $right) ), $statements_source->getSuppressedIssues() )) { // fall through } } } } } /** * @param StatementsSource|null $statements_source * @param \Psalm\Codebase|null $codebase * @param Context|null $context * @param string[] &$invalid_left_messages * @param string[] &$invalid_right_messages * @param bool &$has_valid_left_operand * @param bool &$has_valid_right_operand * * @return Type\Union|null */ public static function analyzeNonDivOperands( $statements_source, $codebase, Config $config, $context, PhpParser\Node\Expr $left, PhpParser\Node\Expr $right, PhpParser\Node $parent, Type\Atomic $left_type_part, Type\Atomic $right_type_part, array &$invalid_left_messages, array &$invalid_right_messages, &$has_valid_left_operand, &$has_valid_right_operand, Type\Union &$result_type = null ) { if ($left_type_part instanceof TNull || $right_type_part instanceof TNull) { // null case is handled above return; } if ($left_type_part instanceof TFalse || $right_type_part instanceof TFalse) { // null case is handled above return; } if ($left_type_part instanceof TMixed || $right_type_part instanceof TMixed || $left_type_part instanceof TTemplateParam || $right_type_part instanceof TTemplateParam ) { if ($statements_source && $codebase && $context) { if (!$context->collect_initializations && !$context->collect_mutations && $statements_source->getFilePath() === $statements_source->getRootFilePath() && (!(($source = $statements_source->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementMixedCount($statements_source->getFilePath()); } } if ($left_type_part instanceof TMixed || $left_type_part instanceof TTemplateParam) { if ($statements_source && IssueBuffer::accepts( new MixedOperand( 'Left operand cannot be mixed', new CodeLocation($statements_source, $left) ), $statements_source->getSuppressedIssues() )) { // fall through } } else { if ($statements_source && IssueBuffer::accepts( new MixedOperand( 'Right operand cannot be mixed', new CodeLocation($statements_source, $right) ), $statements_source->getSuppressedIssues() )) { // fall through } } if ($left_type_part instanceof TMixed && $left_type_part->from_loop_isset && $parent instanceof PhpParser\Node\Expr\AssignOp\Plus && !$right_type_part instanceof TMixed ) { $result_type_member = new Type\Union([$right_type_part]); if (!$result_type) { $result_type = $result_type_member; } else { $result_type = Type::combineUnionTypes($result_type_member, $result_type); } return; } $from_loop_isset = (!($left_type_part instanceof TMixed) || $left_type_part->from_loop_isset) && (!($right_type_part instanceof TMixed) || $right_type_part->from_loop_isset); $result_type = Type::getMixed($from_loop_isset); return $result_type; } if ($statements_source && $codebase && $context) { if (!$context->collect_initializations && !$context->collect_mutations && $statements_source->getFilePath() === $statements_source->getRootFilePath() && (!(($parent_source = $statements_source->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementNonMixedCount($statements_source->getFilePath()); } } if ($left_type_part instanceof TArray || $right_type_part instanceof TArray || $left_type_part instanceof ObjectLike || $right_type_part instanceof ObjectLike ) { if ((!$right_type_part instanceof TArray && !$right_type_part instanceof ObjectLike) || (!$left_type_part instanceof TArray && !$left_type_part instanceof ObjectLike) ) { if (!$left_type_part instanceof TArray && !$left_type_part instanceof ObjectLike) { $invalid_left_messages[] = 'Cannot add an array to a non-array ' . $left_type_part; } else { $invalid_right_messages[] = 'Cannot add an array to a non-array ' . $right_type_part; } if ($left_type_part instanceof TArray || $left_type_part instanceof ObjectLike) { $has_valid_left_operand = true; } elseif ($right_type_part instanceof TArray || $right_type_part instanceof ObjectLike) { $has_valid_right_operand = true; } $result_type = Type::getArray(); return; } $has_valid_right_operand = true; $has_valid_left_operand = true; if ($left_type_part instanceof ObjectLike && $right_type_part instanceof ObjectLike) { $properties = $left_type_part->properties + $right_type_part->properties; $result_type_member = new Type\Union([new ObjectLike($properties)]); } else { $result_type_member = TypeCombination::combineTypes( [$left_type_part, $right_type_part], $codebase, true ); } if (!$result_type) { $result_type = $result_type_member; } else { $result_type = Type::combineUnionTypes($result_type_member, $result_type, $codebase, true); } if ($left instanceof PhpParser\Node\Expr\ArrayDimFetch && $context && $statements_source instanceof StatementsAnalyzer ) { ArrayAssignmentAnalyzer::updateArrayType( $statements_source, $left, $right, $result_type, $context ); } return; } if (($left_type_part instanceof TNamedObject && strtolower($left_type_part->value) === 'gmp') || ($right_type_part instanceof TNamedObject && strtolower($right_type_part->value) === 'gmp') ) { if ((($left_type_part instanceof TNamedObject && strtolower($left_type_part->value) === 'gmp') && (($right_type_part instanceof TNamedObject && strtolower($right_type_part->value) === 'gmp') || ($right_type_part->isNumericType() || $right_type_part instanceof TMixed))) || (($right_type_part instanceof TNamedObject && strtolower($right_type_part->value) === 'gmp') && (($left_type_part instanceof TNamedObject && strtolower($left_type_part->value) === 'gmp') || ($left_type_part->isNumericType() || $left_type_part instanceof TMixed))) ) { if (!$result_type) { $result_type = new Type\Union([new TNamedObject('GMP')]); } else { $result_type = Type::combineUnionTypes( new Type\Union([new TNamedObject('GMP')]), $result_type ); } } else { if ($statements_source && IssueBuffer::accepts( new InvalidOperand( 'Cannot add GMP to non-numeric type', new CodeLocation($statements_source, $parent) ), $statements_source->getSuppressedIssues() )) { // fall through } } return; } if ($left_type_part->isNumericType() || $right_type_part->isNumericType()) { if (($left_type_part instanceof TNumeric || $right_type_part instanceof TNumeric) && ($left_type_part->isNumericType() && $right_type_part->isNumericType()) ) { if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) { $result_type = Type::getInt(); } elseif (!$result_type) { $result_type = Type::getNumeric(); } else { $result_type = Type::combineUnionTypes(Type::getNumeric(), $result_type); } $has_valid_right_operand = true; $has_valid_left_operand = true; return; } if ($left_type_part instanceof TInt && $right_type_part instanceof TInt) { if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) { $result_type = Type::getInt(); } elseif (!$result_type) { $result_type = Type::getInt(true); } else { $result_type = Type::combineUnionTypes(Type::getInt(true), $result_type); } $has_valid_right_operand = true; $has_valid_left_operand = true; return; } if ($left_type_part instanceof TFloat && $right_type_part instanceof TFloat) { if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) { $result_type = Type::getInt(); } elseif (!$result_type) { $result_type = Type::getFloat(); } else { $result_type = Type::combineUnionTypes(Type::getFloat(), $result_type); } $has_valid_right_operand = true; $has_valid_left_operand = true; return; } if (($left_type_part instanceof TFloat && $right_type_part instanceof TInt) || ($left_type_part instanceof TInt && $right_type_part instanceof TFloat) ) { if ($config->strict_binary_operands) { if ($statements_source && IssueBuffer::accepts( new InvalidOperand( 'Cannot add ints to floats', new CodeLocation($statements_source, $parent) ), $statements_source->getSuppressedIssues() )) { // fall through } } if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) { $result_type = Type::getInt(); } elseif (!$result_type) { $result_type = Type::getFloat(); } else { $result_type = Type::combineUnionTypes(Type::getFloat(), $result_type); } $has_valid_right_operand = true; $has_valid_left_operand = true; return; } if ($left_type_part->isNumericType() && $right_type_part->isNumericType()) { if ($config->strict_binary_operands) { if ($statements_source && IssueBuffer::accepts( new InvalidOperand( 'Cannot add numeric types together, please cast explicitly', new CodeLocation($statements_source, $parent) ), $statements_source->getSuppressedIssues() )) { // fall through } } if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) { $result_type = Type::getInt(); } elseif (!$result_type) { $result_type = Type::getFloat(); } else { $result_type = Type::combineUnionTypes(Type::getFloat(), $result_type); } $has_valid_right_operand = true; $has_valid_left_operand = true; return; } if (!$left_type_part->isNumericType()) { $invalid_left_messages[] = 'Cannot perform a numeric operation with a non-numeric type ' . $left_type_part; $has_valid_right_operand = true; } else { $invalid_right_messages[] = 'Cannot perform a numeric operation with a non-numeric type ' . $right_type_part; $has_valid_left_operand = true; } } else { $invalid_left_messages[] = 'Cannot perform a numeric operation with non-numeric types ' . $left_type_part . ' and ' . $right_type_part; } } /** * @param StatementsAnalyzer $statements_analyzer * @param PhpParser\Node\Expr $left * @param PhpParser\Node\Expr $right * @param Type\Union|null &$result_type * * @return void */ public static function analyzeConcatOp( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr $left, PhpParser\Node\Expr $right, Context $context, Type\Union &$result_type = null ) { $codebase = $statements_analyzer->getCodebase(); $left_type = isset($left->inferredType) ? $left->inferredType : null; $right_type = isset($right->inferredType) ? $right->inferredType : null; $config = Config::getInstance(); if ($left_type && $right_type) { $result_type = Type::getString(); if ($left_type->hasMixed() || $right_type->hasMixed()) { if (!$context->collect_initializations && !$context->collect_mutations && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() && (!(($parent_source = $statements_analyzer->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); } if ($left_type->hasMixed()) { if (IssueBuffer::accepts( new MixedOperand( 'Left operand cannot be mixed', new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { if (IssueBuffer::accepts( new MixedOperand( 'Right operand cannot be mixed', new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } return; } if (!$context->collect_initializations && !$context->collect_mutations && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() && (!(($parent_source = $statements_analyzer->getSource()) instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) ) { $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); } if ($left_type->isNull()) { if (IssueBuffer::accepts( new NullOperand( 'Cannot concatenate with a ' . $left_type, new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } if ($right_type->isNull()) { if (IssueBuffer::accepts( new NullOperand( 'Cannot concatenate with a ' . $right_type, new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } if ($left_type->isFalse()) { if (IssueBuffer::accepts( new FalseOperand( 'Cannot concatenate with a ' . $left_type, new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } if ($right_type->isFalse()) { if (IssueBuffer::accepts( new FalseOperand( 'Cannot concatenate with a ' . $right_type, new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } if ($left_type->isNullable() && !$left_type->ignore_nullable_issues) { if (IssueBuffer::accepts( new PossiblyNullOperand( 'Cannot concatenate with a possibly null ' . $left_type, new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } if ($right_type->isNullable() && !$right_type->ignore_nullable_issues) { if (IssueBuffer::accepts( new PossiblyNullOperand( 'Cannot concatenate with a possibly null ' . $right_type, new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } if ($left_type->isFalsable() && !$left_type->ignore_falsable_issues) { if (IssueBuffer::accepts( new PossiblyFalseOperand( 'Cannot concatenate with a possibly false ' . $left_type, new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } if ($right_type->isFalsable() && !$right_type->ignore_falsable_issues) { if (IssueBuffer::accepts( new PossiblyFalseOperand( 'Cannot concatenate with a possibly false ' . $right_type, new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } $left_type_match = true; $right_type_match = true; $has_valid_left_operand = false; $has_valid_right_operand = false; $left_comparison_result = new \Psalm\Internal\Analyzer\TypeComparisonResult(); $right_comparison_result = new \Psalm\Internal\Analyzer\TypeComparisonResult(); foreach ($left_type->getTypes() as $left_type_part) { if ($left_type_part instanceof Type\Atomic\TTemplateParam) { if (IssueBuffer::accepts( new MixedOperand( 'Left operand cannot be mixed', new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } if ($left_type_part instanceof Type\Atomic\TNull || $left_type_part instanceof Type\Atomic\TFalse) { continue; } $left_type_part_match = TypeAnalyzer::isAtomicContainedBy( $codebase, $left_type_part, new Type\Atomic\TString, false, false, $left_comparison_result ); $left_type_match = $left_type_match && $left_type_part_match; $has_valid_left_operand = $has_valid_left_operand || $left_type_part_match; if ($left_comparison_result->to_string_cast && $config->strict_binary_operands) { if (IssueBuffer::accepts( new ImplicitToStringCast( 'Left side of concat op expects string, ' . '\'' . $left_type . '\' provided with a __toString method', new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } foreach ($right_type->getTypes() as $right_type_part) { if ($right_type_part instanceof Type\Atomic\TTemplateParam) { if (IssueBuffer::accepts( new MixedOperand( 'Right operand cannot be a template param', new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } if ($right_type_part instanceof Type\Atomic\TNull || $right_type_part instanceof Type\Atomic\TFalse) { continue; } $right_type_part_match = TypeAnalyzer::isAtomicContainedBy( $codebase, $right_type_part, new Type\Atomic\TString, false, false, $right_comparison_result ); $right_type_match = $right_type_match && $right_type_part_match; $has_valid_right_operand = $has_valid_right_operand || $right_type_part_match; if ($right_comparison_result->to_string_cast && $config->strict_binary_operands) { if (IssueBuffer::accepts( new ImplicitToStringCast( 'Right side of concat op expects string, ' . '\'' . $right_type . '\' provided with a __toString method', new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } if (!$left_type_match && (!$left_comparison_result->scalar_type_match_found || $config->strict_binary_operands) ) { if ($has_valid_left_operand) { if (IssueBuffer::accepts( new PossiblyInvalidOperand( 'Cannot concatenate with a ' . $left_type, new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { if (IssueBuffer::accepts( new InvalidOperand( 'Cannot concatenate with a ' . $left_type, new CodeLocation($statements_analyzer->getSource(), $left) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } if (!$right_type_match && (!$right_comparison_result->scalar_type_match_found || $config->strict_binary_operands) ) { if ($has_valid_right_operand) { if (IssueBuffer::accepts( new PossiblyInvalidOperand( 'Cannot concatenate with a ' . $right_type, new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { if (IssueBuffer::accepts( new InvalidOperand( 'Cannot concatenate with a ' . $right_type, new CodeLocation($statements_analyzer->getSource(), $right) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } } } // When concatenating two known string literals (with only one possibility), // put the concatenated string into $result_type if ($left_type && $right_type && $left_type->isSingleStringLiteral() && $right_type->isSingleStringLiteral()) { $literal = $left_type->getSingleStringLiteral()->value . $right_type->getSingleStringLiteral()->value; if (strlen($literal) <= 1000) { // Limit these to 10000 bytes to avoid extremely large union types from repeated concatenations, etc $result_type = Type::getString($literal); } } } }