getCodebase(); /** @var int $i */ foreach ($stmt->catches as $i => $catch) { $catch_actions[$i] = ScopeAnalyzer::getControlActions( $catch->stmts, $statements_analyzer->node_data, $codebase->config->exit_functions, [] ); $all_catches_leave = $all_catches_leave && !in_array(ScopeAnalyzer::ACTION_NONE, $catch_actions[$i], true); } $existing_thrown_exceptions = $context->possibly_thrown_exceptions; /** * @var array> */ $context->possibly_thrown_exceptions = []; $old_context = clone $context; if ($all_catches_leave && !$stmt->finally) { $try_context = $context; } else { $try_context = clone $context; if ($codebase->alter_code) { $try_context->branch_point = $try_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); } if ($stmt->finally) { $try_context->finally_scope = new FinallyScope($try_context->vars_in_scope); } } $assigned_var_ids = $try_context->assigned_var_ids; $context->assigned_var_ids = []; $old_referenced_var_ids = $try_context->referenced_var_ids; if ($statements_analyzer->analyze($stmt->stmts, $context) === false) { return false; } if ($try_context->finally_scope) { foreach ($context->vars_in_scope as $var_id => $type) { if (isset($try_context->finally_scope->vars_in_scope[$var_id])) { if ($try_context->finally_scope->vars_in_scope[$var_id] !== $type) { $try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( $try_context->finally_scope->vars_in_scope[$var_id], $type, $statements_analyzer->getCodebase() ); } } else { $try_context->finally_scope->vars_in_scope[$var_id] = $type; } } } $context->has_returned = false; $try_block_control_actions = ScopeAnalyzer::getControlActions( $stmt->stmts, $statements_analyzer->node_data, $codebase->config->exit_functions, [] ); /** @var array */ $newly_assigned_var_ids = $context->assigned_var_ids; $context->assigned_var_ids = array_merge( $assigned_var_ids, $newly_assigned_var_ids ); $possibly_referenced_var_ids = array_merge( $context->referenced_var_ids, $old_referenced_var_ids ); if ($try_context !== $context) { foreach ($context->vars_in_scope as $var_id => $type) { if (!isset($try_context->vars_in_scope[$var_id])) { $try_context->vars_in_scope[$var_id] = clone $type; $context->vars_in_scope[$var_id]->possibly_undefined = true; $context->vars_in_scope[$var_id]->possibly_undefined_from_try = true; } else { $try_context->vars_in_scope[$var_id] = Type::combineUnionTypes( $try_context->vars_in_scope[$var_id], $type ); } } $try_context->vars_possibly_in_scope = $context->vars_possibly_in_scope; $try_context->possibly_thrown_exceptions = $context->possibly_thrown_exceptions; $context->referenced_var_ids = array_intersect_key( $try_context->referenced_var_ids, $context->referenced_var_ids ); } $try_leaves_loop = $context->loop_scope && $context->loop_scope->final_actions && !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true); if (!$all_catches_leave) { foreach ($newly_assigned_var_ids as $assigned_var_id => $_) { $context->removeVarFromConflictingClauses($assigned_var_id); } } else { foreach ($newly_assigned_var_ids as $assigned_var_id => $_) { $try_context->removeVarFromConflictingClauses($assigned_var_id); } } // at this point we have two contexts – $context, in which it is assumed that everything was fine, // and $try_context - which allows all variables to have the union of the values before and after // the try was applied $original_context = clone $try_context; $issues_to_suppress = [ 'RedundantCondition', 'RedundantConditionGivenDocblockType', 'TypeDoesNotContainNull', 'TypeDoesNotContainType', ]; $definitely_newly_assigned_var_ids = $newly_assigned_var_ids; /** @var int $i */ foreach ($stmt->catches as $i => $catch) { $catch_context = clone $original_context; $catch_context->has_returned = false; foreach ($catch_context->vars_in_scope as $var_id => $type) { if (!isset($old_context->vars_in_scope[$var_id])) { $type = clone $type; $type->possibly_undefined_from_try = true; $catch_context->vars_in_scope[$var_id] = $type; } else { $catch_context->vars_in_scope[$var_id] = Type::combineUnionTypes( $type, $old_context->vars_in_scope[$var_id] ); } } $fq_catch_classes = []; if (!$catch->types) { throw new \UnexpectedValueException('Very bad'); } foreach ($catch->types as $catch_type) { $fq_catch_class = ClassLikeAnalyzer::getFQCLNFromNameObject( $catch_type, $statements_analyzer->getAliases() ); $fq_catch_class = $codebase->classlikes->getUnAliasedName($fq_catch_class); if ($codebase->alter_code && $fq_catch_class) { $codebase->classlikes->handleClassLikeReferenceInMigration( $codebase, $statements_analyzer, $catch_type, $fq_catch_class, $context->calling_method_id ); } if ($original_context->check_classes) { if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( $statements_analyzer, $fq_catch_class, new CodeLocation($statements_analyzer->getSource(), $catch_type, $context->include_location), $context->self, $context->calling_method_id, $statements_analyzer->getSuppressedIssues(), new ClassLikeNameOptions(true) ) === false) { // fall through } } if (($codebase->classExists($fq_catch_class) && strtolower($fq_catch_class) !== 'exception' && !($codebase->classExtends($fq_catch_class, 'Exception') || $codebase->classImplements($fq_catch_class, 'Throwable'))) || ($codebase->interfaceExists($fq_catch_class) && strtolower($fq_catch_class) !== 'throwable' && !$codebase->interfaceExtends($fq_catch_class, 'Throwable')) ) { if (IssueBuffer::accepts( new InvalidCatch( 'Class/interface ' . $fq_catch_class . ' cannot be caught', new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_catch_class ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } $fq_catch_classes[] = $fq_catch_class; } if ($catch_context->collect_exceptions) { foreach ($fq_catch_classes as $fq_catch_class) { $fq_catch_class_lower = strtolower($fq_catch_class); foreach ($catch_context->possibly_thrown_exceptions as $exception_fqcln => $_) { $exception_fqcln_lower = strtolower($exception_fqcln); if ($exception_fqcln_lower === $fq_catch_class_lower || ($codebase->classExists($exception_fqcln) && $codebase->classExtendsOrImplements($exception_fqcln, $fq_catch_class)) || ($codebase->interfaceExists($exception_fqcln) && $codebase->interfaceExtends($exception_fqcln, $fq_catch_class)) ) { unset($original_context->possibly_thrown_exceptions[$exception_fqcln]); unset($context->possibly_thrown_exceptions[$exception_fqcln]); unset($catch_context->possibly_thrown_exceptions[$exception_fqcln]); } } } /** * @var array> */ $catch_context->possibly_thrown_exceptions = []; } // discard all clauses because crazy stuff may have happened in try block $catch_context->clauses = []; if ($catch->var && is_string($catch->var->name)) { $catch_var_id = '$' . $catch->var->name; $catch_context->vars_in_scope[$catch_var_id] = new Union( array_map( /** * @param string $fq_catch_class */ function ($fq_catch_class) use ($codebase): \Psalm\Type\Atomic\TNamedObject { $catch_class_type = new TNamedObject($fq_catch_class); if (version_compare(PHP_VERSION, '7.0.0dev', '>=') && strtolower($fq_catch_class) !== 'throwable' && $codebase->interfaceExists($fq_catch_class) && !$codebase->interfaceExtends($fq_catch_class, 'Throwable') ) { $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); } return $catch_class_type; }, $fq_catch_classes ) ); // removes dependent vars from $context $catch_context->removeDescendents( $catch_var_id, null, $catch_context->vars_in_scope[$catch_var_id], $statements_analyzer ); $catch_context->vars_possibly_in_scope[$catch_var_id] = true; $location = new CodeLocation($statements_analyzer->getSource(), $catch->var); if (!$statements_analyzer->hasVariable($catch_var_id)) { $statements_analyzer->registerVariable( $catch_var_id, $location, $catch_context->branch_point ); } else { $statements_analyzer->registerVariableAssignment( $catch_var_id, $location ); } if ($statements_analyzer->data_flow_graph) { $catch_var_node = DataFlowNode::getForAssignment($catch_var_id, $location); $catch_context->vars_in_scope[$catch_var_id]->parent_nodes = [ $catch_var_node->id => $catch_var_node ]; if ($statements_analyzer->data_flow_graph instanceof \Psalm\Internal\Codebase\VariableUseGraph) { $statements_analyzer->data_flow_graph->addPath( $catch_var_node, new DataFlowNode('variable-use', 'variable use', null), 'variable-use' ); } } } $suppressed_issues = $statements_analyzer->getSuppressedIssues(); foreach ($issues_to_suppress as $issue_to_suppress) { if (!in_array($issue_to_suppress, $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues([$issue_to_suppress]); } } $old_catch_assigned_var_ids = $catch_context->referenced_var_ids; $catch_context->assigned_var_ids = []; $statements_analyzer->analyze($catch->stmts, $catch_context); // recalculate in case there's a no-return clause $catch_actions[$i] = ScopeAnalyzer::getControlActions( $catch->stmts, $statements_analyzer->node_data, $codebase->config->exit_functions, [] ); foreach ($issues_to_suppress as $issue_to_suppress) { if (!in_array($issue_to_suppress, $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues([$issue_to_suppress]); } } /** @var array */ $new_catch_assigned_var_ids = $catch_context->assigned_var_ids; $catch_context->assigned_var_ids += $old_catch_assigned_var_ids; $context->referenced_var_ids = array_intersect_key( $catch_context->referenced_var_ids, $context->referenced_var_ids ); $possibly_referenced_var_ids = array_merge( $catch_context->referenced_var_ids, $possibly_referenced_var_ids ); if ($catch_context->collect_exceptions) { $context->mergeExceptions($catch_context); } $catch_doesnt_leave_parent_scope = $catch_actions[$i] !== [ScopeAnalyzer::ACTION_END] && $catch_actions[$i] !== [ScopeAnalyzer::ACTION_CONTINUE] && $catch_actions[$i] !== [ScopeAnalyzer::ACTION_BREAK]; if ($catch_doesnt_leave_parent_scope) { $definitely_newly_assigned_var_ids = array_intersect_key( $new_catch_assigned_var_ids, $definitely_newly_assigned_var_ids ); foreach ($catch_context->vars_in_scope as $var_id => $type) { if ($try_block_control_actions === [ScopeAnalyzer::ACTION_END]) { $context->vars_in_scope[$var_id] = $type; } elseif (isset($context->vars_in_scope[$var_id])) { $context->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->vars_in_scope[$var_id], $type ); } } $context->vars_possibly_in_scope = array_merge( $catch_context->vars_possibly_in_scope, $context->vars_possibly_in_scope ); } else { if ($stmt->finally) { $context->vars_possibly_in_scope = array_merge( $catch_context->vars_possibly_in_scope, $context->vars_possibly_in_scope ); } } if ($try_context->finally_scope) { foreach ($catch_context->vars_in_scope as $var_id => $type) { if (isset($try_context->finally_scope->vars_in_scope[$var_id])) { if ($try_context->finally_scope->vars_in_scope[$var_id] !== $type) { $try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( $try_context->finally_scope->vars_in_scope[$var_id], $type, $statements_analyzer->getCodebase() ); } } else { $try_context->finally_scope->vars_in_scope[$var_id] = $type; $type->possibly_undefined = true; $type->possibly_undefined_from_try = true; } } } } if ($context->loop_scope && !$try_leaves_loop && !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true) ) { $context->loop_scope->final_actions[] = ScopeAnalyzer::ACTION_NONE; } $finally_has_returned = false; if ($stmt->finally) { if ($try_context->finally_scope) { $finally_context = clone $context; $finally_context->assigned_var_ids = []; $finally_context->possibly_assigned_var_ids = []; $finally_context->vars_in_scope = $try_context->finally_scope->vars_in_scope; $statements_analyzer->analyze($stmt->finally->stmts, $finally_context); $finally_has_returned = $finally_context->has_returned; /** @var string $var_id */ foreach ($finally_context->assigned_var_ids as $var_id => $_) { if (isset($context->vars_in_scope[$var_id]) && isset($finally_context->vars_in_scope[$var_id]) ) { if ($context->vars_in_scope[$var_id]->possibly_undefined && $context->vars_in_scope[$var_id]->possibly_undefined_from_try ) { $context->vars_in_scope[$var_id]->possibly_undefined = false; $context->vars_in_scope[$var_id]->possibly_undefined_from_try = false; } $context->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->vars_in_scope[$var_id], $finally_context->vars_in_scope[$var_id], $codebase ); } elseif (isset($finally_context->vars_in_scope[$var_id])) { $context->vars_in_scope[$var_id] = clone $finally_context->vars_in_scope[$var_id]; } } } } foreach ($definitely_newly_assigned_var_ids as $var_id => $_) { if (isset($context->vars_in_scope[$var_id])) { $new_type = clone $context->vars_in_scope[$var_id]; if ($new_type->possibly_undefined_from_try) { $new_type->possibly_undefined = false; $new_type->possibly_undefined_from_try = false; } $context->vars_in_scope[$var_id] = $new_type; } } foreach ($existing_thrown_exceptions as $possibly_thrown_exception => $codelocations) { foreach ($codelocations as $hash => $codelocation) { $context->possibly_thrown_exceptions[$possibly_thrown_exception][$hash] = $codelocation; } } $body_has_returned = !in_array(ScopeAnalyzer::ACTION_NONE, $try_block_control_actions, true); $context->has_returned = ($body_has_returned && $all_catches_leave) || $finally_has_returned; return null; } }