Skip to content

Commit ff207d0

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

File tree

6 files changed

+281
-117
lines changed

6 files changed

+281
-117
lines changed

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/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/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)