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();