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
130 changes: 78 additions & 52 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions crates/ty_ide/src/goto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,40 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}

#[test]
fn goto_type_of_bare_type_alias_type() {
let test = cursor_test(
r#"
from typing_extensions import TypeAliasType

Alias = TypeAliasType("Alias", tuple[int, int])

Alias<CURSOR>
"#,
);

assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:4:13
|
2 | from typing_extensions import TypeAliasType
3 |
4 | Alias = TypeAliasType("Alias", tuple[int, int])
| ^^^^^
5 |
6 | Alias
|
info: Source
--> main.py:6:13
|
4 | Alias = TypeAliasType("Alias", tuple[int, int])
5 |
6 | Alias
| ^^^^^
|
"#);
}

#[test]
fn goto_type_on_keyword_argument() {
let test = cursor_test(
Expand Down
6 changes: 5 additions & 1 deletion crates/ty_project/tests/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,8 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {

/// Whether or not the .py/.pyi version of this file is expected to fail
#[rustfmt::skip]
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[];
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
// Fails with too-many-cycle-iterations due to a self-referential
// type alias, see https://github.com/astral-sh/ty/issues/256
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_34.py", true, true),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unfortunate, but that file could just as easily have been written using PEP 695 type aliases …

];
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,54 @@ type TypeAliasType2 = TypeOf[Alias2]
static_assert(not is_equivalent_to(TypeAliasType1, TypeAliasType2))
static_assert(is_disjoint_from(TypeAliasType1, TypeAliasType2))
```

## Direct use of `TypeAliasType`

`TypeAliasType` can also be used directly. This is useful for versions of Python prior to 3.12.

```toml
[environment]
python-version = "3.9"
```

### Basic example

```py
from typing_extensions import TypeAliasType, Union

IntOrStr = TypeAliasType("IntOrStr", Union[int, str])

reveal_type(IntOrStr) # revealed: typing.TypeAliasType

reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]

def f(x: IntOrStr) -> None:
reveal_type(x) # revealed: int | str
```

### Generic example

```py
from typing_extensions import TypeAliasType, TypeVar

T = TypeVar("T")

IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,))

def f(x: IntAnd[str]) -> None:
reveal_type(x) # revealed: @Todo(Generic PEP-695 type alias)
```

### Error cases

#### Name is not a string literal

```py
from typing_extensions import TypeAliasType

def get_name() -> str:
return "IntOrStr"

# error: [invalid-type-alias-type] "The name of a `typing.TypeAlias` must be a string literal"
IntOrStr = TypeAliasType(get_name(), int | str)
```
86 changes: 83 additions & 3 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4012,6 +4012,45 @@ impl<'db> Type<'db> {
Signatures::single(signature)
}

Some(KnownClass::TypeAliasType) => {
// ```py
// def __new__(
// cls,
// name: str,
// value: Any,
// *,
// type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = ()
// ) -> Self: ...
// ```
let signature = CallableSignature::single(
self,
Signature::new(
Parameters::new([
Parameter::positional_or_keyword(Name::new_static("name"))
.with_annotated_type(KnownClass::Str.to_instance(db)),
Parameter::positional_or_keyword(Name::new_static("value"))
.with_annotated_type(Type::any())
.type_form(),
Parameter::keyword_only(Name::new_static("type_params"))
.with_annotated_type(KnownClass::Tuple.to_specialized_instance(
db,
[UnionType::from_elements(
db,
[
KnownClass::TypeVar.to_instance(db),
KnownClass::ParamSpec.to_instance(db),
KnownClass::TypeVarTuple.to_instance(db),
],
)],
))
.with_default_type(TupleType::empty(db)),
]),
None,
),
);
Signatures::single(signature)
}

Some(KnownClass::Property) => {
let getter_signature = Signature::new(
Parameters::new([
Expand Down Expand Up @@ -5318,7 +5357,7 @@ impl<'db> Type<'db> {
Some(TypeDefinition::TypeVar(var.definition(db)))
}
KnownInstanceType::TypeAliasType(type_alias) => {
Some(TypeDefinition::TypeAlias(type_alias.definition(db)))
type_alias.definition(db).map(TypeDefinition::TypeAlias)
}
_ => None,
},
Expand Down Expand Up @@ -7467,15 +7506,15 @@ impl<'db> ModuleLiteralType<'db> {
}

#[salsa::interned(debug)]
pub struct TypeAliasType<'db> {
pub struct PEP695TypeAliasType<'db> {
#[returns(ref)]
pub name: ast::name::Name,

rhs_scope: ScopeId<'db>,
}

#[salsa::tracked]
impl<'db> TypeAliasType<'db> {
impl<'db> PEP695TypeAliasType<'db> {
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
let scope = self.rhs_scope(db);
let type_alias_stmt_node = scope.node(db).expect_type_alias();
Expand All @@ -7492,6 +7531,43 @@ impl<'db> TypeAliasType<'db> {
}
}

#[salsa::interned(debug)]
pub struct BareTypeAliasType<'db> {
#[returns(ref)]
pub name: ast::name::Name,
pub definition: Option<Definition<'db>>,
pub value: Type<'db>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update)]
pub enum TypeAliasType<'db> {
PEP695(PEP695TypeAliasType<'db>),
Bare(BareTypeAliasType<'db>),
}

impl<'db> TypeAliasType<'db> {
pub(crate) fn name(self, db: &'db dyn Db) -> &'db str {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.name(db),
TypeAliasType::Bare(type_alias) => type_alias.name(db).as_str(),
}
}

pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
match self {
TypeAliasType::PEP695(type_alias) => Some(type_alias.definition(db)),
TypeAliasType::Bare(type_alias) => type_alias.definition(db),
}
}

pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> {
match self {
TypeAliasType::PEP695(type_alias) => type_alias.value_type(db),
TypeAliasType::Bare(type_alias) => type_alias.value(db),
}
}
}

/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(super) struct MetaclassCandidate<'db> {
Expand Down Expand Up @@ -8004,6 +8080,10 @@ impl<'db> TupleType<'db> {
.to_specialized_instance(db, [UnionType::from_elements(db, self.elements(db))])
}

pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> {
Type::Tuple(TupleType::new(db, Box::<[Type<'db>]>::from([])))
}

pub(crate) fn from_elements<T: Into<Type<'db>>>(
db: &'db dyn Db,
types: impl IntoIterator<Item = T>,
Expand Down
22 changes: 22 additions & 0 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_GENERIC_CLASS);
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
registry.register_lint(&INVALID_TYPE_ALIAS_TYPE);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_OVERLOAD);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
Expand Down Expand Up @@ -591,6 +592,27 @@ declare_lint! {
}
}

declare_lint! {
/// ## What it does
/// Checks for the creation of invalid `TypeAliasType`s
///
/// ## Why is this bad?
/// There are several requirements that you must follow when creating a `TypeAliasType`.
///
/// ## Examples
/// ```python
/// from typing import TypeAliasType
///
/// IntOrStr = TypeAliasType("IntOrStr", int | str) # okay
/// NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if we should have the same strict requirements here as for legacy TypeVar constructions. I couldn't find anything in the spec, but pyright also emits diagnostics, if a TypeAliasType is not immediately assigned to a variable, for example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me!

/// ```
pub(crate) static INVALID_TYPE_ALIAS_TYPE = {
summary: "detects invalid TypeAliasType definitions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}

declare_lint! {
/// ## What it does
/// Checks for arguments to `metaclass=` that are invalid.
Expand Down
82 changes: 64 additions & 18 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,26 @@ use crate::types::diagnostic::{
CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, INCONSISTENT_MRO,
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS,
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR, report_implicit_return_type,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_generator_function_return_type, report_invalid_return_type,
report_possibly_unbound_attribute,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
report_implicit_return_type, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
report_invalid_return_type, report_possibly_unbound_attribute,
};
use crate::types::generics::GenericContext;
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
CallDunderError, CallableSignature, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, FunctionDecorators, FunctionType, GenericAlias, IntersectionBuilder,
IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MemberLookupPolicy,
MetaclassCandidate, Parameter, ParameterForm, Parameters, Signature, Signatures,
StringLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type,
TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints,
TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type,
todo_type,
BareTypeAliasType, CallDunderError, CallableSignature, CallableType, ClassLiteral, ClassType,
DataclassParams, DynamicType, FunctionDecorators, FunctionType, GenericAlias,
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm,
Parameters, Signature, Signatures, StringLiteralType, SubclassOfType, Symbol,
SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
};
use crate::unpack::{Unpack, UnpackPosition};
use crate::util::subscript::{PyIndex, PySlice};
Expand Down Expand Up @@ -2374,12 +2374,13 @@ impl<'db> TypeInferenceBuilder<'db> {
.node_scope(NodeWithScopeRef::TypeAlias(type_alias))
.to_scope_id(self.db(), self.file());

let type_alias_ty =
Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::new(
let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType(
TypeAliasType::PEP695(PEP695TypeAliasType::new(
self.db(),
&type_alias.name.as_name_expr().unwrap().id,
rhs_scope,
)));
)),
));

self.add_declaration_with_binding(
type_alias.into(),
Expand Down Expand Up @@ -4860,6 +4861,7 @@ impl<'db> TypeInferenceBuilder<'db> {
| KnownClass::Super
| KnownClass::TypeVar
| KnownClass::NamedTuple
| KnownClass::TypeAliasType
)
)
// temporary special-casing for all subclasses of `enum.Enum`
Expand Down Expand Up @@ -5363,6 +5365,50 @@ impl<'db> TypeInferenceBuilder<'db> {
));
}

KnownClass::TypeAliasType => {
let assigned_to = (self.index)
.try_expression(call_expression_node)
.and_then(|expr| expr.assigned_to(self.db()));

let containing_assignment =
assigned_to.as_ref().and_then(|assigned_to| {
match assigned_to.node().targets.as_slice() {
[ast::Expr::Name(target)] => Some(
self.index.expect_single_definition(target),
),
_ => None,
}
});

let [Some(name), Some(value), ..] =
overload.parameter_types()
else {
continue;
};

if let Some(name) = name.into_string_literal() {
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::TypeAliasType(
TypeAliasType::Bare(BareTypeAliasType::new(
self.db(),
ast::name::Name::new(name.value(self.db())),
containing_assignment,
value,
)),
),
));
} else {
if let Some(builder) = self.context.report_lint(
&INVALID_TYPE_ALIAS_TYPE,
call_expression,
) {
builder.into_diagnostic(format_args!(
"The name of a `typing.TypeAlias` must be a string literal",
));
}
}
}

_ => (),
}
}
Expand Down
Loading
Loading