Skip to content

Commit 9f29ce9

Browse files
committed
[ty] Validate PEP 695 type alias scope and redeclaration rules
1 parent 39c3636 commit 9f29ce9

10 files changed

Lines changed: 292 additions & 125 deletions

File tree

crates/ty/docs/rules.md

Lines changed: 134 additions & 105 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_python_semantic/resources/mdtest/class/super.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ def f(x: int):
579579
# error: [invalid-super-argument] "`int` is not a valid class"
580580
super(x, x)
581581

582+
# error: [invalid-type-alias] "`type` statements are not allowed in function scopes"
582583
type IntAlias = int
583584
# error: [invalid-super-argument] "`TypeAliasType` is not a valid class"
584585
super(IntAlias, 0)

crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,13 @@ python-version = "3.12"
321321

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

328329
def _():
330+
# error: [invalid-type-alias] "`type` statements are not allowed in function scopes"
329331
# error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
330332
# error: [invalid-syntax] "yield expression cannot be used within a type alias"
331333
type Y = (yield 1)

crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,9 @@ def _(g: G):
251251
Unless a type default was provided:
252252

253253
```py
254-
type G[T = int] = list[T]
254+
type GWithDefault[T = int] = list[T]
255255

256-
def _(g: G):
256+
def _(g: GWithDefault):
257257
reveal_type(g) # revealed: list[int]
258258
```
259259

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

270270
```py
271-
type B[T = B] = list[T]
271+
type SelfDefaultB[T = SelfDefaultB] = list[T]
272272

273-
def _(x: B) -> None:
273+
def _(x: SelfDefaultB) -> None:
274274
pass
275275
```
276276

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

429429
```py
430-
type MyList[T] = list[T]
430+
type MyListAlias[T] = list[T]
431431

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

435-
def _(x: MyList[int]):
435+
def _(x: MyListAlias[int]):
436436
reveal_type(head(x)) # revealed: int
437437
```
438438

crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,29 @@ class Foo:
123123
But narrowing of names used in the type alias is still respected:
124124

125125
```py
126-
def _(flag: bool):
127-
t = int if flag else None
128-
if t is not None:
129-
type Alias = t | str
130-
def f(x: Alias):
131-
reveal_type(x) # revealed: int | str
126+
flag = True
127+
t = int if flag else None
128+
if t is not None:
129+
type NarrowedAlias = t | str
130+
131+
def f(x: NarrowedAlias):
132+
reveal_type(x) # revealed: int | str
133+
```
134+
135+
`type` statements are only allowed in module and class scopes, and a type alias cannot be redeclared
136+
in the same scope:
137+
138+
```py
139+
class C:
140+
type Alias = int
141+
142+
def _():
143+
# error: [invalid-type-alias] "`type` statements are not allowed in function scopes"
144+
type Local = int
145+
146+
type Redeclared = int
147+
# error: [invalid-type-alias] "Type alias `Redeclared` is already defined in this scope"
148+
type Redeclared = str
132149
```
133150

134151
## Generic type aliases

crates/ty_python_semantic/resources/mdtest/promotion.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,9 +511,9 @@ def _(flag: bool):
511511
reveal_type(promotable4 or unpromotable4) # revealed: Literal[True]
512512
reveal_type([promotable4 or unpromotable4]) # revealed: list[Literal[True]]
513513

514-
type X = Literal[b"bar"]
514+
type XBytes = Literal[b"bar"]
515515

516-
def _(x1: X | None, x2: X):
516+
def _(x1: XBytes | None, x2: XBytes):
517517
reveal_type([x1, x2]) # revealed: list[Literal[b"bar"] | None]
518518
reveal_type([x1 or x2]) # revealed: list[Literal[b"bar"]]
519519
```

crates/ty_python_semantic/src/semantic_index/builder.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,6 +1918,13 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
19181918
.map(|name| name.id.clone())
19191919
.unwrap_or("<unknown>".into()),
19201920
);
1921+
1922+
if type_alias.name.as_name_expr().is_some() {
1923+
let use_id = self.current_ast_ids().record_use(&*type_alias.name);
1924+
self.current_use_def_map_mut()
1925+
.record_use(symbol.into(), use_id);
1926+
}
1927+
19211928
self.add_definition(symbol.into(), type_alias);
19221929
self.visit_expr(&type_alias.name);
19231930

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
9090
registry.register_lint(&INVALID_GENERIC_CLASS);
9191
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
9292
registry.register_lint(&INVALID_PARAMSPEC);
93+
registry.register_lint(&INVALID_TYPE_ALIAS);
9394
registry.register_lint(&INVALID_TYPE_ALIAS_TYPE);
9495
registry.register_lint(&INVALID_NEWTYPE);
9596
registry.register_lint(&INVALID_METACLASS);
@@ -1389,6 +1390,29 @@ declare_lint! {
13891390
}
13901391
}
13911392

1393+
declare_lint! {
1394+
/// ## What it does
1395+
/// Checks for invalid PEP 695 `type` statements.
1396+
///
1397+
/// ## Why is this bad?
1398+
/// A `type` statement is only valid in module and class scopes, and a type alias name should
1399+
/// not be redeclared in the same scope.
1400+
///
1401+
/// ## Examples
1402+
/// ```python
1403+
/// type Alias = int
1404+
/// type Alias = str # error: type alias already defined
1405+
///
1406+
/// def f():
1407+
/// type Local = int # error: type statements are not allowed in function scopes
1408+
/// ```
1409+
pub(crate) static INVALID_TYPE_ALIAS = {
1410+
summary: "detects invalid PEP 695 `type` statements",
1411+
status: LintStatus::stable("0.0.28"),
1412+
default_level: Level::Error,
1413+
}
1414+
}
1415+
13921416
declare_lint! {
13931417
/// ## What it does
13941418
/// Checks for the creation of invalid `TypeAliasType`s

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ use crate::types::diagnostic::{
6868
CYCLIC_TYPE_ALIAS_DEFINITION, DUPLICATE_BASE, GeneratorMismatchKind, INCONSISTENT_MRO,
6969
INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS,
7070
INVALID_BASE, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION,
71-
INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE,
72-
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_BOUND,
73-
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NO_MATCHING_OVERLOAD,
74-
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE, SUBCLASS_OF_FINAL_CLASS,
75-
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE,
76-
UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE,
71+
INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS,
72+
INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
73+
INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
74+
NO_MATCHING_OVERLOAD, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE,
75+
SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
76+
UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE,
7777
hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation,
7878
report_bad_dunder_set_call, report_call_to_abstract_method,
7979
report_cannot_pop_required_field_on_typed_dict, report_conflicting_metaclass_from_bases,
@@ -1439,6 +1439,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
14391439
type_alias: &ast::StmtTypeAlias,
14401440
definition: Definition<'db>,
14411441
) {
1442+
self.report_invalid_type_alias_scope(type_alias, definition);
1443+
self.report_redeclared_type_alias(type_alias, definition);
1444+
14421445
self.infer_expression(&type_alias.name, TypeContext::default());
14431446

14441447
// Check that no type parameter with a default follows a TypeVarTuple
@@ -1471,6 +1474,80 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
14711474
);
14721475
}
14731476

1477+
fn report_invalid_type_alias_scope(
1478+
&mut self,
1479+
type_alias: &ast::StmtTypeAlias,
1480+
definition: Definition<'db>,
1481+
) {
1482+
let db = self.db();
1483+
if !definition
1484+
.scope(db)
1485+
.scope(db)
1486+
.kind()
1487+
.is_non_lambda_function()
1488+
{
1489+
return;
1490+
}
1491+
1492+
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ALIAS, type_alias) {
1493+
builder.into_diagnostic("`type` statements are not allowed in function scopes");
1494+
}
1495+
}
1496+
1497+
fn report_redeclared_type_alias(
1498+
&mut self,
1499+
type_alias: &ast::StmtTypeAlias,
1500+
definition: Definition<'db>,
1501+
) {
1502+
let Some(type_alias_name) = type_alias.name.as_name_expr() else {
1503+
return;
1504+
};
1505+
1506+
let db = self.db();
1507+
let scope = definition.scope(db);
1508+
let use_def = self.index.use_def_map(scope.file_scope_id(db));
1509+
let use_id = type_alias_name.scoped_use_id(db, scope);
1510+
1511+
let Some(previous_definition) = use_def
1512+
.bindings_at_use(use_id)
1513+
.filter_map(|binding| binding.binding.definition())
1514+
.filter(|definition| definition.scope(db) == scope)
1515+
.filter(|definition| {
1516+
matches!(
1517+
definition.kind(db),
1518+
DefinitionKind::TypeAlias(previous_type_alias)
1519+
if previous_type_alias
1520+
.node(self.module())
1521+
.node_index()
1522+
.load()
1523+
< type_alias.node_index().load()
1524+
)
1525+
})
1526+
.max_by_key(|definition| definition.focus_range(db, self.module()).start())
1527+
else {
1528+
return;
1529+
};
1530+
1531+
let Some(builder) = self
1532+
.context
1533+
.report_lint(&INVALID_TYPE_ALIAS, &*type_alias.name)
1534+
else {
1535+
return;
1536+
};
1537+
1538+
let mut diagnostic = builder.into_diagnostic(format_args!(
1539+
"Type alias `{}` is already defined in this scope",
1540+
type_alias_name.id
1541+
));
1542+
diagnostic.annotate(
1543+
Annotation::secondary(previous_definition.focus_range(db, self.module()).into())
1544+
.message(format_args!(
1545+
"`{}` previously defined here",
1546+
type_alias_name.id
1547+
)),
1548+
);
1549+
}
1550+
14741551
fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) {
14751552
let ast::StmtIf {
14761553
range: _,

ty.schema.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)