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
237 changes: 137 additions & 100 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ from typing_extensions import Self, TypeAlias, TypeVar
T = TypeVar("T")

# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter"
# error: [unbound-type-variable]
X: TypeAlias[T] = int

class Foo[T]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def _(x: ProtoInt[int]):

# TODO: TypedDict is just a function object at runtime, we should emit an error
class LegacyDict(TypedDict[T]):
# error: [unbound-type-variable]
x: T

type LegacyDictInt = LegacyDict[int]
Expand Down
54 changes: 46 additions & 8 deletions crates/ty_python_semantic/resources/mdtest/generics/scoping.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ from typing import TypeVar

T = TypeVar("T")

# TODO: error
# error: [unbound-type-variable]
x: T

class C:
# TODO: error
# error: [unbound-type-variable]
x: T

def f() -> None:
# TODO: error
# error: [unbound-type-variable]
x: T
```

Expand Down Expand Up @@ -186,11 +186,11 @@ S = TypeVar("S")

def f(x: T) -> None:
x: list[T] = []
# TODO: invalid-assignment error
# error: [unbound-type-variable]
y: list[S] = []

class C(Generic[T]):
# TODO: error: cannot use S if it's not in the current generic context
# error: [unbound-type-variable]
x: list[S] = []

# This is not an error, as shown in the previous test
Expand All @@ -210,11 +210,11 @@ S = TypeVar("S")

def f[T](x: T) -> None:
x: list[T] = []
# TODO: invalid assignment error
# error: [unbound-type-variable]
y: list[S] = []

class C[T]:
# TODO: error: cannot use S if it's not in the current generic context
# error: [unbound-type-variable]
x: list[S] = []

def m1(self, x: S) -> S:
Expand All @@ -224,6 +224,44 @@ class C[T]:
return x
```

## Should `Callable` annotations create an implicit generic context?

There is disagreement among type checkers around how to handle this case. For now, we do not emit an
error on the following snippet, but we may change this in the future.

```py
from typing import TypeVar, Callable
from ty_extensions import generic_context

T = TypeVar("T")

x: Callable[[T], T] = lambda obj: obj

# TODO: if we decide that `Callable` annotations always create an implicit generic context,
# all of these revealed types and `invalid-argument-type` diagnostics are incorrect.
# If we decide that they do not, we should emit `unbound-type-variable` on both the
# declaration of `x` in the global scope and the parameter annotation of `y`.
#
# NOTE: all the `reveal_type`s are inside a function here so that we test the behaviour
# of the declared type (from the annotation) rather than the local inferred type
def test(y: Callable[[T], T]):
# revealed: None
reveal_type(generic_context(x))
# revealed: (TypeVar, /) -> TypeVar
reveal_type(x)
# error: [invalid-argument-type]
# revealed: TypeVar
reveal_type(x(42))

# revealed: None
reveal_type(generic_context(y))
# revealed: (T@test, /) -> T@test
reveal_type(y)
# error: [invalid-argument-type]
# revealed: T@test
reveal_type(y(42))
```

## Nested formal typevars must be distinct

Generic functions and classes can be nested in each other, but it is an error for the same typevar
Expand Down Expand Up @@ -365,7 +403,7 @@ class C[T]:
ok1: list[T] = []

class Bad:
# TODO: error: cannot refer to T in nested scope
# error: [unbound-type-variable]
bad: list[T] = []

class Inner[S]: ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ def _(doubly_specialized: ProtoInt[int]):

# TODO: TypedDict is just a function object at runtime, we should emit an error
class LegacyDict(TypedDict[T]):
# error: [unbound-type-variable]
x: T

# TODO: should be a `not-subscriptable` error
Expand Down Expand Up @@ -784,7 +785,13 @@ def _(
Similarly, if you try to specialize a union type without a binding context, we emit an error:

```py
from typing import TypeVar

T = TypeVar("T")

# error: [not-subscriptable] "Cannot subscript non-generic type"
# error: [unbound-type-variable]
# error: [unbound-type-variable]
x: (list[T] | set[T])[int]

def _():
Expand Down
2 changes: 2 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -3208,6 +3208,8 @@ S = TypeVar("S")
class Bar(Protocol[S]):
def x(self) -> "S | Bar[S]": ...

# error: [unbound-type-variable]
# error: [unbound-type-variable]
z: S | Bar[S]
```

Expand Down
31 changes: 31 additions & 0 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&INVALID_TYPE_VARIABLE_BOUND);
registry.register_lint(&INVALID_TYPE_VARIABLE_DEFAULT);
registry.register_lint(&UNBOUND_TYPE_VARIABLE);
registry.register_lint(&MISSING_ARGUMENT);
registry.register_lint(&NO_MATCHING_OVERLOAD);
registry.register_lint(&NOT_SUBSCRIPTABLE);
Expand Down Expand Up @@ -1808,6 +1809,36 @@ declare_lint! {
}
}

declare_lint! {
/// ## What it does
/// Checks for type variables that are used in a scope where they are not bound
/// to any enclosing generic context.
///
/// ## Why is this bad?
/// Using a type variable outside of a scope that binds it has no well-defined meaning.
///
/// ## Examples
/// ```python
/// from typing import TypeVar, Generic
///
/// T = TypeVar("T")
/// S = TypeVar("S")
///
/// x: T # error: unbound type variable in module scope
///
/// class C(Generic[T]):
/// x: list[S] = [] # error: S is not in this class's generic context
/// ```
///
/// ## References
/// - [Typing spec: Scoping rules for type variables](https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables)
pub(crate) static UNBOUND_TYPE_VARIABLE = {
summary: "detects type variables used outside of their bound scope",
status: LintStatus::stable("0.0.20"),
default_level: Level::Error,
}
}

declare_lint! {
/// ## What it does
/// Checks for missing required arguments in a call.
Expand Down
26 changes: 19 additions & 7 deletions crates/ty_python_semantic/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,25 @@ pub(crate) fn bind_typevar<'db>(
return Some(typevar.with_binding_context(db, definition));
}
}
enclosing_generic_contexts(db, index, containing_scope)
.find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar))
.or_else(|| {
typevar_binding_context.map(|typevar_binding_context| {
typevar.with_binding_context(db, typevar_binding_context)
})
})
// Walk ancestor scopes, tracking whether we've crossed a class scope boundary.
// Class-scoped type variables are not visible from inner class scopes.
let mut crossed_class_scope = false;
for (_, ancestor_scope) in index.ancestor_scopes(containing_scope) {
let is_class_scope = ancestor_scope.kind().is_class();
// If we've already crossed a class boundary, skip class-scoped generic contexts.
// This prevents inner classes from accessing type parameters of outer classes.
if (!is_class_scope || !crossed_class_scope)
&& let Some(generic_context) = ancestor_scope.node().generic_context(db, index)
&& let Some(bound) = generic_context.binds_typevar(db, typevar)
{
return Some(bound);
}
if is_class_scope {
crossed_class_scope = true;
}
}
typevar_binding_context
.map(|typevar_binding_context| typevar.with_binding_context(db, typevar_binding_context))
}

/// Create a `typing.Self` type variable for a given class.
Expand Down
10 changes: 10 additions & 0 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,12 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> {
/// Whether we are in a context that binds unbound typevars.
typevar_binding_context: Option<Definition<'db>>,

/// Whether to check for unbound type variables in type expressions.
/// This is set to `true` when processing annotation expressions, where unbound type variables
/// are an error. It is `false` in other contexts (e.g., `TypeVar` defaults, explicit class
/// specialization) where unbound type variables are expected.
check_unbound_typevars: bool,

/// The deferred state of inferring types of certain expressions within the region.
///
/// This is different from [`InferenceRegion::Deferred`] which works on the entire definition
Expand Down Expand Up @@ -363,6 +369,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
bindings: VecMap::default(),
declarations: VecMap::default(),
typevar_binding_context: None,
check_unbound_typevars: false,
deferred: VecSet::default(),
undecorated_type: None,
cycle_recovery: None,
Expand Down Expand Up @@ -14603,6 +14610,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {

// builder only state
typevar_binding_context: _,
check_unbound_typevars: _,
deferred_state: _,
multi_inference_state: _,
inner_expression_inference_state: _,
Expand Down Expand Up @@ -14672,6 +14680,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
dataclass_field_specifiers: _,
all_definitely_bound: _,
typevar_binding_context: _,
check_unbound_typevars: _,
deferred_state: _,
inferring_vararg_annotation: _,
multi_inference_state: _,
Expand Down Expand Up @@ -14754,6 +14763,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
dataclass_field_specifiers: _,
all_definitely_bound: _,
typevar_binding_context: _,
check_unbound_typevars: _,
deferred_state: _,
multi_inference_state: _,
inner_expression_inference_state: _,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
};

let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state);
let previous_check_unbound_typevars =
std::mem::replace(&mut self.check_unbound_typevars, true);
let annotation_ty = self.infer_annotation_expression_impl(annotation, pep_613_policy);
self.check_unbound_typevars = previous_check_unbound_typevars;
self.deferred_state = previous_deferred_state;
annotation_ty
}
Expand Down Expand Up @@ -134,21 +137,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
};

special_case.unwrap_or_else(|| {
TypeAndQualifiers::declared(
ty.default_specialize(builder.db())
.in_type_expression(
builder.db(),
builder.scope(),
builder.typevar_binding_context,
let result_ty = ty
.default_specialize(builder.db())
.in_type_expression(
builder.db(),
builder.scope(),
builder.typevar_binding_context,
)
.unwrap_or_else(|error| {
error.into_fallback_type(
&builder.context,
annotation,
builder.is_reachable(annotation),
)
.unwrap_or_else(|error| {
error.into_fallback_type(
&builder.context,
annotation,
builder.is_reachable(annotation),
)
}),
)
});
let result_ty = builder.check_for_unbound_type_variable(annotation, result_ty);
TypeAndQualifiers::declared(result_ty)
})
}

Expand Down
Loading
Loading