From 97b22aaa1313b6fc2d0a33bcce859c427c126fd8 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Mon, 2 May 2022 16:55:02 +0300 Subject: [PATCH] PY-42200 Support parenthesized context managers in Python 3.9+ In case of syntactic ambiguity with previous versions of the grammar, such as "with (expr)" or "with (expr1, expr2)", PyWithStatement is still parsed as having its own parentheses, not a parenthesized expression or a tuple as a single context expression. The latter case, even though syntactically legal, is still reported by the compatibility inspection in Python <3.9. These changes also include proper formatter and editing support (e.g. not inserting backslashes on line breaks inside parentheses), as well as Complete Current Statement, which now takes possible parentheses into account while inserting a missing colon. The changes in the formatter are somewhat ad-hoc, intended to minimize the effect on other constructs. "With" statement is somewhat special in the sense that it's the first compound statement (having a statement list) with its own list-like part in parentheses. Existing tests on with statement processing were expanded and uniformly named. Co-authored-by: Semyon Proshev GitOrigin-RevId: 15c33e97f177e81b5ed23891063555df016feb05 --- .../resources/messages/PyPsiBundle.properties | 1 + .../jetbrains/python/formatter/PyBlock.java | 64 +++++- .../python/parsing/StatementParsing.java | 69 ++++-- .../validation/CompatibilityVisitor.java | 28 ++- .../smartEnter/fixers/PyWithFixer.java | 53 +++-- .../python/editor/PythonEnterHandler.java | 13 +- ...xpressionMissingNoSpaceAfterWithKeyword.py | 1 + ...ionMissingNoSpaceAfterWithKeyword_after.py | 1 + ...ithItemsColonMissingAndTargetIncomplete.py | 1 + ...msColonMissingAndTargetIncomplete_after.py | 1 + ...nthesizedWithItemsFirstTargetIncomplete.py | 2 + ...zedWithItemsFirstTargetIncomplete_after.py | 2 + ...enthesizedWithItemsLastTargetIncomplete.py | 3 + ...izedWithItemsLastTargetIncomplete_after.py | 3 + .../withParenthesizedWithItemsNothingToFix.py | 2 + ...arenthesizedWithItemsNothingToFix_after.py | 3 + ...hParenthesizedWithItemsOnlyColonMissing.py | 1 + ...thesizedWithItemsOnlyColonMissing_after.py | 2 + ...gClosingBracketInParenthesizedWithItems.py | 6 + ...ngBracketInParenthesizedWithItems_after.py | 6 + .../formatter/parenthesizedWithItems.py | 13 ++ ...imilarlyToCollectionsInStatementHeaders.py | 26 +++ ...lyToCollectionsInStatementHeaders_after.py | 26 +++ .../parenthesizedWithItemsWrapping.py | 3 + .../parenthesizedWithItemsWrapping_after.py | 3 + .../formatter/parenthesizedWithItems_after.py | 13 ++ .../parenthesizedWithItems.py | 17 ++ python/testData/psi/WithMissingID.py | 2 - python/testData/psi/WithMissingID.txt | 23 -- python/testData/psi/WithStatement.py | 3 - python/testData/psi/WithStatement.txt | 29 --- python/testData/psi/WithStatement26.py | 2 - python/testData/psi/WithStatement26.txt | 17 -- python/testData/psi/WithStatement31.py | 1 - python/testData/psi/WithStatement31.txt | 27 --- ...tContextExpressionStartsWithParenthesis.py | 11 + ...ContextExpressionStartsWithParenthesis.txt | 80 +++++++ ...mentMultipleWithItemsWithoutParentheses.py | 11 + ...entMultipleWithItemsWithoutParentheses.txt | 150 +++++++++++++ .../WithStatementParenthesizedWithItems.py | 16 ++ .../WithStatementParenthesizedWithItems.txt | 212 ++++++++++++++++++ .../psi/WithStatementRecoveryDanglingComma.py | 5 + .../WithStatementRecoveryDanglingComma.txt | 43 ++++ .../WithStatementRecoveryEmptyParentheses.py | 2 + .../WithStatementRecoveryEmptyParentheses.txt | 13 ++ ...hStatementRecoveryIncompleteParentheses.py | 10 + ...StatementRecoveryIncompleteParentheses.txt | 101 +++++++++ .../psi/WithStatementRecoveryMissingAsName.py | 6 + .../WithStatementRecoveryMissingAsName.txt | 73 ++++++ .../psi/WithStatementRecoveryMissingColon.py | 10 + .../psi/WithStatementRecoveryMissingColon.txt | 100 +++++++++ .../psi/WithStatementRecoveryNoWithItems.py | 2 + .../psi/WithStatementRecoveryNoWithItems.txt | 10 + .../WithStatementWithItemsOwnParentheses.py | 14 ++ .../WithStatementWithItemsOwnParentheses.txt | 175 +++++++++++++++ .../com/jetbrains/python/PyEditingTest.java | 115 ++++++++++ .../com/jetbrains/python/PyFormatterTest.java | 22 ++ .../jetbrains/python/PySmartEnterTest.java | 29 +++ .../PyCompatibilityInspectionTest.java | 5 + .../python/parsing/PythonParsingTest.java | 63 ++++-- 60 files changed, 1581 insertions(+), 164 deletions(-) create mode 100644 python/testData/codeInsight/smartEnter/withExpressionMissingNoSpaceAfterWithKeyword.py create mode 100644 python/testData/codeInsight/smartEnter/withExpressionMissingNoSpaceAfterWithKeyword_after.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsColonMissingAndTargetIncomplete.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsColonMissingAndTargetIncomplete_after.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsFirstTargetIncomplete.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsFirstTargetIncomplete_after.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsLastTargetIncomplete.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsLastTargetIncomplete_after.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsNothingToFix.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsNothingToFix_after.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsOnlyColonMissing.py create mode 100644 python/testData/codeInsight/smartEnter/withParenthesizedWithItemsOnlyColonMissing_after.py create mode 100644 python/testData/formatter/hangingClosingBracketInParenthesizedWithItems.py create mode 100644 python/testData/formatter/hangingClosingBracketInParenthesizedWithItems_after.py create mode 100644 python/testData/formatter/parenthesizedWithItems.py create mode 100644 python/testData/formatter/parenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders.py create mode 100644 python/testData/formatter/parenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders_after.py create mode 100644 python/testData/formatter/parenthesizedWithItemsWrapping.py create mode 100644 python/testData/formatter/parenthesizedWithItemsWrapping_after.py create mode 100644 python/testData/formatter/parenthesizedWithItems_after.py create mode 100644 python/testData/inspections/PyCompatibilityInspection/parenthesizedWithItems.py delete mode 100644 python/testData/psi/WithMissingID.py delete mode 100644 python/testData/psi/WithMissingID.txt delete mode 100644 python/testData/psi/WithStatement.py delete mode 100644 python/testData/psi/WithStatement.txt delete mode 100644 python/testData/psi/WithStatement26.py delete mode 100644 python/testData/psi/WithStatement26.txt delete mode 100644 python/testData/psi/WithStatement31.py delete mode 100644 python/testData/psi/WithStatement31.txt create mode 100644 python/testData/psi/WithStatementContextExpressionStartsWithParenthesis.py create mode 100644 python/testData/psi/WithStatementContextExpressionStartsWithParenthesis.txt create mode 100644 python/testData/psi/WithStatementMultipleWithItemsWithoutParentheses.py create mode 100644 python/testData/psi/WithStatementMultipleWithItemsWithoutParentheses.txt create mode 100644 python/testData/psi/WithStatementParenthesizedWithItems.py create mode 100644 python/testData/psi/WithStatementParenthesizedWithItems.txt create mode 100644 python/testData/psi/WithStatementRecoveryDanglingComma.py create mode 100644 python/testData/psi/WithStatementRecoveryDanglingComma.txt create mode 100644 python/testData/psi/WithStatementRecoveryEmptyParentheses.py create mode 100644 python/testData/psi/WithStatementRecoveryEmptyParentheses.txt create mode 100644 python/testData/psi/WithStatementRecoveryIncompleteParentheses.py create mode 100644 python/testData/psi/WithStatementRecoveryIncompleteParentheses.txt create mode 100644 python/testData/psi/WithStatementRecoveryMissingAsName.py create mode 100644 python/testData/psi/WithStatementRecoveryMissingAsName.txt create mode 100644 python/testData/psi/WithStatementRecoveryMissingColon.py create mode 100644 python/testData/psi/WithStatementRecoveryMissingColon.txt create mode 100644 python/testData/psi/WithStatementRecoveryNoWithItems.py create mode 100644 python/testData/psi/WithStatementRecoveryNoWithItems.txt create mode 100644 python/testData/psi/WithStatementWithItemsOwnParentheses.py create mode 100644 python/testData/psi/WithStatementWithItemsOwnParentheses.txt diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 68201b52b94f..1d3db21206bb 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -806,6 +806,7 @@ INSP.compatibility.old.dict.methods.not.available.in.py3=dict.iterkeys(), dict.i INSP.compatibility.basestring.type.not.available.in.py3=basestring type is not available in Python 3 INSP.compatibility.new.union.syntax.not.available.in.earlier.version=allow writing union types as X | Y INSP.compatibility.feature.support.match.statements=support match statements +INSP.compatibility.feature.support.parenthesized.context.expressions=support parenthesized context expressions # PyUnnecessaryBackslashInspection INSP.NAME.unnecessary.backslash=Unnecessary backslash diff --git a/python/python-psi-impl/src/com/jetbrains/python/formatter/PyBlock.java b/python/python-psi-impl/src/com/jetbrains/python/formatter/PyBlock.java index 26885650d02d..ce1d4cbe2b4b 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/formatter/PyBlock.java +++ b/python/python-psi-impl/src/com/jetbrains/python/formatter/PyBlock.java @@ -12,6 +12,7 @@ import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ArrayUtil; +import com.intellij.util.ObjectUtils; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.PythonCodeStyleService; @@ -74,7 +75,8 @@ public class PyBlock implements ASTBlock { PyElementTypes.FROM_IMPORT_STATEMENT, PyElementTypes.SEQUENCE_PATTERN, PyElementTypes.MAPPING_PATTERN, - PyElementTypes.PATTERN_ARGUMENT_LIST); + PyElementTypes.PATTERN_ARGUMENT_LIST, + PyElementTypes.WITH_STATEMENT); private static final boolean ALIGN_CONDITIONS_WITHOUT_PARENTHESES = false; @@ -456,6 +458,18 @@ public class PyBlock implements ASTBlock { } } + if (parentType == PyElementTypes.WITH_STATEMENT && isInsideWithStatementParentheses(myNode, child)) { + if (needListAlignment(child)) { + childAlignment = getAlignmentForChildren(); + } + else { + childIndent = Indent.getNormalIndent(); + } + if (childType == PyTokenTypes.RPAR && !settings.HANG_CLOSING_BRACKETS) { + childIndent = Indent.getNoneIndent(); + } + } + ASTNode prev = child.getTreePrev(); while (prev != null && prev.getElementType() == TokenType.WHITE_SPACE) { if (prev.textContains('\\') && @@ -475,6 +489,23 @@ public class PyBlock implements ASTBlock { return new PyBlock(this, child, childAlignment, childIndent, childWrap, myContext); } + private static boolean isInsideWithStatementParentheses(@NotNull ASTNode withStatement, @NotNull ASTNode node) { + ASTNode openingParenthesis = withStatement.findChildByType(PyTokenTypes.LPAR); + if (openingParenthesis == null) { + return false; + } + if (node.getStartOffset() < openingParenthesis.getStartOffset()) { + return false; + } + ASTNode closingParenthesis = withStatement.findChildByType(PyTokenTypes.RPAR); + if (closingParenthesis != null) { + return node.getStartOffset() <= closingParenthesis.getStartOffset(); + } + ASTNode afterParentheses = ObjectUtils.chooseNotNull(withStatement.findChildByType(PyTokenTypes.COLON), + withStatement.findChildByType(PyElementTypes.STATEMENT_LIST)); + return afterParentheses == null || node.getStartOffset() < afterParentheses.getStartOffset(); + } + private static boolean isIfCondition(@NotNull ASTNode node) { @NotNull PsiElement element = node.getPsi(); final PyIfPart ifPart = as(element.getParent(), PyIfPart.class); @@ -556,6 +587,9 @@ public class PyBlock implements ASTBlock { if (elem instanceof PyFromImportStatement) { firstChild = ((PyFromImportStatement)elem).getLeftParen(); } + else if (elem instanceof PyWithStatement) { + firstChild = PyPsiUtils.getFirstChildOfType(elem, PyTokenTypes.LPAR); + } else { firstChild = elem.getFirstChild(); } @@ -585,6 +619,10 @@ public class PyBlock implements ASTBlock { final PyExpression value = ((PyKeyValueExpression)firstItem).getValue(); return value != null && hasHangingIndent(value); } + else if (firstItem instanceof PyWithItem) { + PyExpression contextExpression = ((PyWithItem)firstItem).getExpression(); + return contextExpression != null && hasHangingIndent(contextExpression); + } return hasHangingIndent(firstItem); } } @@ -608,6 +646,9 @@ public class PyBlock implements ASTBlock { else if (elem instanceof PyFromImportStatement) { items = ((PyFromImportStatement)elem).getImportElements(); } + else if (elem instanceof PyWithStatement) { + items = ((PyWithStatement)elem).getWithItems(); + } else if (elem instanceof PyParenthesizedExpression) { final PyExpression containedExpression = ((PyParenthesizedExpression)elem).getContainedExpression(); if (containedExpression instanceof PyTupleExpression) { @@ -730,6 +771,9 @@ public class PyBlock implements ASTBlock { return false; } } + if (myNode.getElementType() == PyElementTypes.WITH_STATEMENT) { + return myNode.findChildByType(PyTokenTypes.LPAR) != null && !hasHangingIndent(myNode.getPsi()); + } return myContext.getPySettings().ALIGN_COLLECTIONS_AND_COMPREHENSIONS && !hasHangingIndent(myNode.getPsi()); } @@ -968,6 +1012,21 @@ public class PyBlock implements ASTBlock { final ASTNode prevNode = insertAfterBlock.getNode(); final PsiElement prevElt = prevNode.getPsi(); + // TODO Use the same approach for other list-like constructs + if (myNode.getElementType() == PyElementTypes.WITH_STATEMENT && isInsideWithStatementParentheses(myNode, prevNode)) { + ASTNode openingParenthesis = myNode.findChildByType(PyTokenTypes.LPAR); + for (int i = newChildIndex - 1; i >= 0 ; i--) { + PyBlock prevBlock = mySubBlocks.get(i); + if (prevBlock.myNode == openingParenthesis) { + break; + } + if (prevBlock.getAlignment() != null) { + return new ChildAttributes(Indent.getNormalIndent(), prevBlock.getAlignment()); + } + } + return new ChildAttributes(Indent.getNormalIndent(), null); + } + // stmt lists, parts and definitions should also think for themselves if (prevElt instanceof PyStatementList) { if (dedentAfterLastStatement((PyStatementList)prevElt)) { @@ -1079,6 +1138,9 @@ public class PyBlock implements ASTBlock { return null; } } + if (elem instanceof PyWithStatement && PyPsiUtils.getFirstChildOfType(elem, PyTokenTypes.LPAR) == null) { + return null; + } return getAlignmentForChildren(); } return null; 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 3be1bade2a0a..b7bd4ba3f121 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 @@ -838,27 +838,68 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { private void parseWithStatement(SyntaxTreeBuilder.Marker endMarker) { assertCurrentToken(PyTokenTypes.WITH_KEYWORD); myBuilder.advanceLexer(); - while (true) { - SyntaxTreeBuilder.Marker withItem = myBuilder.mark(); - if (!getExpressionParser().parseSingleExpression(false)) { + if (!parseParenthesizedWithItems()) { + if (!parseWithItems(false)) { myBuilder.error(PyPsiBundle.message("PARSE.expected.expression")); } - if (myBuilder.getTokenType() == PyTokenTypes.AS_KEYWORD) { - myBuilder.advanceLexer(); - if (!getExpressionParser().parseSingleExpression(true)) { - myBuilder.error(PyPsiBundle.message("PARSE.expected.identifier")); - // 'as' is followed by a target - } - } - withItem.done(PyElementTypes.WITH_ITEM); - if (!matchToken(PyTokenTypes.COMMA)) { - break; - } } parseColonAndSuite(); endMarker.done(PyElementTypes.WITH_STATEMENT); } + private boolean parseParenthesizedWithItems() { + if (!atToken(PyTokenTypes.LPAR)) { + return false; + } + final SyntaxTreeBuilder.Marker leftPar = myBuilder.mark(); + nextToken(); + // Reparse empty parentheses as an empty tuple + if (!parseWithItems(true)) { + leftPar.rollbackTo(); + return false; + } + if (!matchToken(PyTokenTypes.RPAR)) { + myBuilder.error(PyPsiBundle.message("PARSE.expected.rpar")); + } + // Reparse something like "(foo()) as bar" or (foo()).bar as a single WithItem + if (!atAnyOfTokens(PyTokenTypes.COLON, PyTokenTypes.STATEMENT_BREAK)) { + leftPar.rollbackTo(); + return false; + } + leftPar.drop(); + return true; + } + + private boolean parseWithItems(boolean insideParentheses) { + if (!parseWithItem()) { + return false; + } + while (matchToken(PyTokenTypes.COMMA)) { + if (!parseWithItem()) { + if (!insideParentheses) { + myBuilder.error(PyPsiBundle.message("PARSE.expected.expression")); + } + break; + } + } + return true; + } + + private boolean parseWithItem() { + SyntaxTreeBuilder.Marker withItem = myBuilder.mark(); + if (!getExpressionParser().parseSingleExpression(false)) { + withItem.drop(); + return false; + } + if (matchToken(PyTokenTypes.AS_KEYWORD)) { + if (!getExpressionParser().parseSingleExpression(true)) { + myBuilder.error(PyPsiBundle.message("PARSE.expected.identifier")); + } + } + withItem.done(PyElementTypes.WITH_ITEM); + return true; + } + private void parseClassDeclaration() { final SyntaxTreeBuilder.Marker classMarker = myBuilder.mark(); parseClassDeclaration(classMarker); diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java index da39ee7ee669..7a2d685fbee6 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/validation/CompatibilityVisitor.java @@ -317,9 +317,33 @@ public abstract class CompatibilityVisitor extends PyAnnotator { @Override public void visitPyWithStatement(@NotNull PyWithStatement node) { super.visitPyWithStatement(node); + checkParenthesizedWithItems(node); checkAsyncKeyword(node); } + private void checkParenthesizedWithItems(@NotNull PyWithStatement node) { + final PsiElement lpar = PyPsiUtils.getFirstChildOfType(node, PyTokenTypes.LPAR); + final PsiElement rpar = PyPsiUtils.getFirstChildOfType(node, PyTokenTypes.RPAR); + if (lpar == null && rpar == null) { + return; + } + // Context expressions such as "(foo(), bar())" or "(foo(),)" are valid in Python < 3.9, but most likely indicate an error anyway + PyWithItem[] withItems = node.getWithItems(); + boolean canBeParsedAsSingleParenthesizedExpression = ( + withItems.length == 1 && + PyPsiUtils.getFirstChildOfType(withItems[0], PyTokenTypes.AS_KEYWORD) == null && + PyPsiUtils.getFirstChildOfType(node, PyTokenTypes.COMMA) == null + ); + if (canBeParsedAsSingleParenthesizedExpression) { + return; + } + for (PsiElement parenthesis : ContainerUtil.packNullables(lpar, rpar)) { + registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON39) && registerForLanguageLevel(level), + PyPsiBundle.message("INSP.compatibility.feature.support.parenthesized.context.expressions"), + parenthesis); + } + } + @Override public void visitPyForStatement(@NotNull PyForStatement node) { super.visitPyForStatement(node); @@ -690,8 +714,8 @@ public abstract class CompatibilityVisitor extends PyAnnotator { @Override public void visitPyMatchStatement(@NotNull PyMatchStatement matchStatement) { - registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON310), - PyPsiBundle.message("INSP.compatibility.feature.support.match.statements"), + registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON310), + PyPsiBundle.message("INSP.compatibility.feature.support.match.statements"), matchStatement.getFirstChild()); } diff --git a/python/src/com/jetbrains/python/codeInsight/editorActions/smartEnter/fixers/PyWithFixer.java b/python/src/com/jetbrains/python/codeInsight/editorActions/smartEnter/fixers/PyWithFixer.java index 618f785b9d17..6754b616e5cb 100644 --- a/python/src/com/jetbrains/python/codeInsight/editorActions/smartEnter/fixers/PyWithFixer.java +++ b/python/src/com/jetbrains/python/codeInsight/editorActions/smartEnter/fixers/PyWithFixer.java @@ -18,11 +18,12 @@ package com.jetbrains.python.codeInsight.editorActions.smartEnter.fixers; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.IncorrectOperationException; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.codeInsight.editorActions.smartEnter.PySmartEnterProcessor; -import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.psi.PyWithItem; import com.jetbrains.python.psi.PyWithStatement; import com.jetbrains.python.psi.impl.PyPsiUtils; @@ -38,34 +39,38 @@ public class PyWithFixer extends PyFixer { @Override public void doApply(@NotNull Editor editor, @NotNull PySmartEnterProcessor processor, @NotNull PyWithStatement withStatement) throws IncorrectOperationException { - final PsiElement colonToken = PyPsiUtils.getFirstChildOfType(withStatement, PyTokenTypes.COLON); final PsiElement withToken = PyPsiUtils.getFirstChildOfType(withStatement, PyTokenTypes.WITH_KEYWORD); + assert withToken != null; final Document document = editor.getDocument(); - if (colonToken == null && withToken != null) { - int insertAt = withToken.getTextRange().getEndOffset(); - String textToInsert = ":"; - final PyWithItem lastItem = ArrayUtil.getLastElement(withStatement.getWithItems()); - if (lastItem == null || lastItem.getExpression() == null) { - textToInsert = " :"; - processor.registerUnresolvedError(insertAt + 1); - } - else { - final PyExpression expression = lastItem.getExpression(); - insertAt = expression.getTextRange().getEndOffset(); - final PsiElement asToken = PyPsiUtils.getFirstChildOfType(lastItem, PyTokenTypes.AS_KEYWORD); - if (asToken != null) { - insertAt = asToken.getTextRange().getEndOffset(); - final PyExpression target = lastItem.getTarget(); - if (target != null) { - insertAt = target.getTextRange().getEndOffset(); - } - else { - textToInsert = " :"; - processor.registerUnresolvedError(insertAt + 1); + final PsiElement colonToken = PyPsiUtils.getFirstChildOfType(withStatement, PyTokenTypes.COLON); + if (colonToken == null) { + PsiElement closingParenthesis = PyPsiUtils.getFirstChildOfType(withStatement, PyTokenTypes.RPAR); + PyWithItem lastWithItem = ArrayUtil.getLastElement(withStatement.getWithItems()); + PsiElement rightmostElement = closingParenthesis != null ? closingParenthesis : + lastWithItem != null ? lastWithItem : + withToken; + document.insertString(rightmostElement.getTextRange().getEndOffset(), ":"); + } + + if (withStatement.getWithItems().length != 0) { + for (PyWithItem withItem : withStatement.getWithItems()) { + final PsiElement asToken = PyPsiUtils.getFirstChildOfType(withItem, PyTokenTypes.AS_KEYWORD); + if (asToken != null && withItem.getTarget() == null) { + int asKeywordEndOffset = asToken.getTextRange().getEndOffset(); + if (!(PsiTreeUtil.nextLeaf(asToken, true) instanceof PsiWhiteSpace)) { + document.insertString(asKeywordEndOffset," "); } + processor.registerUnresolvedError(asKeywordEndOffset + 1); + break; } } - document.insertString(insertAt, textToInsert); + } + else { + int withKeywordEndOffset = withToken.getTextRange().getEndOffset(); + if (!(PsiTreeUtil.nextLeaf(withToken, true) instanceof PsiWhiteSpace)) { + document.insertString(withKeywordEndOffset, " "); + processor.registerUnresolvedError(withKeywordEndOffset + 1); + } } } } diff --git a/python/src/com/jetbrains/python/editor/PythonEnterHandler.java b/python/src/com/jetbrains/python/editor/PythonEnterHandler.java index a69b701513f5..f3c7d64de459 100644 --- a/python/src/com/jetbrains/python/editor/PythonEnterHandler.java +++ b/python/src/com/jetbrains/python/editor/PythonEnterHandler.java @@ -25,6 +25,7 @@ import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.codeInsight.PyCodeInsightSettings; import com.jetbrains.python.documentation.docstrings.*; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyPsiUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -189,7 +190,8 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter { // if we're in middle of typing, it's expected that we will have error elements } - if (inFromImportParentheses(statementBefore, nodeAtCaret.getTextRange().getStartOffset())) { + final int offset = nodeAtCaret.getTextRange().getStartOffset(); + if (inFromImportParentheses(statementBefore, offset) || inWithItemsParentheses(statementBefore, offset)) { return false; } @@ -314,6 +316,15 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter { return false; } + private static boolean inWithItemsParentheses(@NotNull PsiElement statement, int offset) { + if (!(statement instanceof PyWithStatement)) { + return false; + } + + final PsiElement leftParen = PyPsiUtils.getFirstChildOfType(statement, PyTokenTypes.LPAR); + return leftParen != null && offset >= leftParen.getTextRange().getEndOffset(); + } + @Override public Result postProcessEnter(@NotNull PsiFile file, @NotNull Editor editor, diff --git a/python/testData/codeInsight/smartEnter/withExpressionMissingNoSpaceAfterWithKeyword.py b/python/testData/codeInsight/smartEnter/withExpressionMissingNoSpaceAfterWithKeyword.py new file mode 100644 index 000000000000..b0b57d075aac --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withExpressionMissingNoSpaceAfterWithKeyword.py @@ -0,0 +1 @@ +with \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withExpressionMissingNoSpaceAfterWithKeyword_after.py b/python/testData/codeInsight/smartEnter/withExpressionMissingNoSpaceAfterWithKeyword_after.py new file mode 100644 index 000000000000..722e64a456b8 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withExpressionMissingNoSpaceAfterWithKeyword_after.py @@ -0,0 +1 @@ +with : \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsColonMissingAndTargetIncomplete.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsColonMissingAndTargetIncomplete.py new file mode 100644 index 000000000000..b657916aad07 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsColonMissingAndTargetIncomplete.py @@ -0,0 +1 @@ +with (foo() as ) \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsColonMissingAndTargetIncomplete_after.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsColonMissingAndTargetIncomplete_after.py new file mode 100644 index 000000000000..5f71397d1b91 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsColonMissingAndTargetIncomplete_after.py @@ -0,0 +1 @@ +with (foo() as ): \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsFirstTargetIncomplete.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsFirstTargetIncomplete.py new file mode 100644 index 000000000000..b95392d08418 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsFirstTargetIncomplete.py @@ -0,0 +1,2 @@ +with (foo() as, + bar()): \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsFirstTargetIncomplete_after.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsFirstTargetIncomplete_after.py new file mode 100644 index 000000000000..d66ffe5630b1 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsFirstTargetIncomplete_after.py @@ -0,0 +1,2 @@ +with (foo() as , + bar()): \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsLastTargetIncomplete.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsLastTargetIncomplete.py new file mode 100644 index 000000000000..5748324bca9b --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsLastTargetIncomplete.py @@ -0,0 +1,3 @@ +with (foo(), + bar() as): + pass \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsLastTargetIncomplete_after.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsLastTargetIncomplete_after.py new file mode 100644 index 000000000000..3344fbe6003e --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsLastTargetIncomplete_after.py @@ -0,0 +1,3 @@ +with (foo(), + bar() as ): + pass \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsNothingToFix.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsNothingToFix.py new file mode 100644 index 000000000000..1888832260ca --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsNothingToFix.py @@ -0,0 +1,2 @@ +with (foo(), + bar() as baz): \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsNothingToFix_after.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsNothingToFix_after.py new file mode 100644 index 000000000000..a87e87ed5405 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsNothingToFix_after.py @@ -0,0 +1,3 @@ +with (foo(), + bar() as baz): + \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsOnlyColonMissing.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsOnlyColonMissing.py new file mode 100644 index 000000000000..fb3e3180f692 --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsOnlyColonMissing.py @@ -0,0 +1 @@ +with (foo()) \ No newline at end of file diff --git a/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsOnlyColonMissing_after.py b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsOnlyColonMissing_after.py new file mode 100644 index 000000000000..813f4d920b8f --- /dev/null +++ b/python/testData/codeInsight/smartEnter/withParenthesizedWithItemsOnlyColonMissing_after.py @@ -0,0 +1,2 @@ +with (foo()): + \ No newline at end of file diff --git a/python/testData/formatter/hangingClosingBracketInParenthesizedWithItems.py b/python/testData/formatter/hangingClosingBracketInParenthesizedWithItems.py new file mode 100644 index 000000000000..f2f82063da01 --- /dev/null +++ b/python/testData/formatter/hangingClosingBracketInParenthesizedWithItems.py @@ -0,0 +1,6 @@ +with ( + foo(), +bar(), + baz() +): + pass diff --git a/python/testData/formatter/hangingClosingBracketInParenthesizedWithItems_after.py b/python/testData/formatter/hangingClosingBracketInParenthesizedWithItems_after.py new file mode 100644 index 000000000000..571623bb7ee7 --- /dev/null +++ b/python/testData/formatter/hangingClosingBracketInParenthesizedWithItems_after.py @@ -0,0 +1,6 @@ +with ( + foo(), + bar(), + baz() + ): + pass diff --git a/python/testData/formatter/parenthesizedWithItems.py b/python/testData/formatter/parenthesizedWithItems.py new file mode 100644 index 000000000000..715d4ee38768 --- /dev/null +++ b/python/testData/formatter/parenthesizedWithItems.py @@ -0,0 +1,13 @@ +with (foo(), +bar(), + # comment + baz()): + pass + +with ( + # comment + foo(), +bar(), + baz() +): + pass \ No newline at end of file diff --git a/python/testData/formatter/parenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders.py b/python/testData/formatter/parenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders.py new file mode 100644 index 000000000000..4dff1dd67d66 --- /dev/null +++ b/python/testData/formatter/parenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders.py @@ -0,0 +1,26 @@ +if (foo( + 1, + 2 +), foo( + 3, + 4 +)): + pass + +while [foo( + 1, + 2 +), foo( + 3, + 4 +)]: + pass + +with (foo( + 1, + 2 +), foo( + 3, + 4 +)): + pass \ No newline at end of file diff --git a/python/testData/formatter/parenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders_after.py b/python/testData/formatter/parenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders_after.py new file mode 100644 index 000000000000..06d70f813521 --- /dev/null +++ b/python/testData/formatter/parenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders_after.py @@ -0,0 +1,26 @@ +if (foo( + 1, + 2 +), foo( + 3, + 4 +)): + pass + +while [foo( + 1, + 2 +), foo( + 3, + 4 +)]: + pass + +with (foo( + 1, + 2 +), foo( + 3, + 4 +)): + pass diff --git a/python/testData/formatter/parenthesizedWithItemsWrapping.py b/python/testData/formatter/parenthesizedWithItemsWrapping.py new file mode 100644 index 000000000000..563d410f67b3 --- /dev/null +++ b/python/testData/formatter/parenthesizedWithItemsWrapping.py @@ -0,0 +1,3 @@ +with (foo() as bar, + foo() as bar): + pass \ No newline at end of file diff --git a/python/testData/formatter/parenthesizedWithItemsWrapping_after.py b/python/testData/formatter/parenthesizedWithItemsWrapping_after.py new file mode 100644 index 000000000000..2f663a8490d7 --- /dev/null +++ b/python/testData/formatter/parenthesizedWithItemsWrapping_after.py @@ -0,0 +1,3 @@ +with (foo() as bar, + foo() as bar): + pass diff --git a/python/testData/formatter/parenthesizedWithItems_after.py b/python/testData/formatter/parenthesizedWithItems_after.py new file mode 100644 index 000000000000..6d96eabc35ec --- /dev/null +++ b/python/testData/formatter/parenthesizedWithItems_after.py @@ -0,0 +1,13 @@ +with (foo(), + bar(), + # comment + baz()): + pass + +with ( + # comment + foo(), + bar(), + baz() +): + pass diff --git a/python/testData/inspections/PyCompatibilityInspection/parenthesizedWithItems.py b/python/testData/inspections/PyCompatibilityInspection/parenthesizedWithItems.py new file mode 100644 index 000000000000..df63ff3dad18 --- /dev/null +++ b/python/testData/inspections/PyCompatibilityInspection/parenthesizedWithItems.py @@ -0,0 +1,17 @@ +with ( + foo() as baz, + foo() as bar +): + pass + +with (foo()) as baz: + pass + +with (foo()): + pass + +with (foo(),): + pass + +with (foo(), bar()): + pass diff --git a/python/testData/psi/WithMissingID.py b/python/testData/psi/WithMissingID.py deleted file mode 100644 index 2cee86f42b45..000000000000 --- a/python/testData/psi/WithMissingID.py +++ /dev/null @@ -1,2 +0,0 @@ -with open("") as : - pass \ No newline at end of file diff --git a/python/testData/psi/WithMissingID.txt b/python/testData/psi/WithMissingID.txt deleted file mode 100644 index afd37e77269e..000000000000 --- a/python/testData/psi/WithMissingID.txt +++ /dev/null @@ -1,23 +0,0 @@ -PyFile:WithMissingID.py - PyWithStatement - PsiElement(Py:WITH_KEYWORD)('with') - PsiWhiteSpace(' ') - PyWithItem - PyCallExpression: open - PyReferenceExpression: open - PsiElement(Py:IDENTIFIER)('open') - PyArgumentList - PsiElement(Py:LPAR)('(') - PyStringLiteralExpression: - PsiElement(Py:SINGLE_QUOTED_STRING)('""') - PsiElement(Py:RPAR)(')') - PsiWhiteSpace(' ') - PsiElement(Py:AS_KEYWORD)('as') - PsiErrorElement:Identifier expected - - PsiWhiteSpace(' ') - PsiElement(Py:COLON)(':') - PsiWhiteSpace('\n ') - PyStatementList - PyPassStatement - PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatement.py b/python/testData/psi/WithStatement.py deleted file mode 100644 index 91978f7bcc1e..000000000000 --- a/python/testData/psi/WithStatement.py +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import with_statement -with x as y: - pass \ No newline at end of file diff --git a/python/testData/psi/WithStatement.txt b/python/testData/psi/WithStatement.txt deleted file mode 100644 index a5257b83520d..000000000000 --- a/python/testData/psi/WithStatement.txt +++ /dev/null @@ -1,29 +0,0 @@ -PyFile:WithStatement.py - PyFromImportStatement - PsiElement(Py:FROM_KEYWORD)('from') - PsiWhiteSpace(' ') - PyReferenceExpression: __future__ - PsiElement(Py:IDENTIFIER)('__future__') - PsiWhiteSpace(' ') - PsiElement(Py:IMPORT_KEYWORD)('import') - PsiWhiteSpace(' ') - PyImportElement:with_statement - PyReferenceExpression: with_statement - PsiElement(Py:IDENTIFIER)('with_statement') - PsiWhiteSpace('\n') - PyWithStatement - PsiElement(Py:WITH_KEYWORD)('with') - PsiWhiteSpace(' ') - PyWithItem - PyReferenceExpression: x - PsiElement(Py:IDENTIFIER)('x') - PsiWhiteSpace(' ') - PsiElement(Py:AS_KEYWORD)('as') - PsiWhiteSpace(' ') - PyTargetExpression: y - PsiElement(Py:IDENTIFIER)('y') - PsiElement(Py:COLON)(':') - PsiWhiteSpace('\n ') - PyStatementList - PyPassStatement - PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatement26.py b/python/testData/psi/WithStatement26.py deleted file mode 100644 index 34a2a4a9bd42..000000000000 --- a/python/testData/psi/WithStatement26.py +++ /dev/null @@ -1,2 +0,0 @@ -with x as y: - pass \ No newline at end of file diff --git a/python/testData/psi/WithStatement26.txt b/python/testData/psi/WithStatement26.txt deleted file mode 100644 index 50c7f43a174f..000000000000 --- a/python/testData/psi/WithStatement26.txt +++ /dev/null @@ -1,17 +0,0 @@ -PyFile:WithStatement26.py - PyWithStatement - PsiElement(Py:WITH_KEYWORD)('with') - PsiWhiteSpace(' ') - PyWithItem - PyReferenceExpression: x - PsiElement(Py:IDENTIFIER)('x') - PsiWhiteSpace(' ') - PsiElement(Py:AS_KEYWORD)('as') - PsiWhiteSpace(' ') - PyTargetExpression: y - PsiElement(Py:IDENTIFIER)('y') - PsiElement(Py:COLON)(':') - PsiWhiteSpace('\n ') - PyStatementList - PyPassStatement - PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatement31.py b/python/testData/psi/WithStatement31.py deleted file mode 100644 index 08c497e8c7c9..000000000000 --- a/python/testData/psi/WithStatement31.py +++ /dev/null @@ -1 +0,0 @@ -with x as a, y as b: pass diff --git a/python/testData/psi/WithStatement31.txt b/python/testData/psi/WithStatement31.txt deleted file mode 100644 index bb807e6841d3..000000000000 --- a/python/testData/psi/WithStatement31.txt +++ /dev/null @@ -1,27 +0,0 @@ -PyFile:WithStatement31.py - PyWithStatement - PsiElement(Py:WITH_KEYWORD)('with') - PsiWhiteSpace(' ') - PyWithItem - PyReferenceExpression: x - PsiElement(Py:IDENTIFIER)('x') - PsiWhiteSpace(' ') - PsiElement(Py:AS_KEYWORD)('as') - PsiWhiteSpace(' ') - PyTargetExpression: a - PsiElement(Py:IDENTIFIER)('a') - PsiElement(Py:COMMA)(',') - PsiWhiteSpace(' ') - PyWithItem - PyReferenceExpression: y - PsiElement(Py:IDENTIFIER)('y') - PsiWhiteSpace(' ') - PsiElement(Py:AS_KEYWORD)('as') - PsiWhiteSpace(' ') - PyTargetExpression: b - PsiElement(Py:IDENTIFIER)('b') - PsiElement(Py:COLON)(':') - PsiWhiteSpace(' ') - PyStatementList - PyPassStatement - PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementContextExpressionStartsWithParenthesis.py b/python/testData/psi/WithStatementContextExpressionStartsWithParenthesis.py new file mode 100644 index 000000000000..e5d3cdfe1c3f --- /dev/null +++ b/python/testData/psi/WithStatementContextExpressionStartsWithParenthesis.py @@ -0,0 +1,11 @@ +with (foo).bar: + pass + +with (foo)(bar): + pass + +with (foo)[bar]: + pass + +with (foo) | bar: + pass diff --git a/python/testData/psi/WithStatementContextExpressionStartsWithParenthesis.txt b/python/testData/psi/WithStatementContextExpressionStartsWithParenthesis.txt new file mode 100644 index 000000000000..5a1667ec54d2 --- /dev/null +++ b/python/testData/psi/WithStatementContextExpressionStartsWithParenthesis.txt @@ -0,0 +1,80 @@ +PyFile:WithStatementContextExpressionStartsWithParenthesis.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyReferenceExpression: bar + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PsiElement(Py:RPAR)(')') + PsiElement(Py:DOT)('.') + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PsiElement(Py:RPAR)(')') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyReferenceExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PySubscriptionExpression + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PsiElement(Py:RPAR)(')') + PsiElement(Py:LBRACKET)('[') + PyReferenceExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:RBRACKET)(']') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyBinaryExpression + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:OR)('|') + PsiWhiteSpace(' ') + PyReferenceExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementMultipleWithItemsWithoutParentheses.py b/python/testData/psi/WithStatementMultipleWithItemsWithoutParentheses.py new file mode 100644 index 000000000000..4ca293a0da56 --- /dev/null +++ b/python/testData/psi/WithStatementMultipleWithItemsWithoutParentheses.py @@ -0,0 +1,11 @@ +with foo(), foo(): + pass +with foo(), \ + foo(): + pass +with foo() as bar, foo(): + pass +with foo(), foo() as bar(): + pass +with foo() as bar, foo() as bar: + pass \ No newline at end of file diff --git a/python/testData/psi/WithStatementMultipleWithItemsWithoutParentheses.txt b/python/testData/psi/WithStatementMultipleWithItemsWithoutParentheses.txt new file mode 100644 index 000000000000..89e6966494b5 --- /dev/null +++ b/python/testData/psi/WithStatementMultipleWithItemsWithoutParentheses.txt @@ -0,0 +1,150 @@ +PyFile:WithStatementMultipleWithItemsWithoutParentheses.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' \') + PsiWhiteSpace('\n ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyCallExpression: bar + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementParenthesizedWithItems.py b/python/testData/psi/WithStatementParenthesizedWithItems.py new file mode 100644 index 000000000000..1f9e7bb79427 --- /dev/null +++ b/python/testData/psi/WithStatementParenthesizedWithItems.py @@ -0,0 +1,16 @@ +with (foo()): + pass +with (foo(),): + pass +with (foo() as bar): + pass +with (foo() as bar,): + pass +with (foo(), foo()): + pass +with (foo() as bar, foo()): + pass +with (foo(), foo() as bar): + pass +with (foo() as bar, foo() as baz): + pass \ No newline at end of file diff --git a/python/testData/psi/WithStatementParenthesizedWithItems.txt b/python/testData/psi/WithStatementParenthesizedWithItems.txt new file mode 100644 index 000000000000..9ce706510ff4 --- /dev/null +++ b/python/testData/psi/WithStatementParenthesizedWithItems.txt @@ -0,0 +1,212 @@ +PyFile:WithStatementParenthesizedWithItems.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COMMA)(',') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COMMA)(',') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: baz + PsiElement(Py:IDENTIFIER)('baz') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementRecoveryDanglingComma.py b/python/testData/psi/WithStatementRecoveryDanglingComma.py new file mode 100644 index 000000000000..90142d3b41bf --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryDanglingComma.py @@ -0,0 +1,5 @@ +with foo(),: + pass +with foo() as bar,: + pass + diff --git a/python/testData/psi/WithStatementRecoveryDanglingComma.txt b/python/testData/psi/WithStatementRecoveryDanglingComma.txt new file mode 100644 index 000000000000..545f51b940d2 --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryDanglingComma.txt @@ -0,0 +1,43 @@ +PyFile:WithStatementRecoveryDanglingComma.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COMMA)(',') + PsiErrorElement:Expression expected + + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COMMA)(',') + PsiErrorElement:Expression expected + + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementRecoveryEmptyParentheses.py b/python/testData/psi/WithStatementRecoveryEmptyParentheses.py new file mode 100644 index 000000000000..bfa95d68c4a1 --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryEmptyParentheses.py @@ -0,0 +1,2 @@ +with (): + pass diff --git a/python/testData/psi/WithStatementRecoveryEmptyParentheses.txt b/python/testData/psi/WithStatementRecoveryEmptyParentheses.txt new file mode 100644 index 000000000000..590a743836bc --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryEmptyParentheses.txt @@ -0,0 +1,13 @@ +PyFile:WithStatementRecoveryEmptyParentheses.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyTupleExpression + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementRecoveryIncompleteParentheses.py b/python/testData/psi/WithStatementRecoveryIncompleteParentheses.py new file mode 100644 index 000000000000..aa8448907e42 --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryIncompleteParentheses.py @@ -0,0 +1,10 @@ +with (: + pass +with (foo(): + pass +with (foo() as: + pass +with (foo() as bar: + pass +with ((foo() as bar): + pass diff --git a/python/testData/psi/WithStatementRecoveryIncompleteParentheses.txt b/python/testData/psi/WithStatementRecoveryIncompleteParentheses.txt new file mode 100644 index 000000000000..dbc00450dadf --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryIncompleteParentheses.txt @@ -0,0 +1,101 @@ +PyFile:WithStatementRecoveryIncompleteParentheses.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PsiErrorElement:Unexpected expression syntax + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PsiElement(Py:IDENTIFIER)('foo') + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiErrorElement:End of statement expected + + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiErrorElement:Identifier expected + + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiErrorElement:End of statement expected + + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiErrorElement:')' expected + + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiErrorElement:End of statement expected + + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiErrorElement:Unexpected expression syntax + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:RPAR)(')') + PsiErrorElement:')' expected + + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementRecoveryMissingAsName.py b/python/testData/psi/WithStatementRecoveryMissingAsName.py new file mode 100644 index 000000000000..1a881d6fee6d --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryMissingAsName.py @@ -0,0 +1,6 @@ +with foo() as : + pass +with (foo() as ): + pass +with foo() as, foo(): + pass \ No newline at end of file diff --git a/python/testData/psi/WithStatementRecoveryMissingAsName.txt b/python/testData/psi/WithStatementRecoveryMissingAsName.txt new file mode 100644 index 000000000000..4409f73c1da4 --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryMissingAsName.txt @@ -0,0 +1,73 @@ +PyFile:WithStatementRecoveryMissingAsName.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiErrorElement:Identifier expected + + PsiWhiteSpace(' ') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiErrorElement:Identifier expected + + PsiWhiteSpace(' ') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiErrorElement:Identifier expected + + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementRecoveryMissingColon.py b/python/testData/psi/WithStatementRecoveryMissingColon.py new file mode 100644 index 000000000000..b9182203926d --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryMissingColon.py @@ -0,0 +1,10 @@ +with foo() + pass +with (foo()) + pass +with (foo()).bar + pass +with foo() as + pass +with foo() as bar + pass diff --git a/python/testData/psi/WithStatementRecoveryMissingColon.txt b/python/testData/psi/WithStatementRecoveryMissingColon.txt new file mode 100644 index 000000000000..4ec8459a4bcb --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryMissingColon.txt @@ -0,0 +1,100 @@ +PyFile:WithStatementRecoveryMissingColon.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiErrorElement:':' expected + + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiErrorElement:':' expected + + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyReferenceExpression: bar + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:DOT)('.') + PsiElement(Py:IDENTIFIER)('bar') + PsiErrorElement:':' expected + + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiErrorElement:Identifier expected + + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiErrorElement:':' expected + + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementRecoveryNoWithItems.py b/python/testData/psi/WithStatementRecoveryNoWithItems.py new file mode 100644 index 000000000000..f61ff1bed5a3 --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryNoWithItems.py @@ -0,0 +1,2 @@ +with: + pass \ No newline at end of file diff --git a/python/testData/psi/WithStatementRecoveryNoWithItems.txt b/python/testData/psi/WithStatementRecoveryNoWithItems.txt new file mode 100644 index 000000000000..5eeaf5ea0341 --- /dev/null +++ b/python/testData/psi/WithStatementRecoveryNoWithItems.txt @@ -0,0 +1,10 @@ +PyFile:WithStatementRecoveryNoWithItems.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiErrorElement:Expression expected + + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/WithStatementWithItemsOwnParentheses.py b/python/testData/psi/WithStatementWithItemsOwnParentheses.py new file mode 100644 index 000000000000..0cbd8cec4afd --- /dev/null +++ b/python/testData/psi/WithStatementWithItemsOwnParentheses.py @@ -0,0 +1,14 @@ +with (foo()): + pass +with ((foo())): + pass +with (foo()), (foo()): + pass +with (foo()) as bar: + pass +with ((foo()) as bar): + pass +with (bar := foo()): + pass +with (bar := foo()) as bar: + pass \ No newline at end of file diff --git a/python/testData/psi/WithStatementWithItemsOwnParentheses.txt b/python/testData/psi/WithStatementWithItemsOwnParentheses.txt new file mode 100644 index 000000000000..317377243558 --- /dev/null +++ b/python/testData/psi/WithStatementWithItemsOwnParentheses.txt @@ -0,0 +1,175 @@ +PyFile:WithStatementWithItemsOwnParentheses.py + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PsiElement(Py:LPAR)('(') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyAssignmentExpression + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiWhiteSpace(' ') + PsiElement(Py:COLONEQ)(':=') + PsiWhiteSpace(' ') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyWithStatement + PsiElement(Py:WITH_KEYWORD)('with') + PsiWhiteSpace(' ') + PyWithItem + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyAssignmentExpression + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiWhiteSpace(' ') + PsiElement(Py:COLONEQ)(':=') + PsiWhiteSpace(' ') + PyCallExpression: foo + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PyArgumentList + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:AS_KEYWORD)('as') + PsiWhiteSpace(' ') + PyTargetExpression: bar + PsiElement(Py:IDENTIFIER)('bar') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyEditingTest.java b/python/testSrc/com/jetbrains/python/PyEditingTest.java index 6c7c4afde035..1d0c04a80ce1 100644 --- a/python/testSrc/com/jetbrains/python/PyEditingTest.java +++ b/python/testSrc/com/jetbrains/python/PyEditingTest.java @@ -897,6 +897,121 @@ public class PyEditingTest extends PyTestCase { doTypingTest('\n'); } + // PY-42200 + public void testParenthesizedWithItemsEnterBeforeExistingItem() { + doTestEnter("with (foo() as baz,\n" + + " foo() as bar\n" + + "):\n" + + " pass", + "with (\n" + + " foo() as baz,\n" + + " foo() as bar\n" + + "):\n" + + " pass"); + + doTestEnter("with (\n" + + " foo() as baz,foo() as bar\n" + + "):\n" + + " pass", + "with (\n" + + " foo() as baz,\n" + + " foo() as bar\n" + + "):\n" + + " pass"); + + doTestEnter("with (foo() as baz,foo() as bar):\n" + + " pass", + "with (foo() as baz,\n" + + " foo() as bar):\n" + + " pass"); + } + + // PY-42200 + public void testParenthesizedWithItemsEnterBeforeClosingParenthesis() { + doTestEnter("with (foo(), \n" + + " foo()):\n" + + " pass", + "with (foo(), \n" + + " foo()\n" + + " ):\n" + + " pass"); + + getPythonCodeStyleSettings().HANG_CLOSING_BRACKETS = false; + doTestEnter("with (\n" + + " foo() as baz,\n" + + " foo() as bar):\n" + + " pass", + "with (\n" + + " foo() as baz,\n" + + " foo() as bar\n" + + "):\n" + + " pass"); + + getPythonCodeStyleSettings().HANG_CLOSING_BRACKETS = true; + doTestEnter("with (\n" + + " foo() as baz,\n" + + " foo() as bar):\n" + + " pass", + "with (\n" + + " foo() as baz,\n" + + " foo() as bar\n" + + " ):\n" + + " pass"); + } + + // PY-42200 + public void testParenthesizedWithItemsEnterBeforeNonExistingItem() { + doTestEnter("with (foo(),\n" + + " bar()):\n" + + " pass", + "with (foo(),\n" + + " \n" + + " bar()):\n" + + " pass"); + + doTestEnter("with (\n" + + " foo(),\n" + + " bar()\n" + + "):\n" + + " pass", + "with (\n" + + " foo(),\n" + + " \n" + + " bar()\n" + + "):\n" + + " pass"); + } + + // PY-42200 + public void testParenthesizedWithItemsEnterBeforeComment() { + doTestEnter("with (\n" + + " foo(),# comment\n" + + " bar()\n" + + "):\n" + + " pass", + "with (\n" + + " foo(),\n" + + " # comment\n" + + " bar()\n" + + "):\n" + + " pass"); + + doTestEnter("with (foo(),# comment\n" + + " bar()):\n" + + " pass", + "with (foo(),\n" + + " # comment\n" + + " bar()):\n" + + " pass"); + } + + // PY-42200 + public void testParenthesizedWithItemsEnterBeforeStatementList() { + doTestEnter("with (foo(), foo()):", + "with (foo(), foo()):\n" + + " "); + } + @NotNull private PyCodeStyleSettings getPythonCodeStyleSettings() { return getCodeStyleSettings().getCustomSettings(PyCodeStyleSettings.class); diff --git a/python/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index 34e15261c6d9..8845df941646 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -1217,4 +1217,26 @@ public class PyFormatterTest extends PyTestCase { getPythonCodeStyleSettings().SPACE_AROUND_EQ_IN_KEYWORD_ARGUMENT = true; doTest(); } + + // PY-42200 + public void testParenthesizedWithItems() { + doTest(); + } + + // PY-42200 + public void testHangingClosingBracketInParenthesizedWithItems() { + getPythonCodeStyleSettings().HANG_CLOSING_BRACKETS = true; + doTest(); + } + + // PY-42200 + public void testParenthesizedWithItemsHangingIndentProcessedSimilarlyToCollectionsInStatementHeaders() { + doTest(); + } + + // PY-42200 + public void testParenthesizedWithItemsWrapping() { + getCodeStyleSettings().setRightMargin(PythonLanguage.getInstance(), 20); + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/PySmartEnterTest.java b/python/testSrc/com/jetbrains/python/PySmartEnterTest.java index c0fb8d76af41..6bd4c17b5efa 100644 --- a/python/testSrc/com/jetbrains/python/PySmartEnterTest.java +++ b/python/testSrc/com/jetbrains/python/PySmartEnterTest.java @@ -328,11 +328,40 @@ public class PySmartEnterTest extends PyTestCase { doTest(); } + public void testWithExpressionMissingNoSpaceAfterWithKeyword() { + doTest(); + } + // PY-12877 public void testWithOnlyColonMissing() { doTest(); } + // PY-42200 + public void testWithParenthesizedWithItemsOnlyColonMissing() { + doTest(); + } + + // PY-42200 + public void testWithParenthesizedWithItemsColonMissingAndTargetIncomplete() { + doTest(); + } + + // PY-42200 + public void testWithParenthesizedWithItemsFirstTargetIncomplete() { + doTest(); + } + + // PY-42200 + public void testWithParenthesizedWithItemsLastTargetIncomplete() { + doTest(); + } + + // PY-42200 + public void testWithParenthesizedWithItemsNothingToFix() { + doTest(); + } + // PY-9209 public void testSpaceInsertedAfterHashSignInComment() { doTest(); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java index be3adb7e83a7..839dfa748e26 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java @@ -61,6 +61,11 @@ public class PyCompatibilityInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-42200 + public void testParenthesizedWithItems() { + doTest(LanguageLevel.getLatest()); + } + public void testPrintStatement() { doTest(); } diff --git a/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java b/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java index 0ff087b1a759..30b5f7096c5f 100644 --- a/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java +++ b/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java @@ -105,10 +105,6 @@ public class PythonParsingTest extends ParsingTestCase { doTest(); } - public void testWithStatement() { - doTest(); - } - public void testDecoratedFunction() { doTest(); } @@ -117,10 +113,6 @@ public class PythonParsingTest extends ParsingTestCase { doTest(); } - public void testWithStatement26() { - doTest(LanguageLevel.PYTHON26); - } - public void testPrintAsFunction26() { doTest(LanguageLevel.PYTHON26); } @@ -205,10 +197,6 @@ public class PythonParsingTest extends ParsingTestCase { doTest(); } - public void testWithStatement31() { - doTest(LanguageLevel.PYTHON34); - } - public void testLongString() { doTest(); } @@ -421,10 +409,6 @@ public class PythonParsingTest extends ParsingTestCase { doTest(); } - public void testWithMissingID() { // PY-9853 - doTest(LanguageLevel.PYTHON27); - } - public void testOverIndentedComment() { // PY-1909 doTest(); } @@ -1196,6 +1180,51 @@ public class PythonParsingTest extends ParsingTestCase { doTest(LanguageLevel.getLatest()); } + // PY-42200 + public void testWithStatementParenthesizedWithItems() { + doTest(LanguageLevel.getLatest()); + } + + // PY-43505 + public void testWithStatementMultipleWithItemsWithoutParentheses() { + doTest(LanguageLevel.getLatest()); + } + + // PY-42200 + public void testWithStatementWithItemsOwnParentheses() { + doTest(LanguageLevel.getLatest()); + } + + // PY-42200 + public void testWithStatementContextExpressionStartsWithParenthesis() { + doTest(LanguageLevel.getLatest()); + } + + public void testWithStatementRecoveryDanglingComma() { + doTest(LanguageLevel.getLatest()); + } + + public void testWithStatementRecoveryIncompleteParentheses() { + doTest(LanguageLevel.getLatest()); + } + + public void testWithStatementRecoveryMissingColon() { + doTest(LanguageLevel.getLatest()); + } + + public void testWithStatementRecoveryEmptyParentheses() { + doTest(LanguageLevel.getLatest()); + } + + // PY-9853 + public void testWithStatementRecoveryMissingAsName() { + doTest(LanguageLevel.getLatest()); + } + + public void testWithStatementRecoveryNoWithItems() { + doTest(LanguageLevel.getLatest()); + } + public void doTest() { doTest(LanguageLevel.PYTHON26); } @@ -1227,6 +1256,4 @@ public class PythonParsingTest extends ParsingTestCase { functionToCheck.getStatementList(); //To make sure each function has statement list (does not throw exception) } } - - }