mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-13 15:52:01 +07:00
PY-84077 - Support PEP 758 – Allow except and except* expressions without parentheses
- adjust parser to support comma separated list of error classes - add problem annotation and quick fix for missing parentheses - add new tests, adjust old tests GitOrigin-RevId: 545f3597a488f85ba2ff17da0a389f2aed226406
This commit is contained in:
committed by
intellij-monorepo-bot
parent
99917efcba
commit
dcb6914323
@@ -793,26 +793,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper {
|
||||
if (myBuilder.getTokenType() == PyTokenTypes.EXCEPT_KEYWORD) {
|
||||
haveExceptClause = true;
|
||||
while (myBuilder.getTokenType() == PyTokenTypes.EXCEPT_KEYWORD) {
|
||||
final SyntaxTreeBuilder.Marker exceptBlock = myBuilder.mark();
|
||||
myBuilder.advanceLexer();
|
||||
|
||||
boolean star = matchToken(PyTokenTypes.MULT);
|
||||
if (myBuilder.getTokenType() != PyTokenTypes.COLON) {
|
||||
if (!getExpressionParser().parseSingleExpression(false)) {
|
||||
myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
|
||||
}
|
||||
if (myBuilder.getTokenType() == PyTokenTypes.COMMA || myBuilder.getTokenType() == PyTokenTypes.AS_KEYWORD) {
|
||||
myBuilder.advanceLexer();
|
||||
if (!getExpressionParser().parseSingleExpression(true)) {
|
||||
myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (star) {
|
||||
myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
|
||||
}
|
||||
parseColonAndSuite();
|
||||
exceptBlock.done(PyElementTypes.EXCEPT_PART);
|
||||
parseExceptClause();
|
||||
}
|
||||
final SyntaxTreeBuilder.Marker elsePart = myBuilder.mark();
|
||||
if (myBuilder.getTokenType() == PyTokenTypes.ELSE_KEYWORD) {
|
||||
@@ -841,6 +822,45 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper {
|
||||
statement.done(PyElementTypes.TRY_EXCEPT_STATEMENT);
|
||||
}
|
||||
|
||||
private void parseExceptClause() {
|
||||
final SyntaxTreeBuilder.Marker exceptBlock = myBuilder.mark();
|
||||
myBuilder.advanceLexer();
|
||||
|
||||
boolean star = matchToken(PyTokenTypes.MULT);
|
||||
if (myBuilder.getTokenType() != PyTokenTypes.COLON) {
|
||||
if (myContext.getLanguageLevel().isAtLeast(LanguageLevel.PYTHON314)) {
|
||||
// Python 3.14, support PEP-758 syntax: except ImportError, OtherError: ...
|
||||
if (!getExpressionParser().parseExpressionOptional(false)) {
|
||||
myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
|
||||
}
|
||||
if (myBuilder.getTokenType() == PyTokenTypes.AS_KEYWORD) {
|
||||
myBuilder.advanceLexer();
|
||||
if (!getExpressionParser().parseSingleExpression(true)) {
|
||||
myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Python 2, support syntax: except ImportError, targetE: ...
|
||||
if (!getExpressionParser().parseSingleExpression(false)) {
|
||||
myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
|
||||
}
|
||||
// support Py3K syntax with 'as' to show an error with a quickfix
|
||||
if (myBuilder.getTokenType() == PyTokenTypes.COMMA || myBuilder.getTokenType() == PyTokenTypes.AS_KEYWORD) {
|
||||
myBuilder.advanceLexer();
|
||||
if (!getExpressionParser().parseSingleExpression(true)) {
|
||||
myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (star) {
|
||||
myBuilder.error(PyParsingBundle.message("PARSE.expected.expression"));
|
||||
}
|
||||
parseColonAndSuite();
|
||||
exceptBlock.done(PyElementTypes.EXCEPT_PART);
|
||||
}
|
||||
|
||||
private void parseColonAndSuite() {
|
||||
if (expectColon()) {
|
||||
parseSuite();
|
||||
|
||||
@@ -957,6 +957,10 @@ INSP.NAME.bad.except.clauses.order=Wrong order of 'except' clauses
|
||||
INSP.bad.except.exception.class.already.caught=Exception class ''{0}'' has already been caught
|
||||
INSP.bad.except.superclass.of.exception.class.already.caught=''{0}'', superclass of the exception class ''{1}'', has already been caught
|
||||
|
||||
# PyExceptClauseMissingParenthesesInspection
|
||||
INSP.except.clause.missing.parens=Parentheses are required when specifying a target for multiple exception classes
|
||||
QFIX.except.clause.missing.parens=Wrap exception tuple in parentheses
|
||||
|
||||
#PyGlobalUndefinedInspection
|
||||
INSP.NAME.global.undefined=Global variable is not defined at the module level
|
||||
INSP.global.variable.undefined=Global variable ''{0}'' is undefined at the module level
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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.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 {
|
||||
|
||||
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)
|
||||
val level = LanguageLevel.forElement(exceptPartTuple)
|
||||
val wrapped = generator.createExpressionFromText(level, "(${exceptPartTuple.text})")
|
||||
exceptPartTuple.replace(wrapped)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.jetbrains.python.PyPsiBundle;
|
||||
import com.jetbrains.python.PyTokenTypes;
|
||||
import com.jetbrains.python.inspections.quickfix.WrapExceptTupleInParenthesesQuickFix;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.impl.PyPsiUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -60,12 +61,26 @@ public final class PyTryExceptAnnotator extends PyAnnotator {
|
||||
|
||||
@Override
|
||||
public void visitPyExceptBlock(@NotNull PyExceptPart node) {
|
||||
if (!node.isStar()) return;
|
||||
if (node.isStar()) {
|
||||
var exceptClass = node.getExceptClass();
|
||||
var exceptionGroup = tryGetExceptionGroupInExpression(exceptClass);
|
||||
if (exceptionGroup != null) {
|
||||
getHolder().newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.exception.group.in.star.except")).range(exceptionGroup)
|
||||
.create();
|
||||
}
|
||||
}
|
||||
|
||||
var exceptClass = node.getExceptClass();
|
||||
var exceptionGroup = tryGetExceptionGroupInExpression(exceptClass);
|
||||
if (exceptionGroup != null) {
|
||||
getHolder().newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.exception.group.in.star.except")).range(exceptionGroup).create();
|
||||
// Add PEP-758 Py314+ missing parentheses check: except Error1, Error2 as e:
|
||||
var level = LanguageLevel.forElement(node);
|
||||
if (!level.isPy3K()) return;
|
||||
if (node.getTarget() == null) return;
|
||||
var exceptExpr = node.getExceptClass();
|
||||
if (exceptExpr instanceof PyParenthesizedExpression) return;
|
||||
if (exceptExpr instanceof PyTupleExpression tuple && tuple.getElements().length > 1) {
|
||||
getHolder().newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("INSP.except.clause.missing.parens"))
|
||||
.range(tuple)
|
||||
.withFix(new WrapExceptTupleInParenthesesQuickFix(tuple))
|
||||
.create();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
def f():
|
||||
try:
|
||||
pass
|
||||
except <error descr="Parentheses are required when specifying a target for multiple exception classes">ValueError, TypeError</error> as e:
|
||||
pass
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
pass
|
||||
except ImportError,<caret> AccessError as e:
|
||||
pass
|
||||
@@ -0,0 +1,4 @@
|
||||
try:
|
||||
pass
|
||||
except (ImportError, AccessError) as e:
|
||||
pass
|
||||
@@ -1,9 +1,15 @@
|
||||
# try and friends
|
||||
<stmt>try:
|
||||
p<body>ass
|
||||
e<ex1>xcept ArithmeticError, e:
|
||||
e<ex0>xcept ArithmeticError:
|
||||
pass
|
||||
e<ex2>xcept:
|
||||
e<ex1>xcept ArithmeticError, ImportError:
|
||||
pass
|
||||
e<ex2>xcept ArithmeticError as e2:
|
||||
pass
|
||||
e<ex3>xcept (ArithmeticError, ImportError) as e3:
|
||||
pass
|
||||
e<ex4>xcept:
|
||||
pass
|
||||
e<else>lse:
|
||||
pass
|
||||
|
||||
@@ -144,7 +144,7 @@ public class PyStatementPartsTest extends LightMarkedTestCase {
|
||||
|
||||
public void testTry() {
|
||||
Map<String, PsiElement> marks = loadTest();
|
||||
Assert.assertEquals(6, marks.size());
|
||||
Assert.assertEquals(9, marks.size());
|
||||
|
||||
PsiElement elt = marks.get("<stmt>").getParent().getParent(); // keyword -> part -> stmt
|
||||
Assert.assertTrue(elt instanceof PyTryExceptStatement);
|
||||
@@ -155,13 +155,33 @@ public class PyStatementPartsTest extends LightMarkedTestCase {
|
||||
Assert.assertNotNull(stmt_list);
|
||||
Assert.assertEquals(marks.get("<body>").getParent().getParent(), stmt_list); // keyword -> stmt -> stmt_list
|
||||
|
||||
PyExceptPart exc_part = stmt.getExceptParts()[0];
|
||||
Assert.assertEquals("ArithmeticError", exc_part.getExceptClass().getText());
|
||||
Assert.assertEquals(marks.get("<ex1>").getParent(), exc_part);
|
||||
PyExceptPart exc_part0 = stmt.getExceptParts()[0];
|
||||
Assert.assertEquals("ArithmeticError", exc_part0.getExceptClass().getText());
|
||||
Assert.assertNull(exc_part0.getTarget());
|
||||
Assert.assertEquals(marks.get("<ex0>").getParent(), exc_part0);
|
||||
|
||||
exc_part = (PyExceptPart)marks.get("<ex2>").getParent(); // keyword -> part
|
||||
Assert.assertEquals(stmt.getExceptParts()[1], exc_part);
|
||||
Assert.assertNull(exc_part.getExceptClass());
|
||||
PyExceptPart exc_part1 = stmt.getExceptParts()[1];
|
||||
Assert.assertEquals("ArithmeticError, ImportError", exc_part1.getExceptClass().getText());
|
||||
Assert.assertNull(exc_part1.getTarget());
|
||||
Assert.assertEquals(marks.get("<ex1>").getParent(), exc_part1);
|
||||
|
||||
PyExceptPart exc_part2 = stmt.getExceptParts()[2];
|
||||
Assert.assertEquals("ArithmeticError", exc_part2.getExceptClass().getText());
|
||||
Assert.assertNotNull(exc_part2.getTarget());
|
||||
Assert.assertEquals("e2", exc_part2.getTarget().getText());
|
||||
Assert.assertEquals(marks.get("<ex2>").getParent(), exc_part2);
|
||||
|
||||
PyExceptPart exc_part3 = stmt.getExceptParts()[3];
|
||||
Assert.assertEquals("(ArithmeticError, ImportError)", exc_part3.getExceptClass().getText());
|
||||
Assert.assertNotNull(exc_part3.getTarget());
|
||||
Assert.assertEquals("e3", exc_part3.getTarget().getText());
|
||||
Assert.assertEquals(marks.get("<ex3>").getParent(), exc_part3);
|
||||
|
||||
PyExceptPart exc_part4 = stmt.getExceptParts()[4];
|
||||
Assert.assertNull(exc_part4.getExceptClass());
|
||||
Assert.assertNull(exc_part4.getTarget());
|
||||
Assert.assertEquals(marks.get("<ex4>").getParent(), exc_part4);
|
||||
Assert.assertNull(exc_part4.getExceptClass());
|
||||
|
||||
elt = marks.get("<else>").getParent(); // keyword -> part
|
||||
Assert.assertTrue(elt instanceof PyElsePart);
|
||||
|
||||
@@ -301,6 +301,11 @@ public class PythonHighlightingTest extends PyTestCase {
|
||||
doTest(LanguageLevel.getLatest(), false, false);
|
||||
}
|
||||
|
||||
// PY-84077
|
||||
public void testExceptClauseMissingParentheses() {
|
||||
doTest(LanguageLevel.getLatest(), false, false);
|
||||
}
|
||||
|
||||
// PY-52930
|
||||
public void testContinueBreakReturnInExceptStar() {
|
||||
doTest(LanguageLevel.getLatest(), false, false);
|
||||
|
||||
@@ -84,7 +84,11 @@ public class PyIntentionTest extends PyTestCase {
|
||||
}
|
||||
|
||||
public void testReplaceExceptPart() {
|
||||
doTest(PyPsiBundle.message("INTN.convert.except.to"));
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON35, () -> doTest(PyPsiBundle.message("INTN.convert.except.to")));
|
||||
}
|
||||
|
||||
public void testExceptPartAddMissingParentheses() {
|
||||
doTest(PyPsiBundle.message("QFIX.except.clause.missing.parens"));
|
||||
}
|
||||
|
||||
public void testConvertBuiltins() {
|
||||
|
||||
Reference in New Issue
Block a user