diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index a2e84fd9fd04..8999fc20ed49 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -217,6 +217,7 @@ ANN.cannot.assign.to.debug=Cannot assign to __debug__ ANN.unparenthesized.assignment.expression.statement=Unparenthesized assignment expressions are prohibited at the top level of an expression statement ANN.unparenthesized.assignment.expression.value=Unparenthesized assignment expressions are prohibited at the top level of the right hand side of an assignment statement ANN.assignment.expressions.within.a.comprehension.cannot.be.used.in.a.class.body=Assignment expressions within a comprehension cannot be used in a class body +ANN.assignment.expression.as.a.target=Assignment expression cannot be used as a target here ANN.ignore.errors.like.this=Ignore errors like this ANN.function.cannot.be.async=function \"{0}\" cannot be async ANN.python.does.not.support.yield.from.inside.async.functions=Python does not support 'yield from' inside async functions diff --git a/python/python-psi-impl/src/com/jetbrains/python/parsing/ExpressionParsing.java b/python/python-psi-impl/src/com/jetbrains/python/parsing/ExpressionParsing.java index 6749d5b51698..72f066992f7f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/parsing/ExpressionParsing.java +++ b/python/python-psi-impl/src/com/jetbrains/python/parsing/ExpressionParsing.java @@ -264,7 +264,7 @@ public class ExpressionParsing extends Parsing { assertCurrentToken(PyTokenTypes.FOR_KEYWORD); while (true) { myBuilder.advanceLexer(); - parseExpression(true, true); + parseStarTargets(); parseComprehensionRange(exprType == PyElementTypes.GENERATOR_EXPRESSION); while (myBuilder.getTokenType() == PyTokenTypes.IF_KEYWORD) { myBuilder.advanceLexer(); @@ -284,6 +284,32 @@ public class ExpressionParsing extends Parsing { expr.done(exprType); } + public boolean parseStarTargets() { + SyntaxTreeBuilder.Marker expr = myBuilder.mark(); + if (!parseStarExpression(true)) { + myBuilder.error(message("PARSE.expected.expression")); + expr.drop(); + return false; + } + if (myBuilder.getTokenType() == PyTokenTypes.COMMA) { + while (myBuilder.getTokenType() == PyTokenTypes.COMMA) { + myBuilder.advanceLexer(); + SyntaxTreeBuilder.Marker expr2 = myBuilder.mark(); + if (!parseStarExpression(true)) { + myBuilder.error(message("PARSE.expected.expression")); + expr2.rollbackTo(); + break; + } + expr2.drop(); + } + expr.done(PyElementTypes.TUPLE_EXPRESSION); + } + else { + expr.drop(); + } + return true; + } + protected void parseComprehensionRange(boolean generatorExpression) { checkMatches(PyTokenTypes.IN_KEYWORD, message("PARSE.expected.in")); boolean result; diff --git a/python/python-psi-impl/src/com/jetbrains/python/parsing/StatementParsing.java b/python/python-psi-impl/src/com/jetbrains/python/parsing/StatementParsing.java index dca8030e1da4..28d3c9a91369 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/parsing/StatementParsing.java +++ b/python/python-psi-impl/src/com/jetbrains/python/parsing/StatementParsing.java @@ -670,7 +670,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { protected void parseForPart() { final SyntaxTreeBuilder.Marker forPart = myBuilder.mark(); myBuilder.advanceLexer(); - getExpressionParser().parseExpression(true, true); + getExpressionParser().parseStarTargets(); checkMatches(PyTokenTypes.IN_KEYWORD, PyPsiBundle.message("PARSE.expected.in")); getExpressionParser().parseExpression(); parseColonAndSuite(); diff --git a/python/src/com/jetbrains/python/validation/AssignTargetAnnotator.java b/python/src/com/jetbrains/python/validation/AssignTargetAnnotator.java index b93756b2ab78..718a64ec5e8a 100644 --- a/python/src/com/jetbrains/python/validation/AssignTargetAnnotator.java +++ b/python/src/com/jetbrains/python/validation/AssignTargetAnnotator.java @@ -24,9 +24,11 @@ import com.jetbrains.python.PyNames; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.sdk.PythonSdkUtil; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import static com.jetbrains.python.PyPsiBundle.message; @@ -79,6 +81,7 @@ public class AssignTargetAnnotator extends PyAnnotator { PyExpression target = node.getForPart().getTarget(); if (target != null) { target.accept(new ExprVisitor(Operation.For)); + checkTargetIsNotAssignmentExpression(target); } } @@ -110,6 +113,20 @@ public class AssignTargetAnnotator extends PyAnnotator { } } + @Override + public void visitPyComprehensionElement(@NotNull PyComprehensionElement node) { + node.getForComponents().forEach(it -> checkTargetIsNotAssignmentExpression(it.getIteratorVariable())); + } + + private void checkTargetIsNotAssignmentExpression(@Nullable PyExpression expression) { + if (PyPsiUtils.flattenParens(expression) instanceof PyAssignmentExpression) { + getHolder() + .newAnnotation(HighlightSeverity.ERROR, PyBundle.message("ANN.assignment.expression.as.a.target")) + .range(expression) + .create(); + } + } + private class ExprVisitor extends PyElementVisitor { private final Operation myOp; private final @Nls String DELETING_NONE = message("ANN.deleting.none"); diff --git a/python/testData/highlighting/assignmentExpressionAsATarget.py b/python/testData/highlighting/assignmentExpressionAsATarget.py new file mode 100644 index 000000000000..7ab497c1710d --- /dev/null +++ b/python/testData/highlighting/assignmentExpressionAsATarget.py @@ -0,0 +1,4 @@ +[x1 for (x1 := 2) in (1, 2, 3)] + +for (x1 := 2) in (1, 2, 3): + pass \ No newline at end of file diff --git a/python/testData/psi/InvalidAssignmentExpressions.py b/python/testData/psi/InvalidAssignmentExpressions.py index 2adcb0fe409b..c8916fc79495 100644 --- a/python/testData/psi/InvalidAssignmentExpressions.py +++ b/python/testData/psi/InvalidAssignmentExpressions.py @@ -12,4 +12,7 @@ (b := -) (x := ) -x = (b[j] := z) = 'spam' # z is a reference \ No newline at end of file +x = (b[j] := z) = 'spam' # z is a reference + +[x1 for x1 := 2 in (1, 2, 3)] +[x1 for (x1 := 2) in (1, 2, 3)] \ No newline at end of file diff --git a/python/testData/psi/InvalidAssignmentExpressions.txt b/python/testData/psi/InvalidAssignmentExpressions.txt index ac530e5e0c66..7e15ba691ee0 100644 --- a/python/testData/psi/InvalidAssignmentExpressions.txt +++ b/python/testData/psi/InvalidAssignmentExpressions.txt @@ -142,4 +142,81 @@ PyFile:InvalidAssignmentExpressions.py PyStringLiteralExpression: spam PsiElement(Py:SINGLE_QUOTED_STRING)(''spam'') PsiWhiteSpace(' ') - PsiComment(Py:END_OF_LINE_COMMENT)('# z is a reference') \ No newline at end of file + PsiComment(Py:END_OF_LINE_COMMENT)('# z is a reference') + PsiWhiteSpace('\n\n') + PyAssignmentExpression + PyListCompExpression + PsiElement(Py:LBRACKET)('[') + PyReferenceExpression: x1 + PsiElement(Py:IDENTIFIER)('x1') + PsiWhiteSpace(' ') + PsiElement(Py:FOR_KEYWORD)('for') + PsiWhiteSpace(' ') + PyTargetExpression: x1 + PsiElement(Py:IDENTIFIER)('x1') + PsiErrorElement:'in' expected + + PsiWhiteSpace(' ') + PsiElement(Py:COLONEQ)(':=') + PsiWhiteSpace(' ') + PyBinaryExpression + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('2') + PsiWhiteSpace(' ') + PsiElement(Py:IN_KEYWORD)('in') + PsiWhiteSpace(' ') + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyTupleExpression + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('1') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('2') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('3') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RBRACKET)(']') + PsiErrorElement:Statement expected, found Py:LBRACKET + + PsiWhiteSpace('\n') + PyExpressionStatement + PyListCompExpression + PsiElement(Py:LBRACKET)('[') + PyReferenceExpression: x1 + PsiElement(Py:IDENTIFIER)('x1') + PsiWhiteSpace(' ') + PsiElement(Py:FOR_KEYWORD)('for') + PsiWhiteSpace(' ') + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyAssignmentExpression + PyTargetExpression: x1 + PsiElement(Py:IDENTIFIER)('x1') + PsiWhiteSpace(' ') + PsiElement(Py:COLONEQ)(':=') + PsiWhiteSpace(' ') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('2') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:IN_KEYWORD)('in') + PsiWhiteSpace(' ') + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyTupleExpression + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('1') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('2') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('3') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RBRACKET)(']') \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java index da37f550ae47..5d8e5e382962 100644 --- a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java +++ b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java @@ -488,6 +488,11 @@ public class PythonHighlightingTest extends PyTestCase { doTest(LanguageLevel.PYTHON38, false, false); } + // PY-36478 + public void testAssignmentExpressionAsATarget() { + doTest(LanguageLevel.getLatest(), false, false); + } + @NotNull private static EditorColorsScheme createTemporaryColorScheme() { EditorColorsManager manager = EditorColorsManager.getInstance();