[syntax-error] Default type parameter followed by non-default type parameter (#21657)

## Summary

This PR implements syntax error where a default type parameter is
followed by a non-default type parameter.
https://github.com/astral-sh/ruff/issues/17412#issuecomment-3584088217


## Test Plan

I have written inline tests as directed in #17412

---------

Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
This commit is contained in:
Bhuminjay Soni 2025-12-03 12:01:31 +05:30 committed by GitHub
parent abaa49f552
commit f68080b55e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 359 additions and 5 deletions

View File

@ -747,6 +747,7 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. } | SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_) | SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
| SemanticSyntaxErrorKind::AnnotatedGlobal(_) | SemanticSyntaxErrorKind::AnnotatedGlobal(_)
| SemanticSyntaxErrorKind::TypeParameterDefaultOrder(_)
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => { | SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => {
self.semantic_errors.borrow_mut().push(error); self.semantic_errors.borrow_mut().push(error);
} }

View File

@ -0,0 +1,3 @@
class C[T = int, U]: ...
class C[T1, T2 = int, T3, T4]: ...
type Alias[T = int, U] = ...

View File

@ -144,11 +144,16 @@ impl SemanticSyntaxChecker {
} }
} }
} }
Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) Stmt::ClassDef(ast::StmtClassDef {
| Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => { type_params: Some(type_params),
if let Some(type_params) = type_params { ..
})
| Stmt::TypeAlias(ast::StmtTypeAlias {
type_params: Some(type_params),
..
}) => {
Self::duplicate_type_parameter_name(type_params, ctx); Self::duplicate_type_parameter_name(type_params, ctx);
} Self::type_parameter_default_order(type_params, ctx);
} }
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
if let [Expr::Starred(ast::ExprStarred { range, .. })] = targets.as_slice() { if let [Expr::Starred(ast::ExprStarred { range, .. })] = targets.as_slice() {
@ -611,6 +616,39 @@ impl SemanticSyntaxChecker {
} }
} }
fn type_parameter_default_order<Ctx: SemanticSyntaxContext>(
type_params: &ast::TypeParams,
ctx: &Ctx,
) {
let mut seen_default = false;
for type_param in type_params.iter() {
let has_default = match type_param {
ast::TypeParam::TypeVar(ast::TypeParamTypeVar { default, .. })
| ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { default, .. })
| ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { default, .. }) => {
default.is_some()
}
};
if seen_default && !has_default {
// test_err type_parameter_default_order
// class C[T = int, U]: ...
// class C[T1, T2 = int, T3, T4]: ...
// type Alias[T = int, U] = ...
Self::add_error(
ctx,
SemanticSyntaxErrorKind::TypeParameterDefaultOrder(
type_param.name().id.to_string(),
),
type_param.range(),
);
}
if has_default {
seen_default = true;
}
}
}
fn duplicate_parameter_name<Ctx: SemanticSyntaxContext>( fn duplicate_parameter_name<Ctx: SemanticSyntaxContext>(
parameters: &ast::Parameters, parameters: &ast::Parameters,
ctx: &Ctx, ctx: &Ctx,
@ -1066,6 +1104,12 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::DuplicateTypeParameter => { SemanticSyntaxErrorKind::DuplicateTypeParameter => {
f.write_str("duplicate type parameter") f.write_str("duplicate type parameter")
} }
SemanticSyntaxErrorKind::TypeParameterDefaultOrder(name) => {
write!(
f,
"non default type parameter `{name}` follows default type parameter"
)
}
SemanticSyntaxErrorKind::MultipleCaseAssignment(name) => { SemanticSyntaxErrorKind::MultipleCaseAssignment(name) => {
write!(f, "multiple assignments to name `{name}` in pattern") write!(f, "multiple assignments to name `{name}` in pattern")
} }
@ -1572,6 +1616,9 @@ pub enum SemanticSyntaxErrorKind {
/// Represents a nonlocal statement for a name that has no binding in an enclosing scope. /// Represents a nonlocal statement for a name that has no binding in an enclosing scope.
NonlocalWithoutBinding(String), NonlocalWithoutBinding(String),
/// Represents a default type parameter followed by a non-default type parameter.
TypeParameterDefaultOrder(String),
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]

View File

@ -375,3 +375,12 @@ Module(
4 | type X[**P = x := int] = int 4 | type X[**P = x := int] = int
5 | type X[**P = *int] = int 5 | type X[**P = *int] = int
| |
|
2 | type X[**P = yield x] = int
3 | type X[**P = yield from x] = int
4 | type X[**P = x := int] = int
| ^^^ Syntax Error: non default type parameter `int` follows default type parameter
5 | type X[**P = *int] = int
|

View File

@ -459,3 +459,12 @@ Module(
5 | type X[T = x := int] = int 5 | type X[T = x := int] = int
6 | type X[T: int = *int] = int 6 | type X[T: int = *int] = int
| |
|
3 | type X[T = (yield x)] = int
4 | type X[T = yield from x] = int
5 | type X[T = x := int] = int
| ^^^ Syntax Error: non default type parameter `int` follows default type parameter
6 | type X[T: int = *int] = int
|

View File

@ -384,3 +384,11 @@ Module(
| ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a TypeVarTuple default | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a TypeVarTuple default
5 | type X[*Ts = x := int] = int 5 | type X[*Ts = x := int] = int
| |
|
3 | type X[*Ts = yield x] = int
4 | type X[*Ts = yield from x] = int
5 | type X[*Ts = x := int] = int
| ^^^ Syntax Error: non default type parameter `int` follows default type parameter
|

View File

@ -0,0 +1,277 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/type_parameter_default_order.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..89,
body: [
ClassDef(
StmtClassDef {
node_index: NodeIndex(None),
range: 0..24,
decorator_list: [],
name: Identifier {
id: Name("C"),
range: 6..7,
node_index: NodeIndex(None),
},
type_params: Some(
TypeParams {
range: 7..19,
node_index: NodeIndex(None),
type_params: [
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 8..15,
name: Identifier {
id: Name("T"),
range: 8..9,
node_index: NodeIndex(None),
},
bound: None,
default: Some(
Name(
ExprName {
node_index: NodeIndex(None),
range: 12..15,
id: Name("int"),
ctx: Load,
},
),
),
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 17..18,
name: Identifier {
id: Name("U"),
range: 17..18,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
],
},
),
arguments: None,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 21..24,
value: EllipsisLiteral(
ExprEllipsisLiteral {
node_index: NodeIndex(None),
range: 21..24,
},
),
},
),
],
},
),
ClassDef(
StmtClassDef {
node_index: NodeIndex(None),
range: 25..59,
decorator_list: [],
name: Identifier {
id: Name("C"),
range: 31..32,
node_index: NodeIndex(None),
},
type_params: Some(
TypeParams {
range: 32..54,
node_index: NodeIndex(None),
type_params: [
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 33..35,
name: Identifier {
id: Name("T1"),
range: 33..35,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 37..45,
name: Identifier {
id: Name("T2"),
range: 37..39,
node_index: NodeIndex(None),
},
bound: None,
default: Some(
Name(
ExprName {
node_index: NodeIndex(None),
range: 42..45,
id: Name("int"),
ctx: Load,
},
),
),
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 47..49,
name: Identifier {
id: Name("T3"),
range: 47..49,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 51..53,
name: Identifier {
id: Name("T4"),
range: 51..53,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
],
},
),
arguments: None,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 56..59,
value: EllipsisLiteral(
ExprEllipsisLiteral {
node_index: NodeIndex(None),
range: 56..59,
},
),
},
),
],
},
),
TypeAlias(
StmtTypeAlias {
node_index: NodeIndex(None),
range: 60..88,
name: Name(
ExprName {
node_index: NodeIndex(None),
range: 65..70,
id: Name("Alias"),
ctx: Store,
},
),
type_params: Some(
TypeParams {
range: 70..82,
node_index: NodeIndex(None),
type_params: [
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 71..78,
name: Identifier {
id: Name("T"),
range: 71..72,
node_index: NodeIndex(None),
},
bound: None,
default: Some(
Name(
ExprName {
node_index: NodeIndex(None),
range: 75..78,
id: Name("int"),
ctx: Load,
},
),
),
},
),
TypeVar(
TypeParamTypeVar {
node_index: NodeIndex(None),
range: 80..81,
name: Identifier {
id: Name("U"),
range: 80..81,
node_index: NodeIndex(None),
},
bound: None,
default: None,
},
),
],
},
),
value: EllipsisLiteral(
ExprEllipsisLiteral {
node_index: NodeIndex(None),
range: 85..88,
},
),
},
),
],
},
)
```
## Semantic Syntax Errors
|
1 | class C[T = int, U]: ...
| ^ Syntax Error: non default type parameter `U` follows default type parameter
2 | class C[T1, T2 = int, T3, T4]: ...
3 | type Alias[T = int, U] = ...
|
|
1 | class C[T = int, U]: ...
2 | class C[T1, T2 = int, T3, T4]: ...
| ^^ Syntax Error: non default type parameter `T3` follows default type parameter
3 | type Alias[T = int, U] = ...
|
|
1 | class C[T = int, U]: ...
2 | class C[T1, T2 = int, T3, T4]: ...
| ^^ Syntax Error: non default type parameter `T4` follows default type parameter
3 | type Alias[T = int, U] = ...
|
|
1 | class C[T = int, U]: ...
2 | class C[T1, T2 = int, T3, T4]: ...
3 | type Alias[T = int, U] = ...
| ^ Syntax Error: non default type parameter `U` follows default type parameter
|