diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index c1d98b6d50..aa9fa839f2 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -747,6 +747,7 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. } | SemanticSyntaxErrorKind::NonlocalAndGlobal(_) | SemanticSyntaxErrorKind::AnnotatedGlobal(_) + | SemanticSyntaxErrorKind::TypeParameterDefaultOrder(_) | SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => { self.semantic_errors.borrow_mut().push(error); } diff --git a/crates/ruff_python_parser/resources/inline/err/type_parameter_default_order.py b/crates/ruff_python_parser/resources/inline/err/type_parameter_default_order.py new file mode 100644 index 0000000000..91362a5007 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/type_parameter_default_order.py @@ -0,0 +1,3 @@ +class C[T = int, U]: ... +class C[T1, T2 = int, T3, T4]: ... +type Alias[T = int, U] = ... diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index cd7335bdbe..2c573271e1 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -144,11 +144,16 @@ impl SemanticSyntaxChecker { } } } - Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) - | Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => { - if let Some(type_params) = type_params { - Self::duplicate_type_parameter_name(type_params, ctx); - } + Stmt::ClassDef(ast::StmtClassDef { + type_params: Some(type_params), + .. + }) + | Stmt::TypeAlias(ast::StmtTypeAlias { + type_params: Some(type_params), + .. + }) => { + Self::duplicate_type_parameter_name(type_params, ctx); + Self::type_parameter_default_order(type_params, ctx); } Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { if let [Expr::Starred(ast::ExprStarred { range, .. })] = targets.as_slice() { @@ -611,6 +616,39 @@ impl SemanticSyntaxChecker { } } + fn type_parameter_default_order( + 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( parameters: &ast::Parameters, ctx: &Ctx, @@ -1066,6 +1104,12 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::DuplicateTypeParameter => { f.write_str("duplicate type parameter") } + SemanticSyntaxErrorKind::TypeParameterDefaultOrder(name) => { + write!( + f, + "non default type parameter `{name}` follows default type parameter" + ) + } SemanticSyntaxErrorKind::MultipleCaseAssignment(name) => { 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. 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)] diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap index 0bcbd3cc52..fd2b2f4714 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap @@ -375,3 +375,12 @@ Module( 4 | type X[**P = x := 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 + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap index c3e38b8e9a..0b4041088c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap @@ -459,3 +459,12 @@ Module( 5 | type X[T = x := 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 + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap index 4aff137a73..8cd49704ce 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap @@ -384,3 +384,11 @@ Module( | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a TypeVarTuple default 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 + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap new file mode 100644 index 0000000000..a8280e947a --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap @@ -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 + |