diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 151e729eeedc..6c36a35ec643 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -803,6 +803,8 @@ INSP.compatibility.new.union.syntax.not.available.in.earlier.version=allow writi INSP.compatibility.feature.support.match.statements=support match statements INSP.compatibility.feature.support.parenthesized.context.expressions=support parenthesized context expressions INSP.compatibility.feature.support.starred.except.part=support except* part +INSP.compatibility.except.clause.different.semantics=Different semantics of comma-separated elements in Python versions {0}. +INSP.compatibility.except.clause.missing.parens=support missing parentheses in except clauses # PyUnnecessaryBackslashInspection INSP.NAME.unnecessary.backslash=Unnecessary backslash diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyCompatibilityInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyCompatibilityInspection.java index 117094699cc6..f14948492cc8 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyCompatibilityInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyCompatibilityInspection.java @@ -29,6 +29,7 @@ import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.JDOMExternalizableStringList; import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.profile.codeInspection.InspectionProjectProfileManager; import com.intellij.psi.*; @@ -39,6 +40,7 @@ import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyPsiBundle; import com.jetbrains.python.PythonUiService; +import com.jetbrains.python.inspections.quickfix.WrapExceptTupleInParenthesesQuickFix; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.types.PyClassType; @@ -279,7 +281,7 @@ public final class PyCompatibilityInspection extends PyInspection { public void visitPyArgumentList(final @NotNull PyArgumentList node) { //PY-5588 if (node.getParent() instanceof PyClass) { final boolean isPython2 = LanguageLevel.forElement(node).isPython2(); - if (isPython2 || myVersionsToProcess.stream().anyMatch(LanguageLevel::isPython2)) { + if (isPython2 || ContainerUtil.exists(myVersionsToProcess, LanguageLevel::isPython2)) { Arrays .stream(node.getArguments()) .filter(PyKeywordArgument.class::isInstance) @@ -296,7 +298,7 @@ public final class PyCompatibilityInspection extends PyInspection { public void visitPyReferenceExpression(@NotNull PyReferenceExpression node) { super.visitPyElement(node); - if (myVersionsToProcess.stream().anyMatch(LanguageLevel::isPy3K)) { + if (ContainerUtil.exists(myVersionsToProcess, LanguageLevel::isPy3K)) { final String nodeText = node.getText(); if (nodeText.endsWith("iteritems") || nodeText.endsWith("iterkeys") || nodeText.endsWith("itervalues")) { @@ -332,6 +334,42 @@ public final class PyCompatibilityInspection extends PyInspection { } } + @Override + public void visitPyExceptBlock(@NotNull PyExceptPart exceptExpr) { + super.visitPyExceptBlock(exceptExpr); + + // TreeSet is used to sort versions + TreeSet allVersions = new TreeSet<>(myVersionsToProcess); // compatible-with versions + allVersions.add(LanguageLevel.forElement(exceptExpr)); // dev version + + boolean hasBelow300 = ContainerUtil.exists(allVersions, level -> level.isOlderThan(LanguageLevel.PYTHON30)); + boolean hasBelow314 = ContainerUtil.exists(allVersions, level -> level.isOlderThan(LanguageLevel.PYTHON314)); + String allVersionsStr = StringUtil.join(allVersions, LanguageLevel::toString, ", "); + + PyExpression exceptClass = exceptExpr.getExceptClass(); + PyExpression target = exceptExpr.getTarget(); + if (exceptClass == null) return; + + if (LanguageLevel.forElement(exceptExpr).isAtLeast(LanguageLevel.PYTHON314)) { // see #StatementParsing.parseExceptPart() + if (exceptClass instanceof PyTupleExpression tuple && tuple.getElements().length > 1) { + if (target != null) { + // see INSP.except.clause.missing.parens + } + else if (hasBelow300) { + // different semantics in versions <300 vs. >=314 + registerProblem(exceptClass, PyPsiBundle.message("INSP.compatibility.except.clause.different.semantics", allVersionsStr)); + } + else if (hasBelow314) { + // we have: `except expr1, expr2` + // versions <314 require parentheses, versions >=314 not (only if the target is specified, but irrelevant here) + LocalQuickFix quickFix = LocalQuickFix.from(new WrapExceptTupleInParenthesesQuickFix(tuple)); + registerForAllMatchingVersions(level -> level.isAtLeast(LanguageLevel.PYTHON30) && level.isOlderThan(LanguageLevel.PYTHON314), + PyPsiBundle.message("INSP.compatibility.except.clause.missing.parens"), exceptClass, quickFix); + } + } + } + } + @Override public void visitPyTargetExpression(@NotNull PyTargetExpression node) { super.visitPyTargetExpression(node); diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/WrapExceptTupleInParenthesesQuickFix.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/WrapExceptTupleInParenthesesQuickFix.kt index 871c4b20956c..df3021a7c28c 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/WrapExceptTupleInParenthesesQuickFix.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/WrapExceptTupleInParenthesesQuickFix.kt @@ -1,27 +1,20 @@ package com.jetbrains.python.inspections.quickfix -import com.intellij.codeInsight.intention.IntentionAction -import com.intellij.codeInspection.util.IntentionName -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiFile +import com.intellij.modcommand.ActionContext +import com.intellij.modcommand.ModPsiUpdater +import com.intellij.modcommand.PsiUpdateModCommandAction import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.psi.PyElementGenerator import com.jetbrains.python.psi.PyTupleExpression -class WrapExceptTupleInParenthesesQuickFix(val exceptPartTuple: PyTupleExpression) : IntentionAction { +class WrapExceptTupleInParenthesesQuickFix(exceptPartTuple: PyTupleExpression) + : PsiUpdateModCommandAction(exceptPartTuple) { override fun getFamilyName(): String = PyPsiBundle.message("QFIX.except.clause.missing.parens") - override fun getText(): @IntentionName String = PyPsiBundle.message("QFIX.except.clause.missing.parens") - - override fun isAvailable(project: Project, editor: Editor?, psiFile: PsiFile?): Boolean = true - - override fun startInWriteAction(): Boolean = true - - override fun invoke(project: Project, editor: Editor?, psiFile: PsiFile?) { - val generator = PyElementGenerator.getInstance(project) + override fun invoke(context: ActionContext, exceptPartTuple: PyTupleExpression, updater: ModPsiUpdater) { + val generator = PyElementGenerator.getInstance(context.project) val level = LanguageLevel.forElement(exceptPartTuple) val wrapped = generator.createExpressionFromText(level, "(${exceptPartTuple.text})") exceptPartTuple.replace(wrapped) diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/PyCompatibilityVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/validation/PyCompatibilityVisitor.java index c4652aaff91e..2e451a0bc0be 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/validation/PyCompatibilityVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/validation/PyCompatibilityVisitor.java @@ -79,7 +79,7 @@ public abstract class PyCompatibilityVisitor extends PyElementVisitor { } if (element != null && ",".equals(element.getText())) { - registerForAllMatchingVersions(level -> level.isPy3K() && registerForLanguageLevel(level), + registerForAllMatchingVersions(level -> level.isPy3K() && level.isOlderThan(LanguageLevel.PYTHON314) && registerForLanguageLevel(level), PyPsiBundle.message("INSP.compatibility.feature.support.this.syntax"), node, new ReplaceExceptPartQuickFix()); diff --git a/python/testData/inspections/PyCompatibilityInspection/exceptBlock.py b/python/testData/inspections/PyCompatibilityInspection/exceptBlock.py index 9c10bf592838..9840245cccff 100644 --- a/python/testData/inspections/PyCompatibilityInspection/exceptBlock.py +++ b/python/testData/inspections/PyCompatibilityInspection/exceptBlock.py @@ -1,4 +1,4 @@ try: do_smth() -except ImportError, ImportWarning: +except ImportError, ImportWarning: do() \ No newline at end of file diff --git a/python/testData/inspections/PyCompatibilityInspection/tryExcept313Against27.py b/python/testData/inspections/PyCompatibilityInspection/tryExcept313Against27.py new file mode 100644 index 000000000000..0181c2d118ac --- /dev/null +++ b/python/testData/inspections/PyCompatibilityInspection/tryExcept313Against27.py @@ -0,0 +1,4 @@ +try: + pass +except IOError, OSError: + pass \ No newline at end of file diff --git a/python/testData/inspections/PyCompatibilityInspection/tryExcept314Against27.py b/python/testData/inspections/PyCompatibilityInspection/tryExcept314Against27.py new file mode 100644 index 000000000000..fb47903ab341 --- /dev/null +++ b/python/testData/inspections/PyCompatibilityInspection/tryExcept314Against27.py @@ -0,0 +1,4 @@ +try: + pass +except IOError, OSError: + pass \ No newline at end of file diff --git a/python/testData/inspections/PyCompatibilityInspection/tryExcept314Against37.py b/python/testData/inspections/PyCompatibilityInspection/tryExcept314Against37.py new file mode 100644 index 000000000000..9d57b5b1c8b5 --- /dev/null +++ b/python/testData/inspections/PyCompatibilityInspection/tryExcept314Against37.py @@ -0,0 +1,4 @@ +try: + pass +except IOError, OSError: + pass \ No newline at end of file diff --git a/python/testData/psi/TryExceptMultipleNoParensPost314.py b/python/testData/psi/TryExceptMultipleNoParensPost314.py new file mode 100644 index 000000000000..12fe9dc9e088 --- /dev/null +++ b/python/testData/psi/TryExceptMultipleNoParensPost314.py @@ -0,0 +1,4 @@ +try: + f = open('myfile.txt') +except IOError, OtherError: # same code as pre 314 + pass diff --git a/python/testData/psi/TryExceptMultipleNoParensPost314.txt b/python/testData/psi/TryExceptMultipleNoParensPost314.txt new file mode 100644 index 000000000000..fbe7c2f15c13 --- /dev/null +++ b/python/testData/psi/TryExceptMultipleNoParensPost314.txt @@ -0,0 +1,39 @@ +PyFile:TryExceptMultipleNoParensPost314.py + PyTryExceptStatement + PyTryPart + PsiElement(Py:TRY_KEYWORD)('try') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyAssignmentStatement + PyTargetExpression: f + PsiElement(Py:IDENTIFIER)('f') + PsiWhiteSpace(' ') + PsiElement(Py:EQ)('=') + PsiWhiteSpace(' ') + PyCallExpression: open + PyReferenceExpression: open + PsiElement(Py:IDENTIFIER)('open') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyStringLiteralExpression: myfile.txt + PsiElement(Py:SINGLE_QUOTED_STRING)(''myfile.txt'') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace('\n') + PyExceptPart + PsiElement(Py:EXCEPT_KEYWORD)('except') + PsiWhiteSpace(' ') + PyTupleExpression + PyReferenceExpression: IOError + PsiElement(Py:IDENTIFIER)('IOError') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyReferenceExpression: OtherError + PsiElement(Py:IDENTIFIER)('OtherError') + PsiElement(Py:COLON)(':') + PsiWhiteSpace(' ') + PsiComment(Py:END_OF_LINE_COMMENT)('# same code as pre 314') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/TryExceptMultipleNoParensPre314.py b/python/testData/psi/TryExceptMultipleNoParensPre314.py new file mode 100644 index 000000000000..3429f23e5d6e --- /dev/null +++ b/python/testData/psi/TryExceptMultipleNoParensPre314.py @@ -0,0 +1,4 @@ +try: + f = open('myfile.txt') +except IOError, OtherError: # same code as post 314 + pass diff --git a/python/testData/psi/TryExceptMultipleNoParensPre314.txt b/python/testData/psi/TryExceptMultipleNoParensPre314.txt new file mode 100644 index 000000000000..35b9981ce3b7 --- /dev/null +++ b/python/testData/psi/TryExceptMultipleNoParensPre314.txt @@ -0,0 +1,38 @@ +PyFile:TryExceptMultipleNoParensPre314.py + PyTryExceptStatement + PyTryPart + PsiElement(Py:TRY_KEYWORD)('try') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyAssignmentStatement + PyTargetExpression: f + PsiElement(Py:IDENTIFIER)('f') + PsiWhiteSpace(' ') + PsiElement(Py:EQ)('=') + PsiWhiteSpace(' ') + PyCallExpression: open + PyReferenceExpression: open + PsiElement(Py:IDENTIFIER)('open') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyStringLiteralExpression: myfile.txt + PsiElement(Py:SINGLE_QUOTED_STRING)(''myfile.txt'') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace('\n') + PyExceptPart + PsiElement(Py:EXCEPT_KEYWORD)('except') + PsiWhiteSpace(' ') + PyReferenceExpression: IOError + PsiElement(Py:IDENTIFIER)('IOError') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyTargetExpression: OtherError + PsiElement(Py:IDENTIFIER)('OtherError') + PsiElement(Py:COLON)(':') + PsiWhiteSpace(' ') + PsiComment(Py:END_OF_LINE_COMMENT)('# same code as post 314') + PsiWhiteSpace('\n ') + PyStatementList + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') \ No newline at end of file diff --git a/python/testData/psi/TryExceptMultipleWithParens.py b/python/testData/psi/TryExceptMultipleWithParens.py new file mode 100644 index 000000000000..8b3528cf7d77 --- /dev/null +++ b/python/testData/psi/TryExceptMultipleWithParens.py @@ -0,0 +1,4 @@ +try: + f = open('myfile.txt') +except (IOError, OtherError): + pass diff --git a/python/testData/psi/TryExceptMultipleWithParens.txt b/python/testData/psi/TryExceptMultipleWithParens.txt new file mode 100644 index 000000000000..585f6cee644a --- /dev/null +++ b/python/testData/psi/TryExceptMultipleWithParens.txt @@ -0,0 +1,40 @@ +PyFile:TryExceptMultipleWithParens.py + PyTryExceptStatement + PyTryPart + PsiElement(Py:TRY_KEYWORD)('try') + PsiElement(Py:COLON)(':') + PsiWhiteSpace('\n ') + PyStatementList + PyAssignmentStatement + PyTargetExpression: f + PsiElement(Py:IDENTIFIER)('f') + PsiWhiteSpace(' ') + PsiElement(Py:EQ)('=') + PsiWhiteSpace(' ') + PyCallExpression: open + PyReferenceExpression: open + PsiElement(Py:IDENTIFIER)('open') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyStringLiteralExpression: myfile.txt + PsiElement(Py:SINGLE_QUOTED_STRING)(''myfile.txt'') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace('\n') + PyExceptPart + PsiElement(Py:EXCEPT_KEYWORD)('except') + PsiWhiteSpace(' ') + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyTupleExpression + PyReferenceExpression: IOError + PsiElement(Py:IDENTIFIER)('IOError') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyReferenceExpression: OtherError + PsiElement(Py:IDENTIFIER)('OtherError') + 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/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java index 7ac423661dba..602efd444811 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyCompatibilityInspectionTest.java @@ -1,12 +1,17 @@ // Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.inspections; +import com.intellij.psi.PsiFile; import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.fixtures.PyInspectionTestCase; import com.jetbrains.python.psi.LanguageLevel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; +import java.util.List; + /** * User : catherine */ @@ -183,6 +188,21 @@ public class PyCompatibilityInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-84077 + public void testTryExcept313Against27() { + testAgainstVersions(LanguageLevel.PYTHON313, LanguageLevel.PYTHON27); + } + + // PY-84077 + public void testTryExcept314Against27() { + testAgainstVersions(LanguageLevel.PYTHON314, LanguageLevel.PYTHON27); + } + + // PY-84077 + public void testTryExcept314Against37() { + testAgainstVersions(LanguageLevel.PYTHON314, LanguageLevel.PYTHON37); + } + // PY-26510 public void testTryFinallyEmptyRaisePy2() { doTest(); @@ -289,6 +309,23 @@ public class PyCompatibilityInspectionTest extends PyInspectionTestCase { runWithLanguageLevel(level, this::doTest); } + void testAgainstVersions(LanguageLevel runLevel, LanguageLevel ... compLevels) { + assertNotNull(compLevels); + List compLevelsList = Arrays.asList(compLevels); + assertNotEmpty(compLevelsList); + + runWithLanguageLevel(runLevel, () -> { + PsiFile currentFile = myFixture.configureByFile(getTestFilePath()); + PyCompatibilityInspection inspection = new PyCompatibilityInspection(); + inspection.ourVersions.clear(); + inspection.ourVersions.addAll(ContainerUtil.map(compLevelsList, l -> l.toPythonVersion())); + myFixture.enableInspections(inspection); + + myFixture.checkHighlighting(isWarning(), isInfo(), isWeakWarning()); + assertSdkRootsNotParsed(currentFile); + }); + } + @NotNull @Override protected Class getInspectionClass() { diff --git a/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java b/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java index 3c78e94146f7..e1288f3d271e 100644 --- a/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java +++ b/python/testSrc/com/jetbrains/python/parsing/PythonParsingTest.java @@ -82,6 +82,30 @@ public class PythonParsingTest extends ParsingTestCase { doTest(); } + public void testTryExceptAs() { // PY-293 + doTest(); + } + + // PY-52930 + public void testTryExceptStarNoExpression() { + doTest(); + } + + // PY-84077 + public void testTryExceptMultipleNoParensPre314() { + doTest(LanguageLevel.PYTHON26); + } + + // PY-84077 + public void testTryExceptMultipleNoParensPost314() { + doTest(LanguageLevel.PYTHON314); + } + + // PY-84077 + public void testTryExceptMultipleWithParens() { + doTest(); + } + public void testTryFinally() { doTest(); } @@ -112,15 +136,6 @@ public class PythonParsingTest extends ParsingTestCase { doTest(); } - public void testTryExceptAs() { // PY-293 - doTest(); - } - - // PY-52930 - public void testTryExceptStarNoExpression() { - doTest(); - } - public void testPrintAsFunction26() { doTest(LanguageLevel.PYTHON26); }