Repository: astral-sh/ruff
File: crates/ty_python_semantic/src/types/call/bind.rs (Rust)

Code before:
                match (tcx.annotation, &mut combined_tcx.annotation) {
                    (Some(_), None) => combined_tcx = tcx,
                    (Some(ty), Some(combined_ty)) => {
                        *combined_ty = UnionType::from_elements(self.db, [*combined_ty, ty]);
                    }
                    _ => {}
                }

                variance_in_return = variance_in_return.join(variance);
            };

            return_ty.visit_specialization(self.db, self.call_expression_tcx, visit_return_ty);

            // Promotion is only useful if the type variable is in invariant or contravariant
            // position in the return type.
            if variance_in_return.is_covariant() {
                return ty;
            }

            // If the type variable is a non-covariant position in any argument, then we avoid
            // promotion, respecting any literals in the parameter type.
            if variance_in_arguments
                .get(&typevar.identity(self.db))
                .is_some_and(|variance| !variance.is_covariant())
            {
                return ty;
            }

            ty.promote_literals(self.db, combined_tcx)
        };

        let specialization = builder
            .mapped(generic_context, maybe_promote)
            .build(generic_context);

        self.return_ty = self.return_ty.apply_specialization(self.db, specialization);
        self.specialization = Some(specialization);
    }

    fn infer_argument_types(
        &mut self,
        builder: &mut SpecializationBuilder<'db>,
        preferred_type_mappings: &FxHashMap<BoundTypeVarIdentity<'db>, Type<'db>>,
        partially_specialized_declared_type: &FxHashSet<BoundTypeVarIdentity<'_>>,
        variance_in_arguments: &mut FxHashMap<BoundTypeVarIdentity<'db>, TypeVarVariance>,
        specialization_errors: &mut Vec<BindingError<'db>>,
    ) -> bool {
        let mut assignable_to_declared_type = true;

        let parameters = self.signature.parameters();
        for (argument_index, adjusted_argument_index, _, argument_type) in

Review comment (suggestion):
This requirement is extra conservative. Consider:

```py
def f(a: int, b: int = 0, c: str = ""): ...

def _(x: tuple[int] | tuple[int, int]):
    f(*x)
```

This call should pass, and is an instance of the same issue we are aiming to fix here. But this fix won't apply to it, because `a: int` parameter has no default.

Handling this case would require tracking the number of non-defaulted arguments, and erroring if any iterable element of the union doesn't contain enough items to cover all non-defaulted arguments.

Fine with me to defer this type of case to a future PR (this PR is still a strict improvement over the status quo), but we should add a test for it with TODO comment, and a TODO here in the code, to help preserve context for that future PR.

Code after:
            Some(argument_type) => match argument_type.as_paramspec_typevar(db) {
                Some(paramspec) => VariadicArgumentType::ParamSpec(paramspec),
                None => match argument_type {
                    // `Type::iterate` unions tuple specs in a way that can invent additional
                    // arities. Iterate each union element individually and compute per-position
                    // union types, length bounds, and variable element so that the rest of the
                    // matching logic handles unions correctly.
                    //
                    // We restrict this to cases where all remaining positional parameters are
                    // defaulted and there is no variadic parameter, because the per-position
                    // union loses the correlation between element lengths and per-position types.
                    // For example, given overloads `f(x: int, y: int)` and `f(x: int, y: str, z: int)`
                    // with `t: tuple[int, str] | tuple[int, str, int]`, the per-position union
                    // would collapse the two arities, preventing the expansion step from correctly
                    // splitting the union into separate argument lists per overload.
                    //
                    // TODO: This is overly conservative. We could also apply this when all
                    // non-defaulted parameters are covered by the shortest union element,
                    // e.g. `f(a: int, b: int = 0)` with `*x` where `x: tuple[int] | tuple[int, int]`.
                    Type::Union(union)
                        if self.parameters.variadic().is_none()
                            && self
                                .parameters
                                .positional()
                                .skip(self.next_positional)
                                .all(|parameter| parameter.default_type().is_some()) =>
                    {
                        let tuple_specs: Vec<_> =
                            union.elements(db).iter().map(|ty| ty.iterate(db)).collect();

                        let min_len = tuple_specs
                            .iter()
                            .map(|s| s.len().minimum())
                            .min()
                            .unwrap_or(0);
                        let any_variable = tuple_specs.iter().any(|s| s.len().is_variable());
                        let max_elements = tuple_specs
                            .iter()
                            .map(|s| s.all_elements().len())
                            .max()
                            .unwrap_or(0);

                        let variable_element = {
                            let var_types: Vec<_> = tuple_specs
                                .iter()
                                .filter_map(|s| s.variable_element().copied())
                                .collect();
                            if var_types.is_empty() {
                                None
                            } else {
                                Some(UnionType::from_elements_leave_aliases(db, var_types))