Skip to content

bpo-40334: Produce better error messages on invalid targets #20106

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 24 commits into from
Jun 18, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0dfaa22
bpo-40334: Produce better error messages on invalid targets
lysnikolaou May 15, 2020
d4a9dd6
Fix test_exceptions
lysnikolaou May 15, 2020
72a1339
📜🤖 Added by blurb_it.
blurb-it[bot] May 15, 2020
1b6cf0b
Produce the same error messages with the old parser for with and for …
lysnikolaou May 15, 2020
1ad3819
Add invalid_group rule
lysnikolaou May 15, 2020
6f30315
Update Grammar/python.gram
lysnikolaou May 16, 2020
f750aba
Update Parser/pegen/pegen.c
lysnikolaou May 16, 2020
132178f
Address Guido's feedback
lysnikolaou May 16, 2020
17cc4bc
Inline _PyPegen_get_invalid_for_target
lysnikolaou May 17, 2020
def0f3d
Revert inlining
lysnikolaou May 17, 2020
ee3524b
Merge remote-tracking branch 'upstream/master' into targets-errors
pablogsal May 17, 2020
4959a12
Fix searching for invalid for targets, by visiting Compare nodes in _…
lysnikolaou May 17, 2020
dd1d8c8
Call RAISE_SYNTAX_ERROR instead of returning NULL, when no invalid fo…
lysnikolaou May 17, 2020
24d63fb
Merge branch 'master' into targets-errors
lysnikolaou May 18, 2020
c1cf8e6
Replace is_del and is_for args with an enum
lysnikolaou May 18, 2020
06798ff
Only return an invalid target, when the comparison is an In comparison
lysnikolaou May 20, 2020
ef7fe3a
Fix bug
lysnikolaou May 20, 2020
efabcc9
Merge branch 'master' into targets-errors
lysnikolaou May 23, 2020
6d41c55
Merge branch 'master' into targets-errors
lysnikolaou May 26, 2020
82486c1
Minor refactoring
lysnikolaou May 26, 2020
705d55b
Merge branch 'master' into targets-errors
lysnikolaou Jun 10, 2020
f0f934d
Merge branch 'master' into targets-errors
lysnikolaou Jun 10, 2020
ede107e
Merge branch 'master' into targets-errors
lysnikolaou Jun 18, 2020
55b5327
Add CHECK* checks to GET_INVALID_*TARGET macros
lysnikolaou Jun 18, 2020
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
82 changes: 56 additions & 26 deletions Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ assignment[stmt_ty]:
| a=('(' b=single_target ')' { b }
| single_subscript_attribute_target) ':' b=expression c=['=' d=annotated_rhs { d }] {
CHECK_VERSION(6, "Variable annotations syntax is", _Py_AnnAssign(a, b, c, 0, EXTRA)) }
| a=(z=star_targets '=' { z })+ b=(yield_expr | star_expressions) tc=[TYPE_COMMENT] {
| a=(z=star_targets '=' { z })+ ~ b=(yield_expr | star_expressions) tc=[TYPE_COMMENT] {
_Py_Assign(a, b, NEW_TYPE_COMMENT(p, tc), EXTRA) }
| a=single_target b=augassign c=(yield_expr | star_expressions) {
| a=single_target b=augassign ~ c=(yield_expr | star_expressions) {
_Py_AugAssign(a, b->kind, c, EXTRA) }
| invalid_assignment

Expand Down Expand Up @@ -122,7 +122,9 @@ yield_stmt[stmt_ty]: y=yield_expr { _Py_Expr(y, EXTRA) }

assert_stmt[stmt_ty]: 'assert' a=expression b=[',' z=expression { z }] { _Py_Assert(a, b, EXTRA) }

del_stmt[stmt_ty]: 'del' a=del_targets { _Py_Delete(a, EXTRA) }
del_stmt[stmt_ty]:
| 'del' a=del_targets &(';' | NEWLINE) { _Py_Delete(a, EXTRA) }
| invalid_del_stmt

import_stmt[stmt_ty]: import_name | import_from
import_name[stmt_ty]: 'import' a=dotted_as_names { _Py_Import(a, EXTRA) }
Expand Down Expand Up @@ -164,10 +166,11 @@ while_stmt[stmt_ty]:
| 'while' a=named_expression ':' b=block c=[else_block] { _Py_While(a, b, c, EXTRA) }

for_stmt[stmt_ty]:
| 'for' t=star_targets 'in' ex=star_expressions ':' tc=[TYPE_COMMENT] b=block el=[else_block] {
| 'for' t=star_targets 'in' ~ ex=star_expressions ':' tc=[TYPE_COMMENT] b=block el=[else_block] {
_Py_For(t, ex, b, el, NEW_TYPE_COMMENT(p, tc), EXTRA) }
| ASYNC 'for' t=star_targets 'in' ex=star_expressions ':' tc=[TYPE_COMMENT] b=block el=[else_block] {
| ASYNC 'for' t=star_targets 'in' ~ ex=star_expressions ':' tc=[TYPE_COMMENT] b=block el=[else_block] {
CHECK_VERSION(5, "Async for loops are", _Py_AsyncFor(t, ex, b, el, NEW_TYPE_COMMENT(p, tc), EXTRA)) }
| invalid_for_target

with_stmt[stmt_ty]:
| 'with' '(' a=','.with_item+ ','? ')' ':' b=block {
Expand All @@ -179,7 +182,9 @@ with_stmt[stmt_ty]:
| ASYNC 'with' a=','.with_item+ ':' tc=[TYPE_COMMENT] b=block {
CHECK_VERSION(5, "Async with statements are", _Py_AsyncWith(a, b, NEW_TYPE_COMMENT(p, tc), EXTRA)) }
with_item[withitem_ty]:
| e=expression o=['as' t=target { t }] { _Py_withitem(e, o, p->arena) }
| e=expression 'as' t=target &(',' | ')' | ':') { _Py_withitem(e, t, p->arena) }
| invalid_with_item
| e=expression { _Py_withitem(e, NULL, p->arena) }

try_stmt[stmt_ty]:
| 'try' ':' b=block f=finally_block { _Py_Try(b, NULL, NULL, f, EXTRA) }
Expand Down Expand Up @@ -311,7 +316,7 @@ star_named_expression[expr_ty]:
| '*' a=bitwise_or { _Py_Starred(a, Load, EXTRA) }
| named_expression
named_expression[expr_ty]:
| a=NAME ':=' b=expression { _Py_NamedExpr(CHECK(_PyPegen_set_expr_context(p, a, Store)), b, EXTRA) }
| a=NAME ':=' ~ b=expression { _Py_NamedExpr(CHECK(_PyPegen_set_expr_context(p, a, Store)), b, EXTRA) }
| expression !':='
| invalid_named_expression

Expand Down Expand Up @@ -484,18 +489,20 @@ strings[expr_ty] (memo): a=STRING+ { _PyPegen_concatenate_strings(p, a) }
list[expr_ty]:
| '[' a=[star_named_expressions] ']' { _Py_List(a, Load, EXTRA) }
listcomp[expr_ty]:
| '[' a=named_expression b=for_if_clauses ']' { _Py_ListComp(a, b, EXTRA) }
| '[' a=named_expression ~ b=for_if_clauses ']' { _Py_ListComp(a, b, EXTRA) }
| invalid_comprehension
tuple[expr_ty]:
| '(' a=[y=star_named_expression ',' z=[star_named_expressions] { _PyPegen_seq_insert_in_front(p, y, z) } ] ')' {
_Py_Tuple(a, Load, EXTRA) }
group[expr_ty]: '(' a=(yield_expr | named_expression) ')' { a }
group[expr_ty]:
| '(' a=(yield_expr | named_expression) ')' { a }
| invalid_group
genexp[expr_ty]:
| '(' a=expression b=for_if_clauses ')' { _Py_GeneratorExp(a, b, EXTRA) }
| '(' a=expression ~ b=for_if_clauses ')' { _Py_GeneratorExp(a, b, EXTRA) }
| invalid_comprehension
set[expr_ty]: '{' a=expressions_list '}' { _Py_Set(a, EXTRA) }
setcomp[expr_ty]:
| '{' a=expression b=for_if_clauses '}' { _Py_SetComp(a, b, EXTRA) }
| '{' a=expression ~ b=for_if_clauses '}' { _Py_SetComp(a, b, EXTRA) }
| invalid_comprehension
dict[expr_ty]:
| '{' a=[kvpairs] '}' { _Py_Dict(CHECK(_PyPegen_get_keys(p, a)),
Expand All @@ -509,10 +516,11 @@ kvpair[KeyValuePair*]:
for_if_clauses[asdl_seq*]:
| for_if_clause+
for_if_clause[comprehension_ty]:
| ASYNC 'for' a=star_targets 'in' b=disjunction c=('if' z=disjunction { z })* {
| ASYNC 'for' a=star_targets 'in' ~ b=disjunction c=('if' z=disjunction { z })* {
CHECK_VERSION(6, "Async comprehensions are", _Py_comprehension(a, b, c, 1, p->arena)) }
| 'for' a=star_targets 'in' b=disjunction c=('if' z=disjunction { z })* {
| 'for' a=star_targets 'in' ~ b=disjunction c=('if' z=disjunction { z })* {
_Py_comprehension(a, b, c, 0, p->arena) }
| invalid_for_target

yield_expr[expr_ty]:
| 'yield' 'from' a=expression { _Py_YieldFrom(a, EXTRA) }
Expand Down Expand Up @@ -582,19 +590,15 @@ single_subscript_attribute_target[expr_ty]:
| a=t_primary '[' b=slices ']' !t_lookahead { _Py_Subscript(a, b, Store, EXTRA) }

del_targets[asdl_seq*]: a=','.del_target+ [','] { a }
# The lookaheads to del_target_end ensure that we don't match expressions where a prefix of the
# expression matches our rule, thereby letting these cases fall through to invalid_del_target.
del_target[expr_ty] (memo):
| a=t_primary '.' b=NAME &del_target_end { _Py_Attribute(a, b->v.Name.id, Del, EXTRA) }
| a=t_primary '[' b=slices ']' &del_target_end { _Py_Subscript(a, b, Del, EXTRA) }
| a=t_primary '.' b=NAME !t_lookahead { _Py_Attribute(a, b->v.Name.id, Del, EXTRA) }
| a=t_primary '[' b=slices ']' !t_lookahead { _Py_Subscript(a, b, Del, EXTRA) }
| del_t_atom
del_t_atom[expr_ty]:
| a=NAME &del_target_end { _PyPegen_set_expr_context(p, a, Del) }
| a=NAME { _PyPegen_set_expr_context(p, a, Del) }
| '(' a=del_target ')' { _PyPegen_set_expr_context(p, a, Del) }
| '(' a=[del_targets] ')' { _Py_Tuple(a, Del, EXTRA) }
| '[' a=[del_targets] ']' { _Py_List(a, Del, EXTRA) }
| invalid_del_target
del_target_end: ')' | ']' | ',' | ';' | NEWLINE

targets[asdl_seq*]: a=','.target+ [','] { a }
target[expr_ty] (memo):
Expand Down Expand Up @@ -642,15 +646,22 @@ invalid_assignment:
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "illegal target for annotation") }
| a=star_expressions '=' (yield_expr | star_expressions) {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(
_PyPegen_get_invalid_target(a),
"cannot assign to %s", _PyPegen_get_expr_name(_PyPegen_get_invalid_target(a))) }
GET_INVALID_TARGET(a),
"cannot assign to %s", _PyPegen_get_expr_name(GET_INVALID_TARGET(a))) }
| a=star_expressions augassign (yield_expr | star_expressions) {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(
a,
"'%s' is an illegal expression for augmented assignment",
_PyPegen_get_expr_name(a)
)}

invalid_del_stmt:
| 'del' a=star_expressions {
GET_INVALID_DEL_TARGET(a) != NULL ?
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(
GET_INVALID_DEL_TARGET(a),
"cannot delete %s", _PyPegen_get_expr_name(GET_INVALID_DEL_TARGET(a))
) :
RAISE_SYNTAX_ERROR("invalid syntax") }
invalid_block:
| NEWLINE !INDENT { RAISE_INDENTATION_ERROR("expected an indented block") }
invalid_comprehension:
Expand All @@ -666,6 +677,25 @@ invalid_lambda_star_etc:
invalid_double_type_comments:
| TYPE_COMMENT NEWLINE TYPE_COMMENT NEWLINE INDENT {
RAISE_SYNTAX_ERROR("Cannot have two type comments on def") }
invalid_del_target:
| a=star_expression &del_target_end {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "cannot delete %s", _PyPegen_get_expr_name(a)) }
invalid_with_item:
| expression 'as' a=expression {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(
GET_INVALID_TARGET(a),
"cannot assign to %s", _PyPegen_get_expr_name(GET_INVALID_TARGET(a))
) }

# Comparison is used here, because the `a() in b` in `for a() in b: pass` is parsed
# as a comparison. Thus, we only need to search the left side of the comparison
# for invalid targets.
invalid_for_target:
| ASYNC? 'for' a=comparison {
_PyPegen_get_invalid_for_target(a) != NULL ?
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(
_PyPegen_get_invalid_for_target(a),
"cannot assign to %s", _PyPegen_get_expr_name(_PyPegen_get_invalid_for_target(a))
) :
NULL }

invalid_group:
| '(' a=starred_expression ')' {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "can't use starred expression here") }
2 changes: 1 addition & 1 deletion Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,9 @@ def baz():
check('def f():\n x, y: int', 2, 3)
check('[*x for x in xs]', 1, 2)
check('foo(x for x in range(10), 100)', 1, 5)
check('for 1 in []: pass', 1, 5)
check('(yield i) = 2', 1, 1 if support.use_old_parser() else 2)
check('def f(*):\n pass', 1, 7 if support.use_old_parser() else 8)
check('for 1 in []: pass', 1, 5 if support.use_old_parser() else 7)

@cpython_only
def testSettingException(self):
Expand Down
53 changes: 52 additions & 1 deletion Lib/test/test_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,57 @@
Traceback (most recent call last):
SyntaxError: 'list' is an illegal expression for augmented assignment

Invalid targets in `for` loops and `with` statements should also
produce a specialized error message

>>> for a() in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

>>> for (a, b()) in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

>>> for [a, b()] in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

>>> for (*a, b, c+1) in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to operator

>>> for (x, *(y, z.d())) in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

>>> for i < (): pass
Traceback (most recent call last):
SyntaxError: invalid syntax

>>> with a as b(): pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

>>> with a as (b, c()): pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

>>> with a as [b, c()]: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

>>> with a as (*b, c, d+1): pass
Traceback (most recent call last):
SyntaxError: cannot assign to operator

>>> with a as (x, *(y, z.d())): pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

>>> with a as b, c as d(): pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call

From compiler_complex_args():

>>> def f(None=1):
Expand Down Expand Up @@ -702,7 +753,7 @@ def test_assign_del(self):
self._check_error("del (1, 2)", "delete literal")
self._check_error("del None", "delete None")
self._check_error("del *x", "delete starred")
self._check_error("del (*x)", "delete starred")
self._check_error("del (*x)", "use starred expression")
self._check_error("del (*x,)", "delete starred")
self._check_error("del [*x,]", "delete starred")
self._check_error("del f()", "delete function call")
Expand Down
Loading