getFQCLN(); $project_analyzer = $source->getFileAnalyzer()->project_analyzer; $codebase = $source->getCodebase(); if ($context->collect_mutations && $context->self && ( $context->self === $fq_class_name || $codebase->classExtends( $context->self, $fq_class_name, ) ) ) { $method_id = new MethodIdentifier( $fq_class_name, $method_name_lc, ); if ((string) $method_id !== $source->getId()) { if ($context->collect_initializations) { if (isset($context->initialized_methods[(string) $method_id])) { return; } if ($context->initialized_methods === null) { $context->initialized_methods = []; } $context->initialized_methods[(string) $method_id] = true; } $project_analyzer->getMethodMutations( $method_id, $context, $source->getRootFilePath(), $source->getRootFileName(), ); } } elseif ($context->collect_initializations && $context->self && ( $context->self === $fq_class_name || $codebase->classlikes->classExtends( $context->self, $fq_class_name, ) ) && $source->getMethodName() !== $method_name ) { $method_id = new MethodIdentifier($fq_class_name, $method_name_lc); $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); if (isset($context->vars_in_scope['$this'])) { foreach ($context->vars_in_scope['$this']->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TNamedObject) { if ($fq_class_name === $atomic_type->value) { $alt_declaring_method_id = $declaring_method_id; } else { $fq_class_name = $atomic_type->value; $method_id = new MethodIdentifier( $fq_class_name, $method_name_lc, ); $alt_declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); } if ($alt_declaring_method_id) { $declaring_method_id = $alt_declaring_method_id; break; } if (!$atomic_type->extra_types) { continue; } foreach ($atomic_type->extra_types as $intersection_type) { if ($intersection_type instanceof TNamedObject) { $fq_class_name = $intersection_type->value; $method_id = new MethodIdentifier( $fq_class_name, $method_name_lc, ); $alt_declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); if ($alt_declaring_method_id) { $declaring_method_id = $alt_declaring_method_id; break 2; } } } } } } if (!$declaring_method_id) { // can happen for __call return; } if (isset($context->initialized_methods[(string) $declaring_method_id])) { return; } if ($context->initialized_methods === null) { $context->initialized_methods = []; } $context->initialized_methods[(string) $declaring_method_id] = true; $method_storage = $codebase->methods->getStorage($declaring_method_id); $class_analyzer = $source->getSource(); $is_final = $method_storage->final; if ($method_name !== $declaring_method_id->method_name) { $appearing_method_id = $codebase->methods->getAppearingMethodId($method_id); if ($appearing_method_id) { $appearing_class_storage = $codebase->classlike_storage_provider->get( $appearing_method_id->fq_class_name, ); if (isset($appearing_class_storage->trait_final_map[$method_name_lc])) { $is_final = true; } } } if ($class_analyzer instanceof ClassLikeAnalyzer && !$method_storage->is_static && ($context->collect_nonprivate_initializations || $method_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE || $is_final) ) { $local_vars_in_scope = []; foreach ($context->vars_in_scope as $var_id => $type) { if (strpos($var_id, '$this->') === 0) { if ($type->initialized) { $local_vars_in_scope[$var_id] = $context->vars_in_scope[$var_id]; $context->remove($var_id, false); } } elseif ($var_id !== '$this') { $local_vars_in_scope[$var_id] = $context->vars_in_scope[$var_id]; } } $local_vars_possibly_in_scope = $context->vars_possibly_in_scope; $old_calling_method_id = $context->calling_method_id; if ($fq_class_name === $source->getFQCLN()) { $class_analyzer->getMethodMutations($declaring_method_id->method_name, $context); } else { $declaring_fq_class_name = $declaring_method_id->fq_class_name; $old_self = $context->self; $context->self = $declaring_fq_class_name; $project_analyzer->getMethodMutations( $declaring_method_id, $context, $source->getRootFilePath(), $source->getRootFileName(), ); $context->self = $old_self; } $context->calling_method_id = $old_calling_method_id; foreach ($local_vars_in_scope as $var => $type) { $context->vars_in_scope[$var] = $type; } foreach ($local_vars_possibly_in_scope as $var => $_) { $context->vars_possibly_in_scope[$var] = true; } } } } /** * @param list $args */ public static function checkMethodArgs( ?MethodIdentifier $method_id, array $args, TemplateResult $template_result, Context $context, CodeLocation $code_location, StatementsAnalyzer $statements_analyzer ): bool { $codebase = $statements_analyzer->getCodebase(); if (!$method_id) { return ArgumentsAnalyzer::analyze( $statements_analyzer, $args, null, null, true, $context, $template_result, ) !== false; } $method_params = $codebase->methods->getMethodParams($method_id, $statements_analyzer, $args, $context); $fq_class_name = $method_id->fq_class_name; $method_name = $method_id->method_name; $fq_class_name = strtolower($codebase->classlikes->getUnAliasedName($fq_class_name)); $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); $method_storage = null; if (isset($class_storage->declaring_method_ids[$method_name])) { $declaring_method_id = $class_storage->declaring_method_ids[$method_name]; $declaring_fq_class_name = $declaring_method_id->fq_class_name; $declaring_method_name = $declaring_method_id->method_name; if ($declaring_fq_class_name !== $fq_class_name) { $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_fq_class_name); } else { $declaring_class_storage = $class_storage; } if (!isset($declaring_class_storage->methods[$declaring_method_name])) { throw new UnexpectedValueException('Storage should not be empty here'); } $method_storage = $declaring_class_storage->methods[$declaring_method_name]; if ($declaring_class_storage->user_defined && !$method_storage->has_docblock_param_types && isset($declaring_class_storage->documenting_method_ids[$method_name]) ) { $documenting_method_id = $declaring_class_storage->documenting_method_ids[$method_name]; $documenting_method_storage = $codebase->methods->getStorage($documenting_method_id); if ($documenting_method_storage->template_types) { $method_storage = $documenting_method_storage; } } if (!$context->isSuppressingExceptions($statements_analyzer)) { $context->mergeFunctionExceptions($method_storage, $code_location); } } if (ArgumentsAnalyzer::analyze( $statements_analyzer, $args, $method_params, (string) $method_id, $method_storage->allow_named_arg_calls ?? true, $context, $template_result, ) === false) { return false; } if (ArgumentsAnalyzer::checkArgumentsMatch( $statements_analyzer, $args, $method_id, $method_params, $method_storage, $class_storage, $template_result, $code_location, $context, ) === false) { return false; } if ($template_result->template_types) { self::checkTemplateResult( $statements_analyzer, $template_result, $code_location, strtolower((string) $method_id), ); } return true; } /** * This gets all the template params (and their types) that we think * we'll need to know about * * @return array> * @param array> $existing_template_types * @param array> $class_template_params */ public static function getTemplateTypesForCall( Codebase $codebase, ?ClassLikeStorage $declaring_class_storage, ?string $appearing_class_name, ?ClassLikeStorage $calling_class_storage, array $existing_template_types = [], array $class_template_params = [] ): array { $template_types = $existing_template_types; if ($declaring_class_storage) { if ($calling_class_storage && $declaring_class_storage !== $calling_class_storage && $calling_class_storage->template_extended_params ) { foreach ($calling_class_storage->template_extended_params as $class_name => $type_map) { foreach ($type_map as $template_name => $type) { if ($class_name === $declaring_class_storage->name) { $output_type = null; foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TTemplateParam) { $output_type_candidate = self::getGenericParamForOffset( $atomic_type->defining_class, $atomic_type->param_name, $calling_class_storage->template_extended_params, $class_template_params + $template_types, ); } else { $output_type_candidate = new Union([$atomic_type]); } $output_type = Type::combineUnionTypes( $output_type_candidate, $output_type, ); } $template_types[$template_name][$declaring_class_storage->name] = $output_type; } } } } elseif ($declaring_class_storage->template_types) { foreach ($declaring_class_storage->template_types as $template_name => $type_map) { foreach ($type_map as $key => $type) { $template_types[$template_name][$key] = $class_template_params[$template_name][$key] ?? $type; } } } } foreach ($template_types as $key => $type_map) { foreach ($type_map as $class => $type) { $template_types[$key][$class] = TypeExpander::expandUnion( $codebase, $type, $appearing_class_name, $calling_class_storage->name ?? null, null, true, false, $calling_class_storage->final ?? false, ); } } return $template_types; } /** * @param array> $template_extended_params * @param array> $found_generic_params */ public static function getGenericParamForOffset( string $fq_class_name, string $template_name, array $template_extended_params, array $found_generic_params ): Union { if (isset($found_generic_params[$template_name][$fq_class_name])) { return $found_generic_params[$template_name][$fq_class_name]; } foreach ($template_extended_params as $extended_class_name => $type_map) { foreach ($type_map as $extended_template_name => $extended_type) { foreach ($extended_type->getAtomicTypes() as $extended_atomic_type) { if ($extended_atomic_type instanceof TTemplateParam && $extended_atomic_type->param_name === $template_name && $extended_atomic_type->defining_class === $fq_class_name ) { return self::getGenericParamForOffset( $extended_class_name, $extended_template_name, $template_extended_params, $found_generic_params, ); } } } } return Type::getMixed(); } /** * @param PhpParser\Node\Scalar\String_|PhpParser\Node\Expr\Array_|PhpParser\Node\Expr\BinaryOp\Concat $callable_arg * @return list */ public static function getFunctionIdsFromCallableArg( FileSource $file_source, PhpParser\Node\Expr $callable_arg ): array { if ($callable_arg instanceof PhpParser\Node\Expr\BinaryOp\Concat) { if ($callable_arg->left instanceof PhpParser\Node\Expr\ClassConstFetch && $callable_arg->left->class instanceof Name && $callable_arg->left->name instanceof Identifier && strtolower($callable_arg->left->name->name) === 'class' && !in_array(strtolower($callable_arg->left->class->getFirst()), ['self', 'static', 'parent']) && $callable_arg->right instanceof PhpParser\Node\Scalar\String_ && preg_match('/^::[A-Za-z0-9]+$/', $callable_arg->right->value) ) { $r = (string) $callable_arg->left->class->getAttribute('resolvedName') . $callable_arg->right->value; assert($r !== ''); return [$r]; } return []; } if ($callable_arg instanceof PhpParser\Node\Scalar\String_) { $potential_id = preg_replace('/^\\\/', '', $callable_arg->value, 1); if (preg_match('/^[A-Za-z0-9_]+(\\\[A-Za-z0-9_]+)*(::[A-Za-z0-9_]+)?$/', $potential_id)) { assert($potential_id !== ''); return [$potential_id]; } return []; } if (count($callable_arg->items) !== 2) { return []; } /** @psalm-suppress PossiblyNullPropertyFetch */ if ($callable_arg->items[0]->key || $callable_arg->items[1]->key) { return []; } if (!isset($callable_arg->items[0]) || !isset($callable_arg->items[1])) { throw new UnexpectedValueException('These should never be unset'); } $class_arg = $callable_arg->items[0]->value; $method_name_arg = $callable_arg->items[1]->value; if (!$method_name_arg instanceof PhpParser\Node\Scalar\String_) { return []; } if ($class_arg instanceof PhpParser\Node\Scalar\String_) { return [preg_replace('/^\\\/', '', $class_arg->value, 1) . '::' . $method_name_arg->value]; } if ($class_arg instanceof PhpParser\Node\Expr\ClassConstFetch && $class_arg->name instanceof Identifier && strtolower($class_arg->name->name) === 'class' && $class_arg->class instanceof Name ) { $fq_class_name = ClassLikeAnalyzer::getFQCLNFromNameObject( $class_arg->class, $file_source->getAliases(), ); return [$fq_class_name . '::' . $method_name_arg->value]; } if (!$file_source instanceof StatementsAnalyzer || !($class_arg_type = $file_source->node_data->getType($class_arg)) ) { return []; } $method_ids = []; foreach ($class_arg_type->getAtomicTypes() as $type_part) { if ($type_part instanceof TNamedObject) { $method_id = $type_part->value . '::' . $method_name_arg->value; foreach ($type_part->extra_types as $extra_type) { if ($extra_type instanceof TTemplateParam || $extra_type instanceof TObjectWithProperties || $extra_type instanceof TCallableObject ) { throw new UnexpectedValueException('Shouldn’t get a generic param here'); } $method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value; } $method_ids[] = '$' . $method_id; } } return $method_ids; } /** * @param non-empty-string $function_id * @param bool $can_be_in_root_scope if true, the function can be shortened to the root version */ public static function checkFunctionExists( StatementsAnalyzer $statements_analyzer, string &$function_id, CodeLocation $code_location, bool $can_be_in_root_scope ): bool { $cased_function_id = $function_id; $function_id = strtolower($function_id); $codebase = $statements_analyzer->getCodebase(); if (!$codebase->functions->functionExists($statements_analyzer, $function_id)) { /** @var non-empty-lowercase-string */ $root_function_id = preg_replace('/.*\\\/', '', $function_id); if ($can_be_in_root_scope && $function_id !== $root_function_id && $codebase->functions->functionExists($statements_analyzer, $root_function_id) ) { $function_id = $root_function_id; } else { IssueBuffer::maybeAdd( new UndefinedFunction( 'Function ' . $cased_function_id . ' does not exist', $code_location, $function_id, ), $statements_analyzer->getSuppressedIssues(), ); return false; } } return true; } /** * @param Identifier|Name $expr * @param Possibilities[] $var_assertions * @param list $args */ public static function applyAssertionsToContext( PhpParser\NodeAbstract $expr, ?string $thisName, array $var_assertions, array $args, TemplateResult $template_result, Context $context, StatementsAnalyzer $statements_analyzer ): void { $type_assertions = []; $asserted_keys = []; foreach ($var_assertions as $var_possibilities) { $assertion_var_id = null; $arg_value = null; if (is_int($var_possibilities->var_id)) { if (!isset($args[$var_possibilities->var_id])) { continue; } $arg_value = $args[$var_possibilities->var_id]->value; $arg_var_id = ExpressionIdentifier::getExtendedVarId($arg_value, null, $statements_analyzer); if ($arg_var_id) { $assertion_var_id = $arg_var_id; } } elseif ($var_possibilities->var_id === '$this' && $thisName !== null) { $assertion_var_id = $thisName; } elseif (strpos($var_possibilities->var_id, '$this->') === 0 && $thisName !== null) { $assertion_var_id = $thisName . str_replace('$this->', '->', $var_possibilities->var_id); } elseif (strpos($var_possibilities->var_id, 'self::') === 0 && $context->self) { $assertion_var_id = $context->self . str_replace('self::', '::', $var_possibilities->var_id); } elseif (strpos($var_possibilities->var_id, '::$') !== false) { // allow assertions to bring external static props into scope $assertion_var_id = $var_possibilities->var_id; } elseif (isset($context->vars_in_scope[$var_possibilities->var_id])) { $assertion_var_id = $var_possibilities->var_id; } elseif (strpos($var_possibilities->var_id, '->') !== false) { $exploded = explode('->', $var_possibilities->var_id); if (count($exploded) < 2) { IssueBuffer::maybeAdd( new InvalidDocblock( 'Assert notation is malformed', new CodeLocation($statements_analyzer, $expr), ), ); continue; } [$var_id, $property] = $exploded; $var_id = is_numeric($var_id) ? (int) $var_id : $var_id; if (!is_int($var_id) || !isset($args[$var_id])) { IssueBuffer::maybeAdd( new InvalidDocblock( 'Variable ' . $var_id . ' is not an argument so cannot be asserted', new CodeLocation($statements_analyzer, $expr), ), ); continue; } /** @var PhpParser\Node\Expr\Variable $arg_value */ $arg_value = $args[$var_id]->value; $arg_var_id = ExpressionIdentifier::getExtendedVarId($arg_value, null, $statements_analyzer); if (!$arg_var_id) { IssueBuffer::maybeAdd( new InvalidDocblock( 'Variable being asserted as argument ' . ($var_id+1) . ' cannot be found in local scope', new CodeLocation($statements_analyzer, $expr), ), ); continue; } if (count($exploded) === 2) { $failedMessage = AssertionFinder::isPropertyImmutableOnArgument( $property, $statements_analyzer->getNodeTypeProvider(), $statements_analyzer->getCodebase()->classlike_storage_provider, $arg_value, ); if (null !== $failedMessage) { IssueBuffer::maybeAdd( new InvalidDocblock($failedMessage, new CodeLocation($statements_analyzer, $expr)), ); continue; } } $assertion_var_id = str_replace((string) $var_id, $arg_var_id, $var_possibilities->var_id); } $codebase = $statements_analyzer->getCodebase(); if ($assertion_var_id) { $orred_rules = []; foreach ($var_possibilities->rule as $assertion_rule) { $assertion_type_atomic = $assertion_rule->getAtomicType(); if ($assertion_type_atomic) { $assertion_type = TemplateInferredTypeReplacer::replace( new Union([$assertion_type_atomic]), $template_result, $codebase, ); if (count($assertion_type->getAtomicTypes()) === 1) { foreach ($assertion_type->getAtomicTypes() as $atomic_type) { if ($assertion_type_atomic instanceof TTemplateParam && $assertion_type_atomic->as->getId() === $atomic_type->getId() ) { continue; } $assertion_rule = $assertion_rule->setAtomicType($atomic_type); $orred_rules[] = $assertion_rule; } } elseif (isset($context->vars_in_scope[$assertion_var_id])) { $asserted_type = $context->vars_in_scope[$assertion_var_id]; if ($assertion_rule instanceof IsIdentical) { $intersection = Type::intersectUnionTypes($assertion_type, $asserted_type, $codebase); if ($intersection === null) { IssueBuffer::maybeAdd( new TypeDoesNotContainType( $asserted_type->getId() . ' is not contained by ' . $assertion_type->getId(), new CodeLocation($statements_analyzer->getSource(), $expr), $asserted_type->getId() . ' ' . $assertion_type->getId(), ), $statements_analyzer->getSuppressedIssues(), ); $intersection = Type::getNever(); } elseif ($intersection->getId(true) === $asserted_type->getId(true)) { continue; } foreach ($intersection->getAtomicTypes() as $atomic_type) { $orred_rules[] = new IsIdentical($atomic_type); } } elseif ($assertion_rule instanceof IsType) { if (!UnionTypeComparator::canExpressionTypesBeIdentical( $codebase, $assertion_type, $asserted_type, )) { IssueBuffer::maybeAdd( new TypeDoesNotContainType( $asserted_type->getId() . ' is not contained by ' . $assertion_type->getId(), new CodeLocation($statements_analyzer->getSource(), $expr), $asserted_type->getId() . ' ' . $assertion_type->getId(), ), $statements_analyzer->getSuppressedIssues(), ); } } else { // Ignore negations and loose assertions with union types } } } else { $orred_rules[] = $assertion_rule; } } if ($orred_rules) { if (isset($type_assertions[$assertion_var_id])) { $type_assertions[$assertion_var_id] = array_merge( $type_assertions[$assertion_var_id], [$orred_rules], ); } else { $type_assertions[$assertion_var_id] = [$orred_rules]; } } } elseif ($arg_value && count($var_possibilities->rule) === 1 ) { $assert_clauses = []; $single_rule = $var_possibilities->rule[0]; if ($single_rule instanceof Truthy) { $assert_clauses = FormulaGenerator::getFormula( spl_object_id($arg_value), spl_object_id($arg_value), $arg_value, $context->self, $statements_analyzer, $statements_analyzer->getCodebase(), ); } elseif ($single_rule instanceof Falsy) { $assert_clauses = Algebra::negateFormula( FormulaGenerator::getFormula( spl_object_id($arg_value), spl_object_id($arg_value), $arg_value, $context->self, $statements_analyzer, $codebase, ), ); } elseif ($single_rule instanceof IsType && $single_rule->type instanceof TTrue ) { $conditional = new VirtualIdentical( $arg_value, new VirtualConstFetch(new VirtualName('true')), ); $assert_clauses = FormulaGenerator::getFormula( mt_rand(0, 1_000_000), mt_rand(0, 1_000_000), $conditional, $context->self, $statements_analyzer, $codebase, ); } $simplified_clauses = Algebra::simplifyCNF( [...$context->clauses, ...$assert_clauses], ); $assert_type_assertions = Algebra::getTruthsFromFormula( $simplified_clauses, ); $type_assertions = array_merge($type_assertions, $assert_type_assertions); } } $changed_var_ids = []; foreach ($type_assertions as $var_id => $_) { $asserted_keys[$var_id] = true; } $codebase = $statements_analyzer->getCodebase(); if ($type_assertions) { $template_type_map = []; // while in an and, we allow scope to boil over to support // statements of the form if ($x && $x->foo()) [$op_vars_in_scope, $op_references_in_scope] = Reconciler::reconcileKeyedTypes( $type_assertions, $type_assertions, $context->vars_in_scope, $context->references_in_scope, $changed_var_ids, $asserted_keys, $statements_analyzer, $template_type_map, $context->inside_loop, new CodeLocation($statements_analyzer->getSource(), $expr), ); foreach ($changed_var_ids as $var_id => $_) { if (isset($op_vars_in_scope[$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 FunctionLikeAnalyzer) || !$parent_source->getSource() instanceof TraitAnalyzer) ) { $codebase->analyzer->decrementMixedCount($statements_analyzer->getFilePath()); } IssueBuffer::remove( $statements_analyzer->getFilePath(), 'MixedAssignment', $first_appearance->raw_file_start, ); } $op_vars_in_scope[$var_id] = $op_vars_in_scope[$var_id]->setFromDocblock(true); } } $context->vars_in_scope = $op_vars_in_scope; $context->references_in_scope = $op_references_in_scope; } } /** * This method looks for problems with a generated TemplateResult. * * The TemplateResult object contains upper bounds and lower bounds for each template param. * * Those upper bounds represent a series of constraints like * * Lower bound: * T >: X (the type param T matches X, or is a supertype of X) * Upper bound: * T <: Y (the type param T matches Y, or is a subtype of Y) * Equality (currently represented as an upper bound with a special flag) * T = Z (the template T must match Z) * * This method attempts to reconcile those constraints. * * Valid constraints: * * T <: int|float, T >: int --- implies T is an int * T = int --- implies T is an int * * Invalid constraints: * * T <: int|string, T >: string|float --- implies T <: int and T >: float, which is impossible * T = int, T = string --- implies T is a string _and_ and int, which is impossible */ public static function checkTemplateResult( StatementsAnalyzer $statements_analyzer, TemplateResult $template_result, CodeLocation $code_location, ?string $function_id ): void { if ($template_result->lower_bounds && $template_result->upper_bounds) { foreach ($template_result->upper_bounds as $template_name => $defining_map) { foreach ($defining_map as $defining_id => $upper_bound) { if (isset($template_result->lower_bounds[$template_name][$defining_id])) { $lower_bound_type = TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( $template_result->lower_bounds[$template_name][$defining_id], $statements_analyzer->getCodebase(), ); $upper_bound_type = $upper_bound->type; $union_comparison_result = new TypeComparisonResult(); if (count($template_result->upper_bounds_unintersectable_types) > 1) { [$lower_bound_type, $upper_bound_type] = $template_result->upper_bounds_unintersectable_types; } if (!UnionTypeComparator::isContainedBy( $statements_analyzer->getCodebase(), $lower_bound_type, $upper_bound_type, false, false, $union_comparison_result, )) { if ($union_comparison_result->type_coerced) { if ($union_comparison_result->type_coerced_from_mixed) { IssueBuffer::maybeAdd( new MixedArgumentTypeCoercion( 'Type ' . $lower_bound_type->getId() . ' should be a subtype of ' . $upper_bound_type->getId(), $code_location, $function_id, ), $statements_analyzer->getSuppressedIssues(), ); } else { IssueBuffer::maybeAdd( new ArgumentTypeCoercion( 'Type ' . $lower_bound_type->getId() . ' should be a subtype of ' . $upper_bound_type->getId(), $code_location, $function_id, ), $statements_analyzer->getSuppressedIssues(), ); } } elseif ($union_comparison_result->scalar_type_match_found) { IssueBuffer::maybeAdd( new InvalidScalarArgument( 'Type ' . $lower_bound_type->getId() . ' should be a subtype of ' . $upper_bound_type->getId(), $code_location, $function_id, ), $statements_analyzer->getSuppressedIssues(), ); } else { IssueBuffer::maybeAdd( new InvalidArgument( 'Type ' . $lower_bound_type->getId() . ' should be a subtype of ' . $upper_bound_type->getId(), $code_location, $function_id, ), $statements_analyzer->getSuppressedIssues(), ); } } } else { $template_result->lower_bounds[$template_name][$defining_id] = [ new TemplateBound( $upper_bound->type, ), ]; } } } } // Attempt to identify invalid lower bounds foreach ($template_result->lower_bounds as $template_name => $lower_bounds) { foreach ($lower_bounds as $lower_bounds) { if (count($lower_bounds) > 1) { $bounds_with_equality = array_filter( $lower_bounds, static fn($lower_bound): bool => (bool)$lower_bound->equality_bound_classlike, ); if (!$bounds_with_equality) { continue; } $equality_types = array_unique( array_map( static fn($bound_with_equality) => $bound_with_equality->type->getId(), $bounds_with_equality, ), ); if (count($equality_types) > 1) { IssueBuffer::maybeAdd( new InvalidArgument( 'Incompatible types found for ' . $template_name . ' (must have only one of ' . implode(', ', $equality_types) . ')', $code_location, $function_id, ), $statements_analyzer->getSuppressedIssues(), ); } else { foreach ($lower_bounds as $lower_bound) { if ($lower_bound->equality_bound_classlike === null) { foreach ($bounds_with_equality as $bound_with_equality) { if (UnionTypeComparator::isContainedBy( $statements_analyzer->getCodebase(), $lower_bound->type, $bound_with_equality->type, )) { continue 2; } } IssueBuffer::maybeAdd( new InvalidArgument( 'Incompatible types found for ' . $template_name . ' (' . $lower_bound->type->getId() . ' is not in ' . implode(', ', $equality_types) . ')', $code_location, $function_id, ), $statements_analyzer->getSuppressedIssues(), ); } } } } } } } }