Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,23 @@ def foo1(c: Callable[P, int]) -> None:
**kwargs: P.args,
) -> None: ...

# TODO: error
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
def nested3(*args: P.args) -> None: ...

# TODO: error
# error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`"
def nested4(**kwargs: P.kwargs) -> None: ...

# TODO: error
# error: [invalid-paramspec] "No parameters may appear between `*args: P.args` and `**kwargs: P.kwargs`"
def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ...

# error: [invalid-paramspec] "`P.args` is only valid for annotating `*args`"
def nested6(x: P.args) -> None: ...
def nested7(
*args: P.args,
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
**kwargs: int,
) -> None: ...

# TODO: error
def bar1(*args: P.args, **kwargs: P.kwargs) -> None:
pass
Expand All @@ -223,17 +231,17 @@ And, they need to be used together.

```py
def foo2(c: Callable[P, int]) -> None:
# TODO: error
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
def nested1(*args: P.args) -> None: ...

# TODO: error
# error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`"
def nested2(**kwargs: P.kwargs) -> None: ...

class Foo2:
# TODO: error
# error: [invalid-paramspec] "`P.args` is only valid for annotating `*args` function parameters"
args: P.args

# TODO: error
# error: [invalid-paramspec] "`P.kwargs` is only valid for annotating `**kwargs` function parameters"
kwargs: P.kwargs
```

Expand All @@ -252,6 +260,23 @@ class Foo3(Generic[P]):
) -> None: ...
```

Error messages for `invalid-paramspec` also use the actual parameter names:

```py
def bar(c: Callable[P, int]) -> None:
# error: [invalid-paramspec] "`*my_args: P.args` must be accompanied by `**my_kwargs: P.kwargs`"
def f1(*my_args: P.args, **my_kwargs: int) -> None: ...

# error: [invalid-paramspec] "`*positional: P.args` must be accompanied by `**kwargs: P.kwargs`"
def f2(*positional: P.args) -> None: ...

# error: [invalid-paramspec] "`**keyword: P.kwargs` must be accompanied by `*args: P.args`"
def f3(**keyword: P.kwargs) -> None: ...

# error: [invalid-paramspec] "No parameters may appear between `*a: P.args` and `**kw: P.kwargs`"
def f4(*a: P.args, x: int, **kw: P.kwargs) -> None: ...
```

## Specializing generic classes explicitly

```py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,31 +103,39 @@ def foo[**P](c: Callable[P, int]) -> None:
# error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?"
def nested2(*args: P.kwargs, **kwargs: P.args) -> None: ...

# TODO: error
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
def nested3(*args: P.args) -> None: ...

# TODO: error
# error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`"
def nested4(**kwargs: P.kwargs) -> None: ...

# TODO: error
# error: [invalid-paramspec] "No parameters may appear between `*args: P.args` and `**kwargs: P.kwargs`"
def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ...

# error: [invalid-paramspec] "`P.args` is only valid for annotating `*args`"
def nested6(x: P.args) -> None: ...
def nested7(
*args: P.args,
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
**kwargs: int,
) -> None: ...
```

And, they need to be used together.

```py
def foo[**P](c: Callable[P, int]) -> None:
# TODO: error
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
def nested1(*args: P.args) -> None: ...

# TODO: error
# error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`"
def nested2(**kwargs: P.kwargs) -> None: ...

class Foo[**P]:
# TODO: error
# error: [invalid-paramspec] "`P.args` is only valid for annotating `*args` function parameters"
args: P.args

# TODO: error
# error: [invalid-paramspec] "`P.kwargs` is only valid for annotating `**kwargs` function parameters"
kwargs: P.kwargs
```

Expand All @@ -146,15 +154,32 @@ class Foo3[**P]:
) -> None: ...
```

Error messages for `invalid-paramspec` also use the actual parameter names:

```py
def bar[**P](c: Callable[P, int]) -> None:
# error: [invalid-paramspec] "`*my_args: P.args` must be accompanied by `**my_kwargs: P.kwargs`"
def f1(*my_args: P.args, **my_kwargs: int) -> None: ...

# error: [invalid-paramspec] "`*positional: P.args` must be accompanied by `**kwargs: P.kwargs`"
def f2(*positional: P.args) -> None: ...

# error: [invalid-paramspec] "`**keyword: P.kwargs` must be accompanied by `*args: P.args`"
def f3(**keyword: P.kwargs) -> None: ...

# error: [invalid-paramspec] "No parameters may appear between `*a: P.args` and `**kw: P.kwargs`"
def f4(*a: P.args, x: int, **kw: P.kwargs) -> None: ...
```

It isn't allowed to annotate an instance attribute either:

```py
class Foo4[**P]:
def __init__(self, fn: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None:
self.fn = fn
# TODO: error
# error: [invalid-paramspec] "`P.args` is only valid for annotating `*args` function parameters"
self.args: P.args = args
# TODO: error
# error: [invalid-paramspec] "`P.kwargs` is only valid for annotating `**kwargs` function parameters"
self.kwargs: P.kwargs = kwargs
```

Expand Down
112 changes: 112 additions & 0 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ use crate::types::generics::{
GenericContext, InferableTypeVars, SpecializationBuilder, bind_typevar,
enclosing_generic_contexts, typing_self,
};
use crate::types::infer::builder::paramspec_validation::validate_paramspec_components;
use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function};
use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind};
use crate::types::newtype::NewType;
Expand Down Expand Up @@ -149,6 +150,7 @@ use crate::unpack::{EvaluationMode, UnpackPosition};
use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet, Program};

mod annotation_expression;
mod paramspec_validation;
mod type_expression;

/// Whether the intersection type is on the left or right side of the comparison.
Expand Down Expand Up @@ -3176,6 +3178,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
for parameter in &function.parameters {
self.infer_definition(parameter);
}

validate_paramspec_components(&self.context, &function.parameters, |expr| {
self.file_expression_type(expr)
});

self.infer_body(&function.body);

if let Some(returns) = function.returns.as_deref() {
Expand Down Expand Up @@ -3668,6 +3675,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let default_expr = default.as_ref();
if let Some(annotation) = parameter.annotation.as_ref() {
let declared_ty = self.file_expression_type(annotation);

// P.args and P.kwargs are only valid as annotations on *args and **kwargs,
// not on regular parameters.
if let Type::TypeVar(typevar) = declared_ty
&& typevar.is_paramspec(self.db())
&& let Some(attr) = typevar.paramspec_attr(self.db())
{
let name = typevar.name(self.db());
let (attr_name, variadic) = match attr {
ParamSpecAttrKind::Args => ("args", "*args"),
ParamSpecAttrKind::Kwargs => ("kwargs", "**kwargs"),
};
if let Some(builder) = self
.context
.report_lint(&INVALID_PARAMSPEC, annotation.as_ref())
{
builder.into_diagnostic(format_args!(
"`{name}.{attr_name}` is only valid for annotating `{variadic}`",
));
}
}

if let Some(default_expr) = default_expr {
let default_expr = default_expr.as_ref();
let default_ty = self.file_expression_type(default_expr);
Expand Down Expand Up @@ -9224,6 +9253,49 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}

// P.args and P.kwargs are only valid as annotations on *args and **kwargs.
if let Type::TypeVar(typevar) = annotated.inner_type()
&& typevar.is_paramspec(self.db())
&& let Some(attr) = typevar.paramspec_attr(self.db())
{
let name = typevar.name(self.db());
let (attr_name, variadic) = match attr {
ParamSpecAttrKind::Args => ("args", "*args"),
ParamSpecAttrKind::Kwargs => ("kwargs", "**kwargs"),
};
if let Some(builder) = self
.context
.report_lint(&INVALID_PARAMSPEC, annotation.as_ref())
{
builder.into_diagnostic(format_args!(
"`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters",
));
}
} else if let ast::Expr::Attribute(attr_expr) = annotation.as_ref()
&& matches!(attr_expr.attr.as_str(), "args" | "kwargs")
{
let value_ty = self.expression_type(&attr_expr.value);
if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty
&& typevar.is_paramspec(self.db())
{
let name = typevar.name(self.db());
let attr_name = &attr_expr.attr;
let variadic = if attr_name == "args" {
"*args"
} else {
"**kwargs"
};
if let Some(builder) = self
.context
.report_lint(&INVALID_PARAMSPEC, annotation.as_ref())
{
builder.into_diagnostic(format_args!(
"`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters",
));
}
}
}

let value_ty = value.as_ref().map(|value| {
self.infer_maybe_standalone_expression(
value,
Expand Down Expand Up @@ -9368,6 +9440,46 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
DeferredExpressionState::from(self.defer_annotations()),
);

// P.args and P.kwargs are only valid as annotations on *args and **kwargs,
// not as variable annotations. Check both resolved type and AST form.
if let Type::TypeVar(typevar) = declared.inner_type()
&& typevar.is_paramspec(self.db())
&& let Some(attr) = typevar.paramspec_attr(self.db())
{
let name = typevar.name(self.db());
let (attr_name, variadic) = match attr {
ParamSpecAttrKind::Args => ("args", "*args"),
ParamSpecAttrKind::Kwargs => ("kwargs", "**kwargs"),
};
if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, annotation) {
builder.into_diagnostic(format_args!(
"`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters",
));
}
} else if let ast::Expr::Attribute(attr_expr) = annotation
&& matches!(attr_expr.attr.as_str(), "args" | "kwargs")
{
// Also check the AST form for cases where P isn't bound (e.g., class body
// annotations). In this case, the type might not resolve to a TypeVar.
let value_ty = self.expression_type(&attr_expr.value);
if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty
&& typevar.is_paramspec(self.db())
{
let name = typevar.name(self.db());
let attr_name = &attr_expr.attr;
let variadic = if attr_name == "args" {
"*args"
} else {
"**kwargs"
};
if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, annotation) {
builder.into_diagnostic(format_args!(
"`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters",
));
}
}
}

let is_pep_613_type_alias = declared.inner_type().is_typealias_special_form();

if is_pep_613_type_alias
Expand Down
Loading