Skip to content
Closed
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
241 changes: 135 additions & 106 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 @@ -579,6 +579,7 @@ def f(x: int):
# error: [invalid-super-argument] "`int` is not a valid class"
super(x, x)

# error: [invalid-type-alias] "`type` statements are not allowed in function scopes"
type IntAlias = int
# error: [invalid-super-argument] "`TypeAliasType` is not a valid class"
super(IntAlias, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,13 @@ python-version = "3.12"

```py
def _():
# error: [invalid-type-alias] "`type` statements are not allowed in function scopes"
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
# error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound"
type X[T: (yield 1)] = int

def _():
# error: [invalid-type-alias] "`type` statements are not allowed in function scopes"
# error: [invalid-type-form] "`yield` expressions are not allowed in type alias values"
# error: [invalid-syntax] "yield expression cannot be used within a type alias"
type Y = (yield 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,9 @@ def _(g: G):
Unless a type default was provided:

```py
type G[T = int] = list[T]
type GWithDefault[T = int] = list[T]

def _(g: G):
def _(g: GWithDefault):
reveal_type(g) # revealed: list[int]
```

Expand All @@ -268,9 +268,9 @@ A self-referential default that does not reference itself in the alias body shou
even when the default is evaluated (e.g., by omitting the type argument):

```py
type B[T = B] = list[T]
type SelfDefaultB[T = SelfDefaultB] = list[T]

def _(x: B) -> None:
def _(x: SelfDefaultB) -> None:
pass
```

Expand Down Expand Up @@ -427,12 +427,12 @@ reveal_type(get_value(d, "a")) # revealed: int
It also works in the reverse direction, where the type alias is used as the argument type:

```py
type MyList[T] = list[T]
type MyListAlias[T] = list[T]

def head[T](l: list[T]) -> T:
return l[0]

def _(x: MyList[int]):
def _(x: MyListAlias[int]):
reveal_type(head(x)) # revealed: int
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,45 @@ class Foo:
But narrowing of names used in the type alias is still respected:

```py
def _(flag: bool):
t = int if flag else None
if t is not None:
type Alias = t | str
def f(x: Alias):
reveal_type(x) # revealed: int | str
flag = True
t = int if flag else None
if t is not None:
type NarrowedAlias = t | str

def f(x: NarrowedAlias):
reveal_type(x) # revealed: int | str
```

`type` statements are only allowed in module and class scopes:

```py
class C:
type Alias = int

def _():
# error: [invalid-type-alias] "`type` statements are not allowed in function scopes"
type Local = int
```

## Redeclared aliases

<!-- snapshot-diagnostics -->

```py
type Redeclared = int
# error: [invalid-type-alias] "Type alias `Redeclared` is already defined in this scope"
type Redeclared = str
```

```py
from random import random

flag = random() > 0.5
if flag:
type BranchRedeclared = int
else:
# error: [invalid-type-alias] "Type alias `BranchRedeclared` is already defined in this scope"
type BranchRedeclared = str
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I was a bit surprised by this, but other type checkers do flag it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't like flagging this -- does the conformance suite require it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we do flag this, we also need to ensure we don't flag it if one of the branches is statically unreachable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with @carljm, but yes -- unfortunately the conformance suite does require it currently: https://github.com/python/typing/blob/1df1565c69730d88ce6877009d268ba1d602af1e/conformance/tests/aliases_type_statement.py#L52. Not sure if that's actually following the spec or if the conformance suite is going beyond the spec there

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I can't find any such requirement in PEP 695 or in the spec. I think we should PR the conformance suite to not require this error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I also don't see the rule against defining type aliases in function bodies specified anywhere, and that also seems sorta unnecessary... it's not totally clear to me why users should be prohibited from doing something like this, which seems fine: https://play.ty.dev/f918c85d-8bf3-440c-a5bf-1188cd72d053

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just put up python/typing#2249. If that goes through, I think we can close this PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To confirm, I'm holding off on reviewing the code on the assumption that we're about to close the PR. If that changes I'll take a look!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's correct. Given the approvals on that PR, I'm just going to close this out!

```

## Generic type aliases
Expand Down
4 changes: 2 additions & 2 deletions crates/ty_python_semantic/resources/mdtest/promotion.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,9 +511,9 @@ def _(flag: bool):
reveal_type(promotable4 or unpromotable4) # revealed: Literal[True]
reveal_type([promotable4 or unpromotable4]) # revealed: list[Literal[True]]

type X = Literal[b"bar"]
type XBytes = Literal[b"bar"]

def _(x1: X | None, x2: X):
def _(x1: XBytes | None, x2: XBytes):
reveal_type([x1, x2]) # revealed: list[Literal[b"bar"] | None]
reveal_type([x1 or x2]) # revealed: list[Literal[b"bar"]]
```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: pep695_type_aliases.md - PEP 695 type aliases - Redeclared aliases
mdtest path: crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md
---

# Python source files

## mdtest_snippet.py

```
1 | type Redeclared = int
2 | # error: [invalid-type-alias] "Type alias `Redeclared` is already defined in this scope"
3 | type Redeclared = str
4 | from random import random
5 |
6 | flag = random() > 0.5
7 | if flag:
8 | type BranchRedeclared = int
9 | else:
10 | # error: [invalid-type-alias] "Type alias `BranchRedeclared` is already defined in this scope"
11 | type BranchRedeclared = str
```

# Diagnostics

```
error[invalid-type-alias]: Type alias `Redeclared` is already defined in this scope
--> src/mdtest_snippet.py:1:6
|
1 | type Redeclared = int
| ---------- `Redeclared` previously defined here
2 | # error: [invalid-type-alias] "Type alias `Redeclared` is already defined in this scope"
3 | type Redeclared = str
| ^^^^^^^^^^
4 | from random import random
|

```

```
error[invalid-type-alias]: Type alias `BranchRedeclared` is already defined in this scope
--> src/mdtest_snippet.py:8:10
|
6 | flag = random() > 0.5
7 | if flag:
8 | type BranchRedeclared = int
| ---------------- `BranchRedeclared` previously defined here
9 | else:
10 | # error: [invalid-type-alias] "Type alias `BranchRedeclared` is already defined in this scope"
11 | type BranchRedeclared = str
| ^^^^^^^^^^^^^^^^
|

```
7 changes: 7 additions & 0 deletions crates/ty_python_semantic/src/semantic_index/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1920,6 +1920,13 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
.map(|name| name.id.clone())
.unwrap_or("<unknown>".into()),
);

if type_alias.name.as_name_expr().is_some() {
let use_id = self.current_ast_ids().record_use(&*type_alias.name);
self.current_use_def_map_mut()
.record_use(symbol.into(), use_id);
}

self.add_definition(symbol.into(), type_alias);
self.visit_expr(&type_alias.name);

Expand Down
24 changes: 24 additions & 0 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_GENERIC_CLASS);
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
registry.register_lint(&INVALID_PARAMSPEC);
registry.register_lint(&INVALID_TYPE_ALIAS);
registry.register_lint(&INVALID_TYPE_ALIAS_TYPE);
registry.register_lint(&INVALID_NEWTYPE);
registry.register_lint(&INVALID_METACLASS);
Expand Down Expand Up @@ -1387,6 +1388,29 @@ declare_lint! {
}
}

declare_lint! {
/// ## What it does
/// Checks for invalid PEP 695 `type` statements.
///
/// ## Why is this bad?
/// A `type` statement is only valid in module and class scopes, and a type alias name should
/// not be redeclared in the same scope.
///
/// ## Examples
/// ```python
/// type Alias = int
/// type Alias = str # error: type alias already defined
///
/// def f():
/// type Local = int # error: type statements are not allowed in function scopes
/// ```
pub(crate) static INVALID_TYPE_ALIAS = {
summary: "detects invalid PEP 695 `type` statements",
status: LintStatus::stable("0.0.28"),
default_level: Level::Error,
}
}

declare_lint! {
/// ## What it does
/// Checks for the creation of invalid `TypeAliasType`s
Expand Down
103 changes: 91 additions & 12 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,19 @@ use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CYCLIC_TYPE_ALIAS_DEFINITION,
GeneratorMismatchKind, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT,
INVALID_ATTRIBUTE_ACCESS, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION,
INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE,
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_BOUND,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE,
UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, hint_if_stdlib_attribute_exists_on_other_versions,
report_attempted_protocol_instantiation, report_bad_dunder_set_call,
report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_class_match_pattern, report_invalid_exception_caught,
report_invalid_exception_cause, report_invalid_exception_raised,
report_invalid_exception_tuple_caught, report_invalid_generator_yield_type,
report_invalid_key_on_typed_dict, report_invalid_type_checking_constant,
INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS,
INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_MISSING_IMPLICIT_CALL,
POSSIBLY_MISSING_SUBMODULE, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE,
hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_call_to_abstract_method,
report_cannot_pop_required_field_on_typed_dict, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_class_match_pattern,
report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_exception_tuple_caught,
report_invalid_generator_yield_type, report_invalid_key_on_typed_dict,
report_invalid_type_checking_constant,
report_match_pattern_against_non_runtime_checkable_protocol,
report_match_pattern_against_typed_dict, report_possibly_missing_attribute,
report_possibly_unresolved_reference, report_unsupported_augmented_assignment,
Expand Down Expand Up @@ -1455,6 +1456,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
type_alias: &ast::StmtTypeAlias,
definition: Definition<'db>,
) {
self.report_invalid_type_alias_scope(type_alias, definition);
self.report_redeclared_type_alias(type_alias, definition);

self.infer_expression(&type_alias.name, TypeContext::default());

// Check that no type parameter with a default follows a TypeVarTuple
Expand Down Expand Up @@ -1487,6 +1491,81 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
}

fn report_invalid_type_alias_scope(
&mut self,
type_alias: &ast::StmtTypeAlias,
definition: Definition<'db>,
) {
let db = self.db();
if !definition
.scope(db)
.scope(db)
.kind()
.is_non_lambda_function()
{
return;
}

if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ALIAS, type_alias) {
builder.into_diagnostic("`type` statements are not allowed in function scopes");
}
}

fn report_redeclared_type_alias(
&mut self,
type_alias: &ast::StmtTypeAlias,
definition: Definition<'db>,
) {
let Some(type_alias_name) = type_alias.name.as_name_expr() else {
return;
};

let db = self.db();
let scope = definition.scope(db);
let use_def = self.index.use_def_map(scope.file_scope_id(db));

// Type alias redeclarations are a scope-level property, so we need all earlier
// definitions for this symbol in the scope, not just the bindings reachable here.
let Some(previous_definition) = use_def
.all_definitions_with_usage()
.filter_map(|(_, state, _)| state.definition())
.filter(|previous_definition| previous_definition.place(db) == definition.place(db))
.filter(|previous_definition| {
matches!(
previous_definition.kind(db),
DefinitionKind::TypeAlias(previous_type_alias)
if previous_type_alias
.node(self.module())
.node_index()
.load()
< type_alias.node_index().load()
)
})
.max_by_key(|definition| definition.focus_range(db, self.module()).start())
else {
return;
};

let Some(builder) = self
.context
.report_lint(&INVALID_TYPE_ALIAS, &*type_alias.name)
else {
return;
};

let mut diagnostic = builder.into_diagnostic(format_args!(
"Type alias `{}` is already defined in this scope",
type_alias_name.id
));
diagnostic.annotate(
Annotation::secondary(previous_definition.focus_range(db, self.module()).into())
.message(format_args!(
"`{}` previously defined here",
type_alias_name.id
)),
);
}

fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) {
let ast::StmtIf {
range: _,
Expand Down
10 changes: 10 additions & 0 deletions ty.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading