Skip to content

[syntax-errors] Invalid syntax in annotations #17101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 3, 2025
Merged
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
1 change: 1 addition & 0 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_)
| SemanticSyntaxErrorKind::SingleStarredAssignment
| SemanticSyntaxErrorKind::WriteToDebug(_)
| SemanticSyntaxErrorKind::InvalidExpression(..)
| SemanticSyntaxErrorKind::DuplicateMatchKey(_)
| SemanticSyntaxErrorKind::InvalidStarExpression => {
if self.settings.preview.is_enabled() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class F[T](y := list): ...
class G((yield 1)): ...
class H((yield from 1)): ...
class I[T]((yield 1)): ...
class J[T]((yield from 1)): ...
class K[T: (yield 1)]: ... # yield in TypeVar
class L[T: (x := 1)]: ... # named expr in TypeVar
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
def f[T]() -> (y := 3): ...
def g[T](arg: (x := 1)): ...
def h[T](x: (yield 1)): ...
def i(x: (yield 1)): ...
def j[T]() -> (yield 1): ...
def k() -> (yield 1): ...
def l[T](x: (yield from 1)): ...
def m(x: (yield from 1)): ...
def n[T]() -> (yield from 1): ...
def o() -> (yield from 1): ...
def p[T: (yield 1)](): ... # yield in TypeVar bound
def q[T = (yield 1)](): ... # yield in TypeVar default
def r[*Ts = (yield 1)](): ... # yield in TypeVarTuple default
def s[**Ts = (yield 1)](): ... # yield in ParamSpec default
def t[T: (x := 1)](): ... # named expr in TypeVar bound
def u[T = (x := 1)](): ... # named expr in TypeVar default
def v[*Ts = (x := 1)](): ... # named expr in TypeVarTuple default
def w[**Ts = (x := 1)](): ... # named expr in ParamSpec default
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type X[T: (yield 1)] = int # TypeVar bound
type X[T = (yield 1)] = int # TypeVar default
type X[*Ts = (yield 1)] = int # TypeVarTuple default
type X[**Ts = (yield 1)] = int # ParamSpec default
type Y = (yield 1) # yield in value
type Y = (x := 1) # named expr in value
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
def foo() -> int | str: ...
def foo() -> lambda x: x: ...
def foo() -> (yield x): ...
def foo() -> int if True else str: ...
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
def foo(arg: int): ...
def foo(arg: lambda x: x): ...
def foo(arg: (yield x)): ...
def foo(arg: (x := int)): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class F(y := list): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def f() -> (y := 3): ...
def g(arg: (x := 1)): ...
2 changes: 0 additions & 2 deletions crates/ruff_python_parser/src/parser/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1891,7 +1891,6 @@ impl<'src> Parser<'src> {
// test_ok function_def_valid_return_expr
// def foo() -> int | str: ...
// def foo() -> lambda x: x: ...
// def foo() -> (yield x): ...
// def foo() -> int if True else str: ...

// test_err function_def_invalid_return_expr
Expand Down Expand Up @@ -2986,7 +2985,6 @@ impl<'src> Parser<'src> {
// test_ok param_with_annotation
// def foo(arg: int): ...
// def foo(arg: lambda x: x): ...
// def foo(arg: (yield x)): ...
// def foo(arg: (x := int)): ...

// test_err param_with_invalid_annotation
Expand Down
257 changes: 257 additions & 0 deletions crates/ruff_python_parser/src/semantic_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,120 @@ impl SemanticSyntaxChecker {
}

Self::debug_shadowing(stmt, ctx);
Self::check_annotation(stmt, ctx);
}

fn check_annotation<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
type_params,
parameters,
returns,
..
}) => {
// test_ok valid_annotation_function
// def f() -> (y := 3): ...
// def g(arg: (x := 1)): ...

// test_err invalid_annotation_function
// def f[T]() -> (y := 3): ...
// def g[T](arg: (x := 1)): ...
// def h[T](x: (yield 1)): ...
// def i(x: (yield 1)): ...
// def j[T]() -> (yield 1): ...
// def k() -> (yield 1): ...
// def l[T](x: (yield from 1)): ...
// def m(x: (yield from 1)): ...
// def n[T]() -> (yield from 1): ...
// def o() -> (yield from 1): ...
// def p[T: (yield 1)](): ... # yield in TypeVar bound
// def q[T = (yield 1)](): ... # yield in TypeVar default
// def r[*Ts = (yield 1)](): ... # yield in TypeVarTuple default
// def s[**Ts = (yield 1)](): ... # yield in ParamSpec default
// def t[T: (x := 1)](): ... # named expr in TypeVar bound
// def u[T = (x := 1)](): ... # named expr in TypeVar default
// def v[*Ts = (x := 1)](): ... # named expr in TypeVarTuple default
// def w[**Ts = (x := 1)](): ... # named expr in ParamSpec default
let is_generic = type_params.is_some();
let mut visitor = InvalidExpressionVisitor {
allow_named_expr: !is_generic,
position: InvalidExpressionPosition::TypeAnnotation,
ctx,
};
if let Some(type_params) = type_params {
visitor.visit_type_params(type_params);
}
if is_generic {
visitor.position = InvalidExpressionPosition::GenericDefinition;
} else {
visitor.position = InvalidExpressionPosition::TypeAnnotation;
}
for param in parameters
.iter()
.filter_map(ast::AnyParameterRef::annotation)
{
visitor.visit_expr(param);
}
Comment on lines +160 to +165
Copy link
Member

Choose a reason for hiding this comment

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

I know this rule is specific to annotations but I was wondering if we need to run the same check for the default value? I'm mainly asking because this PR also checks the default value in type params (type X[T = (yield 1)] = int).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right that this is a syntax error, but it looks like we already catch these for yield expressions: playground, and they seem to be okay for named expressions:

>>> def foo(x=(y := [])): ...
... def bar(x=(y := [])): ...
...
>>>

I guess these are currently caught in the parser, so we could move the detection here if we wanted and use visitor.visit_parameters, though.

Copy link
Member

Choose a reason for hiding this comment

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

Can you specify which errors are caught in the parser? The playground link that you've provided suggest that the yield expressions are part of the lint rule while the named expressions are fine.

Regardless, we also visit the type param default in here which makes me think whether we should instead split the rule as "InvalidYieldExpressionUsage" and "InvalidNamedExpressionUsage" similar to the variants on ParseErrorType?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you specify which errors are caught in the parser? The playground link that you've provided suggest that the yield expressions are part of the lint rule while the named expressions are fine.

Oh you're right, I just assumed it was a ParseError when I saw a diagnostic, but I see now that it's lint rule F704. I guess that's another rule I should reimplement, like F404, but I'll save that for a follow-up.

Regardless, we also visit the type param default in here which makes me think whether we should instead split the rule as "InvalidYieldExpressionUsage" and "InvalidNamedExpressionUsage" similar to the variants on ParseErrorType?

Ah, I see what you mean. I guess I thought type parameter defaults were close enough to "type annotations" that the current error message made sense, but I see now that they are pretty different. That's also in line with CPython's different error message:

>>> type X[T= (yield 1)] = int
  File "<python-input-112>", line 1
    type X[T= (yield 1)] = int
               ^^^^^^^
SyntaxError: yield expression cannot be used within a TypeVar default

Is it enough to split them into one variant for yield and one for named expressions, or should we try to split them up more like CPython?

Copy link
Member

Choose a reason for hiding this comment

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

Is it enough to split them into one variant for yield and one for named expressions, or should we try to split them up more like CPython?

I think what you have right now looks great. I only suggested to split them up if we didn't want to provide granular context but I think providing them seems more useful.

if let Some(returns) = returns {
visitor.visit_expr(returns);
}
}
Stmt::ClassDef(ast::StmtClassDef {
type_params,
arguments,
..
}) => {
// test_ok valid_annotation_class
// class F(y := list): ...

// test_err invalid_annotation_class
// class F[T](y := list): ...
// class G((yield 1)): ...
// class H((yield from 1)): ...
// class I[T]((yield 1)): ...
// class J[T]((yield from 1)): ...
// class K[T: (yield 1)]: ... # yield in TypeVar
// class L[T: (x := 1)]: ... # named expr in TypeVar
let is_generic = type_params.is_some();
let mut visitor = InvalidExpressionVisitor {
allow_named_expr: !is_generic,
position: InvalidExpressionPosition::TypeAnnotation,
ctx,
};
if let Some(type_params) = type_params {
visitor.visit_type_params(type_params);
}
if is_generic {
visitor.position = InvalidExpressionPosition::GenericDefinition;
} else {
visitor.position = InvalidExpressionPosition::BaseClass;
}
if let Some(arguments) = arguments {
visitor.visit_arguments(arguments);
}
}
Stmt::TypeAlias(ast::StmtTypeAlias {
type_params, value, ..
}) => {
// test_err invalid_annotation_type_alias
// type X[T: (yield 1)] = int # TypeVar bound
// type X[T = (yield 1)] = int # TypeVar default
// type X[*Ts = (yield 1)] = int # TypeVarTuple default
// type X[**Ts = (yield 1)] = int # ParamSpec default
// type Y = (yield 1) # yield in value
// type Y = (x := 1) # named expr in value
let mut visitor = InvalidExpressionVisitor {
allow_named_expr: false,
position: InvalidExpressionPosition::TypeAlias,
ctx,
};
visitor.visit_expr(value);
if let Some(type_params) = type_params {
visitor.visit_type_params(type_params);
}
}
_ => {}
}
}

/// Emit a [`SemanticSyntaxErrorKind::InvalidStarExpression`] if `expr` is starred.
Expand Down Expand Up @@ -568,6 +682,15 @@ impl Display for SemanticSyntaxError {
write!(f, "cannot delete `__debug__` on Python {python_version} (syntax was removed in 3.9)")
}
},
SemanticSyntaxErrorKind::InvalidExpression(
kind,
InvalidExpressionPosition::BaseClass,
) => {
write!(f, "{kind} cannot be used as a base class")
}
SemanticSyntaxErrorKind::InvalidExpression(kind, position) => {
write!(f, "{kind} cannot be used within a {position}")
}
SemanticSyntaxErrorKind::DuplicateMatchKey(key) => {
write!(
f,
Expand Down Expand Up @@ -695,6 +818,21 @@ pub enum SemanticSyntaxErrorKind {
/// [BPO 45000]: https://github.com/python/cpython/issues/89163
WriteToDebug(WriteToDebugKind),

/// Represents the use of an invalid expression kind in one of several locations.
///
/// The kinds include `yield` and `yield from` expressions and named expressions, and locations
/// include type parameter bounds and defaults, type annotations, type aliases, and base class
/// lists.
///
/// ## Examples
///
/// ```python
/// type X[T: (yield 1)] = int
/// type Y = (yield 1)
Copy link
Member

Choose a reason for hiding this comment

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

I'm not 100% sure but using the term type annotation in a type alias seems misleading because the value isn't a type annotation. The same applies to when using a yield expression in a type var default. We could use a more generic term (e.g. yield expression cannot be used in a type expression) or we distinguish the different contexts. I'm leaning towards the latter.

/// def f[T](x: int) -> (y := 3): return x
/// ```
InvalidExpression(InvalidExpressionKind, InvalidExpressionPosition),

/// Represents a duplicate key in a `match` mapping pattern.
///
/// The [CPython grammar] allows keys in mapping patterns to be literals or attribute accesses:
Expand Down Expand Up @@ -757,6 +895,48 @@ pub enum SemanticSyntaxErrorKind {
InvalidStarExpression,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InvalidExpressionPosition {
TypeVarBound,
TypeVarDefault,
TypeVarTupleDefault,
ParamSpecDefault,
TypeAnnotation,
BaseClass,
GenericDefinition,
TypeAlias,
}

impl Display for InvalidExpressionPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
InvalidExpressionPosition::TypeVarBound => "TypeVar bound",
InvalidExpressionPosition::TypeVarDefault => "TypeVar default",
InvalidExpressionPosition::TypeVarTupleDefault => "TypeVarTuple default",
InvalidExpressionPosition::ParamSpecDefault => "ParamSpec default",
InvalidExpressionPosition::TypeAnnotation => "type annotation",
InvalidExpressionPosition::GenericDefinition => "generic definition",
InvalidExpressionPosition::BaseClass => "base class",
InvalidExpressionPosition::TypeAlias => "type alias",
})
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum InvalidExpressionKind {
Yield,
NamedExpr,
}

impl Display for InvalidExpressionKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
InvalidExpressionKind::Yield => "yield expression",
InvalidExpressionKind::NamedExpr => "named expression",
})
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum WriteToDebugKind {
Store,
Expand Down Expand Up @@ -878,6 +1058,83 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> {
}
}

struct InvalidExpressionVisitor<'a, Ctx> {
/// Allow named expressions (`x := ...`) to appear in annotations.
///
/// These are allowed in non-generic functions, for example:
///
/// ```python
/// def foo(arg: (x := int)): ... # ok
/// def foo[T](arg: (x := int)): ... # syntax error
/// ```
allow_named_expr: bool,

/// Context used for emitting errors.
ctx: &'a Ctx,

position: InvalidExpressionPosition,
}

impl<Ctx> Visitor<'_> for InvalidExpressionVisitor<'_, Ctx>
where
Ctx: SemanticSyntaxContext,
{
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Named(ast::ExprNamed { range, .. }) if !self.allow_named_expr => {
SemanticSyntaxChecker::add_error(
self.ctx,
SemanticSyntaxErrorKind::InvalidExpression(
InvalidExpressionKind::NamedExpr,
self.position,
),
*range,
);
}
Expr::Yield(ast::ExprYield { range, .. })
| Expr::YieldFrom(ast::ExprYieldFrom { range, .. }) => {
SemanticSyntaxChecker::add_error(
self.ctx,
SemanticSyntaxErrorKind::InvalidExpression(
InvalidExpressionKind::Yield,
self.position,
),
*range,
);
}
_ => {}
}
ast::visitor::walk_expr(self, expr);
}

fn visit_type_param(&mut self, type_param: &ast::TypeParam) {
match type_param {
ast::TypeParam::TypeVar(ast::TypeParamTypeVar { bound, default, .. }) => {
if let Some(expr) = bound {
self.position = InvalidExpressionPosition::TypeVarBound;
self.visit_expr(expr);
}
if let Some(expr) = default {
self.position = InvalidExpressionPosition::TypeVarDefault;
self.visit_expr(expr);
}
}
ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { default, .. }) => {
if let Some(expr) = default {
self.position = InvalidExpressionPosition::TypeVarTupleDefault;
self.visit_expr(expr);
}
}
ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { default, .. }) => {
if let Some(expr) = default {
self.position = InvalidExpressionPosition::ParamSpecDefault;
self.visit_expr(expr);
}
}
};
}
}

pub trait SemanticSyntaxContext {
/// Returns `true` if a module's docstring boundary has been passed.
fn seen_docstring_boundary(&self) -> bool;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/function_def_invalid_return_expr.py
snapshot_kind: text
---
## AST

Expand Down Expand Up @@ -180,3 +179,13 @@ Module(
3 | def foo() -> yield x: ...
| ^^^^^^^ Syntax Error: Yield expression cannot be used here
|


## Semantic Syntax Errors

|
1 | def foo() -> *int: ...
2 | def foo() -> (*int): ...
3 | def foo() -> yield x: ...
| ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation
|
Loading
Loading