mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-84322 Code compatibility inspection doesn't warn about absent parentheses in except clauses in Python <3.14
- add incompatibility inspections, quickfix, tests - improve quickfix for adding parentheses - add AST tests for PY-84077 GitOrigin-RevId: 2d09a06d4d0b5ae106f556808900573c50f41800
This commit is contained in:
committed by
intellij-monorepo-bot
parent
c1985c7eab
commit
9c2d77a961
@@ -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
|
||||
|
||||
+40
-2
@@ -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<LanguageLevel> 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);
|
||||
|
||||
+7
-14
@@ -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<PyTupleExpression>(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)
|
||||
|
||||
+1
-1
@@ -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());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
try:
|
||||
do_smth()
|
||||
<warning descr="Python versions 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 do not support this syntax">except ImportError, ImportWarning:
|
||||
<warning descr="Python versions 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 do not support this syntax">except ImportError, ImportWarning:
|
||||
do()</warning>
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
pass
|
||||
<error descr="Python version 3.13 does not support this syntax">except IOError, OSError:
|
||||
pass</error>
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
pass
|
||||
except <warning descr="Different semantics of comma-separated elements in Python versions 2.7, 3.14.">IOError, OSError</warning>:
|
||||
pass
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
pass
|
||||
except <warning descr="Python version 3.7 does not support missing parentheses in except clauses">IOError, OSError</warning>:
|
||||
pass
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
f = open('myfile.txt')
|
||||
except IOError, OtherError: # same code as pre 314
|
||||
pass
|
||||
@@ -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')
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
f = open('myfile.txt')
|
||||
except IOError, OtherError: # same code as post 314
|
||||
pass
|
||||
@@ -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')
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
f = open('myfile.txt')
|
||||
except (IOError, OtherError):
|
||||
pass
|
||||
@@ -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')
|
||||
@@ -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<LanguageLevel> 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<? extends PyInspection> getInspectionClass() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user