name; $codebase = $statements_analyzer->getCodebase(); $code_location = new CodeLocation($statements_analyzer->getSource(), $stmt); $config = $codebase->config; $real_stmt = $stmt; if ($function_name instanceof PhpParser\Node\Name && isset($stmt->args[0]) && !$stmt->args[0]->unpack ) { $original_function_id = implode('\\', $function_name->parts); if ($original_function_id === 'call_user_func') { $other_args = \array_slice($stmt->args, 1); $function_name = $stmt->args[0]->value; $stmt = new PhpParser\Node\Expr\FuncCall( $function_name, $other_args, $stmt->getAttributes() ); } if ($original_function_id === 'call_user_func_array' && isset($stmt->args[1])) { $function_name = $stmt->args[0]->value; $stmt = new PhpParser\Node\Expr\FuncCall( $function_name, [new PhpParser\Node\Arg($stmt->args[1]->value, false, true)], $stmt->getAttributes() ); } } if ($function_name instanceof PhpParser\Node\Expr) { $function_call_info = self::getAnalyzeNamedExpression( $statements_analyzer, $stmt, $real_stmt, $function_name, $context ); if ($function_call_info->function_exists === false) { return true; } if ($function_call_info->new_function_name) { $function_name = $function_call_info->new_function_name; } } else { $function_call_info = self::handleNamedFunction( $statements_analyzer, $stmt, $function_name, $context, $code_location ); if (!$function_call_info->function_exists) { return true; } } $set_inside_conditional = false; if ($function_name instanceof PhpParser\Node\Name && $function_name->parts === ['assert'] && !$context->inside_conditional ) { $context->inside_conditional = true; $set_inside_conditional = true; } ArgumentsAnalyzer::analyze( $statements_analyzer, $stmt->args, $function_call_info->function_params, $function_call_info->function_id, $function_call_info->allow_named_args, $context ); if ($set_inside_conditional) { $context->inside_conditional = false; } $function_callable = null; if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { if (!$function_call_info->is_stubbed && $function_call_info->in_call_map) { $function_callable = \Psalm\Internal\Codebase\InternalCallMapHandler::getCallableFromCallMapById( $codebase, $function_call_info->function_id, $stmt->args, $statements_analyzer->node_data ); $function_call_info->function_params = $function_callable->params; } } $template_result = new TemplateResult([], []); // do this here to allow closure param checks if ($function_call_info->function_params !== null) { ArgumentsAnalyzer::checkArgumentsMatch( $statements_analyzer, $stmt->args, $function_call_info->function_id, $function_call_info->function_params, $function_call_info->function_storage, null, $template_result, $code_location, $context ); } CallAnalyzer::checkTemplateResult( $statements_analyzer, $template_result, $code_location, $function_call_info->function_id ); if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { $stmt_type = FunctionCallReturnTypeFetcher::fetch( $statements_analyzer, $codebase, $stmt, $function_name, $function_call_info->function_id, $function_call_info->in_call_map, $function_call_info->is_stubbed, $function_call_info->function_storage, $function_callable, $template_result, $context ); $statements_analyzer->node_data->setType($real_stmt, $stmt_type); if ($config->after_every_function_checks) { foreach ($config->after_every_function_checks as $plugin_fq_class_name) { $plugin_fq_class_name::afterEveryFunctionCallAnalysis( $stmt, $function_call_info->function_id, $context, $statements_analyzer->getSource(), $codebase ); } } } foreach ($function_call_info->defined_constants as $const_name => $const_type) { $context->constants[$const_name] = clone $const_type; $context->vars_in_scope[$const_name] = clone $const_type; } foreach ($function_call_info->global_variables as $var_id => $_) { $context->vars_in_scope[$var_id] = Type::getMixed(); $context->vars_possibly_in_scope[$var_id] = true; } if ($function_name instanceof PhpParser\Node\Name && $function_name->parts === ['assert'] && isset($stmt->args[0]) ) { self::processAssertFunctionEffects( $statements_analyzer, $codebase, $stmt, $stmt->args[0], $context ); } if ($codebase->store_node_types && !$context->collect_initializations && !$context->collect_mutations && ($stmt_type = $statements_analyzer->node_data->getType($real_stmt)) ) { $codebase->analyzer->addNodeType( $statements_analyzer->getFilePath(), $stmt, $stmt_type->getId() ); } self::checkFunctionCallPurity( $statements_analyzer, $codebase, $stmt, $function_name, $function_call_info, $context ); if ($function_call_info->function_storage) { $inferred_upper_bounds = $template_result->upper_bounds; if ($function_call_info->function_storage->assertions && $function_name instanceof PhpParser\Node\Name) { self::applyAssertionsToContext( $function_name, null, $function_call_info->function_storage->assertions, $stmt->args, $inferred_upper_bounds, $context, $statements_analyzer ); } if ($function_call_info->function_storage->if_true_assertions) { $statements_analyzer->node_data->setIfTrueAssertions( $stmt, array_map( function (Assertion $assertion) use ($inferred_upper_bounds) : Assertion { return $assertion->getUntemplatedCopy($inferred_upper_bounds ?: [], null); }, $function_call_info->function_storage->if_true_assertions ) ); } if ($function_call_info->function_storage->if_false_assertions) { $statements_analyzer->node_data->setIfFalseAssertions( $stmt, array_map( function (Assertion $assertion) use ($inferred_upper_bounds) : Assertion { return $assertion->getUntemplatedCopy($inferred_upper_bounds ?: [], null); }, $function_call_info->function_storage->if_false_assertions ) ); } if ($function_call_info->function_storage->deprecated && $function_call_info->function_id) { if (IssueBuffer::accepts( new DeprecatedFunction( 'The function ' . $function_call_info->function_id . ' has been marked as deprecated', $code_location, $function_call_info->function_id ), $statements_analyzer->getSuppressedIssues() )) { // continue } } } if ($function_call_info->byref_uses) { foreach ($function_call_info->byref_uses as $byref_use_var => $_) { $context->vars_in_scope['$' . $byref_use_var] = Type::getMixed(); $context->vars_possibly_in_scope['$' . $byref_use_var] = true; } } if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { NamedFunctionCallHandler::handle( $statements_analyzer, $codebase, $stmt, $real_stmt, $function_name, strtolower($function_call_info->function_id), $context ); } if (!$statements_analyzer->node_data->getType($real_stmt)) { $statements_analyzer->node_data->setType($real_stmt, Type::getMixed()); } return true; } /** * @return FunctionCallInfo */ private static function handleNamedFunction( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\FuncCall $stmt, PhpParser\Node\Name $function_name, Context $context, CodeLocation $code_location ) { $function_call_info = new FunctionCallInfo(); $codebase = $statements_analyzer->getCodebase(); $codebase_functions = $codebase->functions; $original_function_id = implode('\\', $function_name->parts); if (!$function_name instanceof PhpParser\Node\Name\FullyQualified) { $function_call_info->function_id = $codebase_functions->getFullyQualifiedFunctionNameFromString( $original_function_id, $statements_analyzer ); } else { $function_call_info->function_id = $original_function_id; } $namespaced_function_exists = $codebase_functions->functionExists( $statements_analyzer, strtolower($function_call_info->function_id) ); if (!$namespaced_function_exists && !$function_name instanceof PhpParser\Node\Name\FullyQualified ) { $function_call_info->in_call_map = InternalCallMapHandler::inCallMap($original_function_id); $function_call_info->is_stubbed = $codebase_functions->hasStubbedFunction($original_function_id); if ($function_call_info->is_stubbed || $function_call_info->in_call_map) { $function_call_info->function_id = $original_function_id; } } else { $function_call_info->in_call_map = InternalCallMapHandler::inCallMap($function_call_info->function_id); $function_call_info->is_stubbed = $codebase_functions->hasStubbedFunction($function_call_info->function_id); } $function_call_info->function_exists = $function_call_info->is_stubbed || $function_call_info->in_call_map || $namespaced_function_exists; if ($function_call_info->function_exists && $codebase->store_node_types && !$context->collect_initializations && !$context->collect_mutations ) { ArgumentMapPopulator::recordArgumentPositions( $statements_analyzer, $stmt, $codebase, $function_call_info->function_id ); } $is_predefined = true; $is_maybe_root_function = !$function_name instanceof PhpParser\Node\Name\FullyQualified && count($function_name->parts) === 1; if (!$function_call_info->in_call_map) { $predefined_functions = $codebase->config->getPredefinedFunctions(); $is_predefined = isset($predefined_functions[strtolower($original_function_id)]) || isset($predefined_functions[strtolower($function_call_info->function_id)]); if ($context->check_functions) { if (self::checkFunctionExists( $statements_analyzer, $function_call_info->function_id, $code_location, $is_maybe_root_function ) === false ) { if (ArgumentsAnalyzer::analyze( $statements_analyzer, $stmt->args, null, null, true, $context ) === false) { // fall through } return $function_call_info; } else { $function_call_info->function_exists = true; } } } else { $function_call_info->function_exists = true; } $function_call_info->function_params = null; $function_call_info->defined_constants = []; $function_call_info->global_variables = []; if ($function_call_info->function_exists) { if ($codebase->functions->params_provider->has($function_call_info->function_id)) { $function_call_info->function_params = $codebase->functions->params_provider->getFunctionParams( $statements_analyzer, $function_call_info->function_id, $stmt->args, null, $code_location ); } if ($function_call_info->function_params === null) { if (!$function_call_info->in_call_map || $function_call_info->is_stubbed) { try { $function_call_info->function_storage = $function_storage = $codebase_functions->getStorage( $statements_analyzer, strtolower($function_call_info->function_id) ); $function_call_info->function_params = $function_call_info->function_storage->params; if (!$function_storage->allow_named_arg_calls) { $function_call_info->allow_named_args = false; } if (!$is_predefined) { $function_call_info->defined_constants = $function_storage->defined_constants; $function_call_info->global_variables = $function_storage->global_variables; } } catch (\UnexpectedValueException $e) { $function_call_info->function_params = [ new FunctionLikeParameter('args', false, null, null, null, false, false, true) ]; } } else { $function_callable = InternalCallMapHandler::getCallableFromCallMapById( $codebase, $function_call_info->function_id, $stmt->args, $statements_analyzer->node_data ); $function_call_info->function_params = $function_callable->params; } } if ($codebase->store_node_types && !$context->collect_initializations && !$context->collect_mutations ) { $codebase->analyzer->addNodeReference( $statements_analyzer->getFilePath(), $function_name, $function_call_info->function_id . '()' ); } } return $function_call_info; } private static function getAnalyzeNamedExpression( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\FuncCall $stmt, PhpParser\Node\Expr\FuncCall $real_stmt, PhpParser\Node\Expr $function_name, Context $context ): FunctionCallInfo { $function_call_info = new FunctionCallInfo(); $codebase = $statements_analyzer->getCodebase(); $was_in_call = $context->inside_call; $context->inside_call = true; if (ExpressionAnalyzer::analyze($statements_analyzer, $function_name, $context) === false) { $context->inside_call = $was_in_call; return $function_call_info; } $context->inside_call = $was_in_call; $function_call_info->byref_uses = []; if ($stmt_name_type = $statements_analyzer->node_data->getType($function_name)) { if ($stmt_name_type->isNull()) { if (IssueBuffer::accepts( new NullFunctionCall( 'Cannot call function on null value', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return $function_call_info; } if ($stmt_name_type->isNullable()) { if (IssueBuffer::accepts( new PossiblyNullFunctionCall( 'Cannot call function on possibly null value', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } $invalid_function_call_types = []; $has_valid_function_call_type = false; foreach ($stmt_name_type->getAtomicTypes() as $var_type_part) { if ($var_type_part instanceof Type\Atomic\TClosure || $var_type_part instanceof TCallable) { if (!$var_type_part->is_pure && $context->pure) { if (IssueBuffer::accepts( new ImpureFunctionCall( 'Cannot call an impure function from a mutation-free context', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } $function_call_info->function_params = $var_type_part->params; if (($stmt_type = $statements_analyzer->node_data->getType($real_stmt)) && $var_type_part->return_type ) { $statements_analyzer->node_data->setType( $real_stmt, Type::combineUnionTypes( $stmt_type, $var_type_part->return_type ) ); } else { $statements_analyzer->node_data->setType( $real_stmt, $var_type_part->return_type ?: Type::getMixed() ); } if ($var_type_part instanceof Type\Atomic\TClosure) { $function_call_info->byref_uses += $var_type_part->byref_uses; } $function_call_info->function_exists = true; $has_valid_function_call_type = true; } elseif ($var_type_part instanceof TTemplateParam && $var_type_part->as->hasCallableType()) { $has_valid_function_call_type = true; } elseif ($var_type_part instanceof TMixed || $var_type_part instanceof TTemplateParam) { $has_valid_function_call_type = true; if (IssueBuffer::accepts( new MixedFunctionCall( 'Cannot call function on ' . $var_type_part->getId(), new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } elseif ($var_type_part instanceof TCallableObject || $var_type_part instanceof TCallableString ) { // this is fine $has_valid_function_call_type = true; } elseif (($var_type_part instanceof TNamedObject && $var_type_part->value === 'Closure')) { // this is fine $has_valid_function_call_type = true; } elseif ($var_type_part instanceof TString || $var_type_part instanceof Type\Atomic\TArray || $var_type_part instanceof Type\Atomic\TList || ($var_type_part instanceof Type\Atomic\TKeyedArray && count($var_type_part->properties) === 2) ) { $potential_method_id = null; if ($var_type_part instanceof Type\Atomic\TKeyedArray) { $potential_method_id = CallableTypeComparator::getCallableMethodIdFromTKeyedArray( $var_type_part, $codebase, $context->calling_method_id, $statements_analyzer->getFilePath() ); if ($potential_method_id === 'not-callable') { $potential_method_id = null; } } elseif ($var_type_part instanceof Type\Atomic\TLiteralString) { if (!$var_type_part->value) { $invalid_function_call_types[] = '\'\''; continue; } if (strpos($var_type_part->value, '::')) { $parts = explode('::', strtolower($var_type_part->value)); $fq_class_name = $parts[0]; $fq_class_name = \preg_replace('/^\\\\/', '', $fq_class_name); $potential_method_id = new \Psalm\Internal\MethodIdentifier($fq_class_name, $parts[1]); } else { $function_call_info->new_function_name = new PhpParser\Node\Name\FullyQualified( $var_type_part->value, $function_name->getAttributes() ); } } if ($potential_method_id) { $codebase->methods->methodExists( $potential_method_id, $context->calling_method_id, null, $statements_analyzer, $statements_analyzer->getFilePath() ); } // this is also kind of fine $has_valid_function_call_type = true; } elseif ($var_type_part instanceof TNull) { // handled above } elseif (!$var_type_part instanceof TNamedObject || !$codebase->classlikes->classOrInterfaceExists($var_type_part->value) || !$codebase->methods->methodExists( new \Psalm\Internal\MethodIdentifier( $var_type_part->value, '__invoke' ) ) ) { $invalid_function_call_types[] = (string)$var_type_part; } else { self::analyzeInvokeCall( $statements_analyzer, $stmt, $real_stmt, $function_name, $context, $var_type_part ); } } if ($invalid_function_call_types) { $var_type_part = reset($invalid_function_call_types); if ($has_valid_function_call_type) { if (IssueBuffer::accepts( new PossiblyInvalidFunctionCall( 'Cannot treat type ' . $var_type_part . ' as callable', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { if (IssueBuffer::accepts( new InvalidFunctionCall( 'Cannot treat type ' . $var_type_part . ' as callable', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } return $function_call_info; } if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph && $stmt_name_type->parent_nodes && $stmt_name_type->hasString() && !\in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) ) { $arg_location = new CodeLocation($statements_analyzer->getSource(), $function_name); $custom_call_sink = \Psalm\Internal\DataFlow\TaintSink::getForMethodArgument( 'variable-call', 'variable-call', 0, $arg_location, $arg_location ); $custom_call_sink->taints = [\Psalm\Type\TaintKind::INPUT_CALLABLE]; $statements_analyzer->data_flow_graph->addSink($custom_call_sink); foreach ($stmt_name_type->parent_nodes as $parent_node) { $statements_analyzer->data_flow_graph->addPath($parent_node, $custom_call_sink, 'call'); } } } if (!$statements_analyzer->node_data->getType($real_stmt)) { $statements_analyzer->node_data->setType($real_stmt, Type::getMixed()); } return $function_call_info; } private static function analyzeInvokeCall( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\FuncCall $stmt, PhpParser\Node\Expr\FuncCall $real_stmt, PhpParser\Node\Expr $function_name, Context $context, Type\Atomic $atomic_type ) : void { $old_data_provider = $statements_analyzer->node_data; $statements_analyzer->node_data = clone $statements_analyzer->node_data; $fake_method_call = new PhpParser\Node\Expr\MethodCall( $function_name, new PhpParser\Node\Identifier('__invoke', $function_name->getAttributes()), $stmt->args ); $suppressed_issues = $statements_analyzer->getSuppressedIssues(); if (!in_array('PossiblyNullReference', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['PossiblyNullReference']); } if (!in_array('InternalMethod', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['InternalMethod']); } if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']); } $statements_analyzer->node_data->setType($function_name, new Type\Union([$atomic_type])); \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze( $statements_analyzer, $fake_method_call, $context, false ); if (!in_array('PossiblyNullReference', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['PossiblyNullReference']); } if (!in_array('InternalMethod', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['InternalMethod']); } if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']); } $fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call); $statements_analyzer->node_data = $old_data_provider; if ($stmt_type = $statements_analyzer->node_data->getType($real_stmt)) { $statements_analyzer->node_data->setType( $real_stmt, Type::combineUnionTypes( $fake_method_call_type ?: Type::getMixed(), $stmt_type ) ); } else { $statements_analyzer->node_data->setType( $real_stmt, $fake_method_call_type ?: Type::getMixed() ); } } private static function processAssertFunctionEffects( StatementsAnalyzer $statements_analyzer, \Psalm\Codebase $codebase, PhpParser\Node\Expr\FuncCall $stmt, PhpParser\Node\Arg $first_arg, Context $context ) : void { $first_arg_value_id = \spl_object_id($first_arg->value); $assert_clauses = FormulaGenerator::getFormula( $first_arg_value_id, $first_arg_value_id, $first_arg->value, $context->self, $statements_analyzer, $codebase ); \Psalm\Internal\Analyzer\AlgebraAnalyzer::checkForParadox( $context->clauses, $assert_clauses, $statements_analyzer, $stmt, [] ); $simplified_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $assert_clauses)); $assert_type_assertions = Algebra::getTruthsFromFormula($simplified_clauses); $changed_var_ids = []; if ($assert_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( $assert_type_assertions, $assert_type_assertions, $context->vars_in_scope, $changed_var_ids, array_map( function ($_): bool { return true; }, $assert_type_assertions ), $statements_analyzer, $statements_analyzer->getTemplateTypeMap() ?: [], $context->inside_loop, new CodeLocation($statements_analyzer->getSource(), $stmt) ); foreach ($changed_var_ids as $var_id => $_) { $first_appearance = $statements_analyzer->getFirstAppearance($var_id); if ($first_appearance && isset($context->vars_in_scope[$var_id]) && $context->vars_in_scope[$var_id]->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->decrementMixedCount($statements_analyzer->getFilePath()); } IssueBuffer::remove( $statements_analyzer->getFilePath(), 'MixedAssignment', $first_appearance->raw_file_start ); } if (isset($op_vars_in_scope[$var_id])) { $op_vars_in_scope[$var_id]->from_docblock = true; } } $context->vars_in_scope = $op_vars_in_scope; } if ($changed_var_ids) { $simplified_clauses = Context::removeReconciledClauses($simplified_clauses, $changed_var_ids)[0]; } $context->clauses = $simplified_clauses; } private static function checkFunctionCallPurity( StatementsAnalyzer $statements_analyzer, \Psalm\Codebase $codebase, PhpParser\Node\Expr\FuncCall $stmt, PhpParser\Node $function_name, FunctionCallInfo $function_call_info, Context $context ) : void { $config = $codebase->config; if (!$context->collect_initializations && !$context->collect_mutations && ($context->mutation_free || $context->external_mutation_free || $codebase->find_unused_variables || !$config->remember_property_assignments_after_call || ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer && $statements_analyzer->getSource()->track_mutations)) ) { $must_use = true; $callmap_function_pure = $function_call_info->function_id && $function_call_info->in_call_map ? $codebase->functions->isCallMapFunctionPure( $codebase, $statements_analyzer->node_data, $function_call_info->function_id, $stmt->args, $must_use ) : null; if ((!$function_call_info->in_call_map && $function_call_info->function_storage && !$function_call_info->function_storage->pure) || ($callmap_function_pure === false) ) { if ($context->mutation_free || $context->external_mutation_free) { if (IssueBuffer::accepts( new ImpureFunctionCall( 'Cannot call an impure function from a mutation-free context', new CodeLocation($statements_analyzer, $function_name) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; } if (!$config->remember_property_assignments_after_call) { $context->removeMutableObjectVars(); } } elseif ($function_call_info->function_id && (($function_call_info->function_storage && $function_call_info->function_storage->pure && !$function_call_info->function_storage->assertions && $must_use) || ($callmap_function_pure === true && $must_use)) && $codebase->find_unused_variables && !$context->inside_conditional && !$context->inside_unset ) { if (!$context->inside_assignment && !$context->inside_call && !$context->inside_use) { if (IssueBuffer::accepts( new UnusedFunctionCall( 'The call to ' . $function_call_info->function_id . ' is not used', new CodeLocation($statements_analyzer, $function_name), $function_call_info->function_id ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } else { /** @psalm-suppress UndefinedPropertyAssignment */ $stmt->pure = true; } } } } }