value; if (!ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( $statements_analyzer, $fq_class_name, new CodeLocation($statements_analyzer, $stmt->class), !$context->collect_initializations && !$context->collect_mutations ? $context->self : null, !$context->collect_initializations && !$context->collect_mutations ? $context->calling_method_id : null, $statements_analyzer->getSuppressedIssues(), $stmt->class instanceof PhpParser\Node\Name && count($stmt->class->parts) === 1 && in_array(strtolower($stmt->class->parts[0]), ['self', 'static'], true) )) { return; } $intersection_types = $lhs_type_part->extra_types; } elseif ($lhs_type_part instanceof Type\Atomic\TClassString && $lhs_type_part->as_type ) { $fq_class_name = $lhs_type_part->as_type->value; if (!ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( $statements_analyzer, $fq_class_name, new CodeLocation($statements_analyzer, $stmt->class), $context->self, $context->calling_method_id, $statements_analyzer->getSuppressedIssues(), false )) { return; } $intersection_types = $lhs_type_part->as_type->extra_types; } elseif ($lhs_type_part instanceof Type\Atomic\TDependentGetClass && !$lhs_type_part->as_type->hasObject() ) { $fq_class_name = 'object'; if ($lhs_type_part->as_type->hasObjectType() && $lhs_type_part->as_type->isSingle() ) { foreach ($lhs_type_part->as_type->getAtomicTypes() as $typeof_type_atomic) { if ($typeof_type_atomic instanceof Type\Atomic\TNamedObject) { $fq_class_name = $typeof_type_atomic->value; } } } if ($fq_class_name === 'object') { return; } } elseif ($lhs_type_part instanceof Type\Atomic\TLiteralClassString) { $fq_class_name = $lhs_type_part->value; if (!ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( $statements_analyzer, $fq_class_name, new CodeLocation($statements_analyzer, $stmt->class), $context->self, $context->calling_method_id, $statements_analyzer->getSuppressedIssues(), false )) { return; } } elseif ($lhs_type_part instanceof Type\Atomic\TTemplateParam && !$lhs_type_part->as->isMixed() && !$lhs_type_part->as->hasObject() ) { $fq_class_name = null; foreach ($lhs_type_part->as->getAtomicTypes() as $generic_param_type) { if (!$generic_param_type instanceof TNamedObject) { return; } $fq_class_name = $generic_param_type->value; break; } if (!$fq_class_name) { if (IssueBuffer::accepts( new UndefinedClass( 'Type ' . $lhs_type_part->as . ' cannot be called as a class', new CodeLocation($statements_analyzer->getSource(), $stmt), (string) $lhs_type_part ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } } else { self::handleNonObjectCall( $statements_analyzer, $stmt, $context, $lhs_type_part, $ignore_nullable_issues ); return; } $codebase = $statements_analyzer->getCodebase(); $fq_class_name = $codebase->classlikes->getUnAliasedName($fq_class_name); $is_mock = ExpressionAnalyzer::isMock($fq_class_name); $has_mock = $has_mock || $is_mock; if ($stmt->name instanceof PhpParser\Node\Identifier && !$is_mock) { self::handleNamedCall( $statements_analyzer, $stmt, $stmt->name, $context, $lhs_type_part, $intersection_types ?: [], $fq_class_name, $moved_call, $has_existing_method ); } else { if ($stmt->name instanceof PhpParser\Node\Expr) { $was_inside_use = $context->inside_use; $context->inside_use = true; ExpressionAnalyzer::analyze($statements_analyzer, $stmt->name, $context); $context->inside_use = $was_inside_use; } if (!$context->ignore_variable_method) { $codebase->analyzer->addMixedMemberName( strtolower($fq_class_name) . '::', $context->calling_method_id ?: $statements_analyzer->getFileName() ); } if (ArgumentsAnalyzer::analyze( $statements_analyzer, $stmt->args, null, null, true, $context ) === false) { return; } } if ($codebase->alter_code && $fq_class_name && !$moved_call && $stmt->class instanceof PhpParser\Node\Name && !in_array($stmt->class->parts[0], ['parent', 'static']) ) { $codebase->classlikes->handleClassLikeReferenceInMigration( $codebase, $statements_analyzer, $stmt->class, $fq_class_name, $context->calling_method_id, false, $stmt->class->parts[0] === 'self' ); } } private static function handleNamedCall( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\StaticCall $stmt, PhpParser\Node\Identifier $stmt_name, Context $context, Type\Atomic $lhs_type_part, array $intersection_types, string $fq_class_name, bool &$moved_call, bool &$has_existing_method ) : bool { $codebase = $statements_analyzer->getCodebase(); $method_name_lc = strtolower($stmt_name->name); $method_id = new MethodIdentifier($fq_class_name, $method_name_lc); $cased_method_id = $fq_class_name . '::' . $stmt_name->name; if ($codebase->store_node_types && !$context->collect_initializations && !$context->collect_mutations ) { \Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentMapPopulator::recordArgumentPositions( $statements_analyzer, $stmt, $codebase, (string) $method_id ); } $args = $stmt->args; if ($intersection_types && !$codebase->methods->methodExists($method_id) ) { foreach ($intersection_types as $intersection_type) { if (!$intersection_type instanceof TNamedObject) { continue; } $intersection_method_id = new MethodIdentifier( $intersection_type->value, $method_name_lc ); if ($codebase->methods->methodExists($intersection_method_id)) { $method_id = $intersection_method_id; $cased_method_id = $intersection_type->value . '::' . $stmt_name->name; $fq_class_name = $intersection_type->value; break; } } } $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); $naive_method_exists = $codebase->methods->methodExists( $method_id, !$context->collect_initializations && !$context->collect_mutations ? $context->calling_method_id : null, $codebase->collect_locations ? new CodeLocation($statements_analyzer, $stmt_name) : null, $statements_analyzer, $statements_analyzer->getFilePath(), false ); $fake_method_exists = false; if (!$naive_method_exists && $codebase->methods->existence_provider->has($fq_class_name) ) { $method_exists = $codebase->methods->existence_provider->doesMethodExist( $fq_class_name, $method_id->method_name, $statements_analyzer, null ); if ($method_exists) { $fake_method_exists = true; } } if (!$naive_method_exists && $class_storage->mixin_declaring_fqcln && $class_storage->namedMixins ) { foreach ($class_storage->namedMixins as $mixin) { $new_method_id = new MethodIdentifier( $mixin->value, $method_name_lc ); if ($codebase->methods->methodExists( $new_method_id, $context->calling_method_id, $codebase->collect_locations ? new CodeLocation($statements_analyzer, $stmt_name) : null, !$context->collect_initializations && !$context->collect_mutations ? $statements_analyzer : null, $statements_analyzer->getFilePath() )) { $mixin_candidates = []; foreach ($class_storage->templatedMixins as $mixin_candidate) { $mixin_candidates[] = clone $mixin_candidate; } foreach ($class_storage->namedMixins as $mixin_candidate) { $mixin_candidates[] = clone $mixin_candidate; } $mixin_candidates_no_generic = array_filter($mixin_candidates, function ($check): bool { return !($check instanceof Type\Atomic\TGenericObject); }); // $mixin_candidates_no_generic will only be empty when there are TGenericObject entries. // In that case, Union will be initialized with an empty array but // replaced with non-empty types in the following loop. /** @psalm-suppress ArgumentTypeCoercion */ $mixin_candidate_type = new Type\Union($mixin_candidates_no_generic); foreach ($mixin_candidates as $tGenericMixin) { if (!($tGenericMixin instanceof Type\Atomic\TGenericObject)) { continue; } $mixin_declaring_class_storage = $codebase->classlike_storage_provider->get( $class_storage->mixin_declaring_fqcln ); $new_mixin_candidate_type = AtomicPropertyFetchAnalyzer::localizePropertyType( $codebase, new Type\Union([$lhs_type_part]), $tGenericMixin, $class_storage, $mixin_declaring_class_storage ); foreach ($mixin_candidate_type->getAtomicTypes() as $type) { $new_mixin_candidate_type->addType($type); } $mixin_candidate_type = $new_mixin_candidate_type; } $new_lhs_type = \Psalm\Internal\Type\TypeExpander::expandUnion( $codebase, $mixin_candidate_type, $fq_class_name, $fq_class_name, $class_storage->parent_class, true, false, $class_storage->final ); $old_data_provider = $statements_analyzer->node_data; $statements_analyzer->node_data = clone $statements_analyzer->node_data; $context->vars_in_scope['$tmp_mixin_var'] = $new_lhs_type; $fake_method_call_expr = new PhpParser\Node\Expr\MethodCall( new PhpParser\Node\Expr\Variable( 'tmp_mixin_var', $stmt->class->getAttributes() ), $stmt_name, $stmt->args, $stmt->getAttributes() ); if (MethodCallAnalyzer::analyze( $statements_analyzer, $fake_method_call_expr, $context ) === false) { return false; } $fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr); $statements_analyzer->node_data = $old_data_provider; $statements_analyzer->node_data->setType($stmt, $fake_method_call_type ?: Type::getMixed()); return true; } } } $config = $codebase->config; if (!$naive_method_exists || !MethodAnalyzer::isMethodVisible( $method_id, $context, $statements_analyzer->getSource() ) || $fake_method_exists || (isset($class_storage->pseudo_static_methods[$method_name_lc]) && ($config->use_phpdoc_method_without_magic_or_parent || $class_storage->parent_class)) ) { $callstatic_id = new MethodIdentifier( $fq_class_name, '__callstatic' ); if ($codebase->methods->methodExists( $callstatic_id, $context->calling_method_id, $codebase->collect_locations ? new CodeLocation($statements_analyzer, $stmt_name) : null, !$context->collect_initializations && !$context->collect_mutations ? $statements_analyzer : null, $statements_analyzer->getFilePath() )) { if ($codebase->methods->return_type_provider->has($fq_class_name)) { $return_type_candidate = $codebase->methods->return_type_provider->getReturnType( $statements_analyzer, $method_id->fq_class_name, $method_id->method_name, $stmt->args, $context, new CodeLocation($statements_analyzer->getSource(), $stmt_name), null, null, strtolower($stmt_name->name) ); if ($return_type_candidate) { CallAnalyzer::checkMethodArgs( $method_id, $stmt->args, null, $context, new CodeLocation($statements_analyzer->getSource(), $stmt), $statements_analyzer ); $statements_analyzer->node_data->setType($stmt, $return_type_candidate); return true; } } if (isset($class_storage->pseudo_static_methods[$method_name_lc])) { $pseudo_method_storage = $class_storage->pseudo_static_methods[$method_name_lc]; if (self::checkPseudoMethod( $statements_analyzer, $stmt, $method_id, $fq_class_name, $args, $class_storage, $pseudo_method_storage, $context ) === false ) { return false; } if ($pseudo_method_storage->return_type) { return true; } } else { if (ArgumentsAnalyzer::analyze( $statements_analyzer, $args, null, null, true, $context ) === false) { return false; } } $array_values = array_map( function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem { return new PhpParser\Node\Expr\ArrayItem($arg->value); }, $args ); $args = [ new PhpParser\Node\Arg(new PhpParser\Node\Scalar\String_((string) $method_id)), new PhpParser\Node\Arg(new PhpParser\Node\Expr\Array_($array_values)), ]; $method_id = new MethodIdentifier( $fq_class_name, '__callstatic' ); } elseif (isset($class_storage->pseudo_static_methods[$method_name_lc]) && ($config->use_phpdoc_method_without_magic_or_parent || $class_storage->parent_class) ) { $pseudo_method_storage = $class_storage->pseudo_static_methods[$method_name_lc]; if (self::checkPseudoMethod( $statements_analyzer, $stmt, $method_id, $fq_class_name, $args, $class_storage, $pseudo_method_storage, $context ) === false ) { return false; } if ($pseudo_method_storage->return_type) { return true; } } if (!$context->check_methods) { if (ArgumentsAnalyzer::analyze( $statements_analyzer, $stmt->args, null, null, true, $context ) === false) { return false; } return true; } } $does_method_exist = MethodAnalyzer::checkMethodExists( $codebase, $method_id, new CodeLocation($statements_analyzer, $stmt), $statements_analyzer->getSuppressedIssues(), $context->calling_method_id ); if (!$does_method_exist) { if (ArgumentsAnalyzer::analyze( $statements_analyzer, $stmt->args, null, null, true, $context ) === false) { return false; } if ($codebase->alter_code && $fq_class_name && !$moved_call) { $codebase->classlikes->handleClassLikeReferenceInMigration( $codebase, $statements_analyzer, $stmt->class, $fq_class_name, $context->calling_method_id ); } return true; } $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); if ($class_storage->deprecated && $fq_class_name !== $context->self) { if (IssueBuffer::accepts( new DeprecatedClass( $fq_class_name . ' is marked deprecated', new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } if ($context->self && ! NamespaceAnalyzer::isWithin($context->self, $class_storage->internal)) { if (IssueBuffer::accepts( new InternalClass( $fq_class_name . ' is internal to ' . $class_storage->internal . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } if (MethodVisibilityAnalyzer::analyze( $method_id, $context, $statements_analyzer->getSource(), new CodeLocation($statements_analyzer, $stmt), $statements_analyzer->getSuppressedIssues() ) === false) { return false; } if ((!$stmt->class instanceof PhpParser\Node\Name || $stmt->class->parts[0] !== 'parent' || $statements_analyzer->isStatic()) && ( !$context->self || $statements_analyzer->isStatic() || !$codebase->classExtends($context->self, $fq_class_name) ) ) { if (MethodAnalyzer::checkStatic( $method_id, ($stmt->class instanceof PhpParser\Node\Name && strtolower($stmt->class->parts[0]) === 'self') || $context->self === $fq_class_name, !$statements_analyzer->isStatic(), $codebase, new CodeLocation($statements_analyzer, $stmt), $statements_analyzer->getSuppressedIssues(), $is_dynamic_this_method ) === false) { // fall through } if ($is_dynamic_this_method) { $old_data_provider = $statements_analyzer->node_data; $statements_analyzer->node_data = clone $statements_analyzer->node_data; $fake_method_call_expr = new PhpParser\Node\Expr\MethodCall( new PhpParser\Node\Expr\Variable( 'this', $stmt->class->getAttributes() ), $stmt_name, $stmt->args, $stmt->getAttributes() ); if (MethodCallAnalyzer::analyze( $statements_analyzer, $fake_method_call_expr, $context ) === false) { return false; } $fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr); $statements_analyzer->node_data = $old_data_provider; if ($fake_method_call_type) { $statements_analyzer->node_data->setType($stmt, $fake_method_call_type); } return true; } } $has_existing_method = true; ExistingAtomicStaticCallAnalyzer::analyze( $statements_analyzer, $stmt, $stmt_name, $args, $context, $lhs_type_part, $method_id, $cased_method_id, $class_storage, $moved_call ); return true; } /** * @param list $args * @return false|null */ private static function checkPseudoMethod( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\StaticCall $stmt, MethodIdentifier $method_id, string $fq_class_name, array $args, \Psalm\Storage\ClassLikeStorage $class_storage, \Psalm\Storage\MethodStorage $pseudo_method_storage, Context $context ): ?bool { if (ArgumentsAnalyzer::analyze( $statements_analyzer, $args, $pseudo_method_storage->params, (string) $method_id, true, $context ) === false) { return false; } $codebase = $statements_analyzer->getCodebase(); if (ArgumentsAnalyzer::checkArgumentsMatch( $statements_analyzer, $args, $method_id, $pseudo_method_storage->params, $pseudo_method_storage, null, null, new CodeLocation($statements_analyzer, $stmt), $context ) === false) { return false; } $method_storage = null; if ($statements_analyzer->data_flow_graph) { try { $method_storage = $codebase->methods->getStorage($method_id); ArgumentsAnalyzer::analyze( $statements_analyzer, $args, $method_storage->params, (string) $method_id, true, $context ); ArgumentsAnalyzer::checkArgumentsMatch( $statements_analyzer, $args, $method_id, $method_storage->params, $method_storage, null, null, new CodeLocation($statements_analyzer, $stmt), $context ); } catch (\Exception $e) { // do nothing } } if ($pseudo_method_storage->return_type) { $return_type_candidate = clone $pseudo_method_storage->return_type; $return_type_candidate = \Psalm\Internal\Type\TypeExpander::expandUnion( $statements_analyzer->getCodebase(), $return_type_candidate, $fq_class_name, $fq_class_name, $class_storage->parent_class ); if ($method_storage) { \Psalm\Internal\Analyzer\Statements\Expression\Call\StaticCallAnalyzer::taintReturnType( $statements_analyzer, $stmt, $method_id, (string) $method_id, $return_type_candidate, $method_storage ); } $stmt_type = $statements_analyzer->node_data->getType($stmt); if (!$stmt_type) { $statements_analyzer->node_data->setType($stmt, $return_type_candidate); } else { $statements_analyzer->node_data->setType( $stmt, Type::combineUnionTypes( $return_type_candidate, $stmt_type ) ); } } return null; } public static function handleNonObjectCall( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\StaticCall $stmt, Context $context, Type\Atomic $lhs_type_part, bool $ignore_nullable_issues ) : void { $codebase = $statements_analyzer->getCodebase(); $config = $codebase->config; if ($lhs_type_part instanceof Type\Atomic\TMixed || $lhs_type_part instanceof Type\Atomic\TTemplateParam || $lhs_type_part instanceof Type\Atomic\TClassString ) { if ($stmt->name instanceof PhpParser\Node\Identifier) { $codebase->analyzer->addMixedMemberName( strtolower($stmt->name->name), $context->calling_method_id ?: $statements_analyzer->getFileName() ); } if (IssueBuffer::accepts( new MixedMethodCall( 'Cannot call method on an unknown class', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } if ($lhs_type_part instanceof Type\Atomic\TString) { if ($config->allow_string_standin_for_class && !$lhs_type_part instanceof Type\Atomic\TNumericString ) { return; } if (IssueBuffer::accepts( new InvalidStringClass( 'String cannot be used as a class', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() )) { // fall through } return; } if ($lhs_type_part instanceof Type\Atomic\TNull && $ignore_nullable_issues ) { return; } if (IssueBuffer::accepts( new UndefinedClass( 'Type ' . $lhs_type_part . ' cannot be called as a class', new CodeLocation($statements_analyzer->getSource(), $stmt), (string) $lhs_type_part ), $statements_analyzer->getSuppressedIssues() )) { // fall through } } }