-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[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
Changes from all commits
711c435
5532370
c800731
b88e89d
38a6662
aaae2c3
1786cef
aa13524
78e5937
0a2672c
250a5b7
2fac42e
bb38567
134ee83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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)): ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
} | ||
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. | ||
|
@@ -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, | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not 100% sure but using the term |
||
/// 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: | ||
|
@@ -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, | ||
|
@@ -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; | ||
|
There was a problem hiding this comment.
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
).There was a problem hiding this comment.
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: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.There was a problem hiding this comment.
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
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.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:
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?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.