IDEA-CR-63687: PY-41305 Add PEP 614 support

Allows to use any expression as a decorator

GitOrigin-RevId: e92af1ebd2c4d7024971fd4542bfe52640faaa26
This commit is contained in:
Lada Gagina
2020-06-17 00:30:49 +03:00
committed by intellij-monorepo-bot
parent d41cfccb7a
commit 2e617b803c
22 changed files with 205 additions and 61 deletions
@@ -54,4 +54,8 @@ public interface PyDecorator extends PyCallExpression, StubBasedPsiElement<PyDec
@Nullable
QualifiedName getQualifiedName();
/**
* True if its callee name consists only of identifiers and dots, e.g. {@code x.y.z}
*/
boolean hasPlainReferenceCallee();
}
@@ -55,7 +55,7 @@ public class PyArgumentEqualDefaultInspection extends PyInspection {
@Override
public void visitPyCallExpression(final PyCallExpression node) {
if (node.getArgumentList() == null) {
if (node.getParent() instanceof PyDecorator) {
return;
}
final List<PyCallable> callables = node.multiResolveCalleeFunction(getResolveContext());
@@ -66,6 +66,8 @@ public class PyCallingNonCallableInspection extends PyInspection {
}
private void checkCallable(@NotNull PyElement node, @Nullable PyExpression callee) {
if (node.getParent() instanceof PyDecorator) return; //we've already been here
if (callee != null && isCallable(callee, myTypeEvalContext) == Boolean.FALSE) {
final PyType calleeType = myTypeEvalContext.getType(callee);
String message = "Expression is not callable";
@@ -16,7 +16,6 @@
package com.jetbrains.python.parsing;
import com.intellij.lang.SyntaxTreeBuilder;
import com.intellij.lang.WhitespacesBinders;
import com.intellij.psi.tree.IElementType;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyTokenTypes;
@@ -83,14 +82,9 @@ public class FunctionParsing extends Parsing {
while (myBuilder.getTokenType() == PyTokenTypes.AT) {
SyntaxTreeBuilder.Marker decoratorMarker = myBuilder.mark();
myBuilder.advanceLexer();
getStatementParser().parseDottedName();
if (myBuilder.getTokenType() == PyTokenTypes.LPAR) {
getExpressionParser().parseArgumentList();
}
else { // empty arglist node, so we always have it
SyntaxTreeBuilder.Marker argListMarker = myBuilder.mark();
argListMarker.setCustomEdgeTokenBinders(WhitespacesBinders.GREEDY_LEFT_BINDER, null);
argListMarker.done(PyElementTypes.ARGUMENT_LIST);
if (!getExpressionParser().parseSingleExpression(false)) {
myBuilder.error(message("PARSE.expected.expression"));
}
if (atToken(PyTokenTypes.STATEMENT_BREAK)) {
decoratorMarker.done(PyElementTypes.DECORATOR_CALL);
@@ -68,8 +68,13 @@ public class PyDecoratorImpl extends StubBasedPsiElementBase<PyDecoratorStub> im
@Override
public boolean hasArgumentList() {
final ASTNode arglistNode = getNode().findChildByType(PyElementTypes.ARGUMENT_LIST);
return (arglistNode != null) && (arglistNode.findChildByType(PyTokenTypes.LPAR) != null);
return findChildByClass(PyCallExpression.class) != null;
}
@Override
public PyArgumentList getArgumentList() {
final PyCallExpression callExpr = findChildByClass(PyCallExpression.class);
return callExpr != null ? callExpr.getArgumentList() : null;
}
@Override
@@ -80,7 +85,8 @@ public class PyDecoratorImpl extends StubBasedPsiElementBase<PyDecoratorStub> im
return stub.getQualifiedName();
}
else {
final PyReferenceExpression node = PsiTreeUtil.getChildOfType(this, PyReferenceExpression.class);
final PyCallExpression callExpr = findChildByClass(PyCallExpression.class);
final PyReferenceExpression node = PsiTreeUtil.getChildOfType(callExpr != null ? callExpr : this, PyReferenceExpression.class);
if (node != null) {
return node.asQualifiedName();
}
@@ -91,15 +97,8 @@ public class PyDecoratorImpl extends StubBasedPsiElementBase<PyDecoratorStub> im
@Override
@Nullable
public PyExpression getCallee() {
try {
return (PyExpression)getFirstChild().getNextSibling(); // skip the @ before call
}
catch (NullPointerException npe) { // no sibling
return null;
}
catch (ClassCastException cce) { // error node instead
return null;
}
final PyExpression exprAfterAt = findChildByClass(PyExpression.class);
return exprAfterAt instanceof PyCallExpression ? ((PyCallExpression)exprAfterAt).getCallee() : exprAfterAt;
}
@NotNull
@@ -151,4 +150,10 @@ public class PyDecoratorImpl extends StubBasedPsiElementBase<PyDecoratorStub> im
public PyType getType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) {
return PyCallExpressionHelper.getCallType(this, context, key);
}
@Override
public boolean hasPlainReferenceCallee() {
final PyExpression callee = getCallee();
return callee instanceof PyReferenceExpression && ((PyReferenceExpression)callee).asQualifiedName() != null;
}
}
@@ -8,6 +8,7 @@ import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
@@ -740,4 +741,16 @@ public abstract class CompatibilityVisitor extends PyAnnotator {
node);
}
}
@Override
public void visitPyDecoratorList(PyDecoratorList node) {
super.visitPyDecoratorList(node);
for (PyDecorator decorator : node.getDecorators()) {
if (PsiTreeUtil.getChildOfType(decorator, PsiErrorElement.class) == null && !decorator.hasPlainReferenceCallee()) {
registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON39) && registerForLanguageLevel(level),
" not support arbitrary expressions as a decorator", decorator);
}
}
}
}
@@ -83,6 +83,7 @@ public class HighlightingAnnotator extends PyAnnotator {
@Override
public void visitPyCallExpression(PyCallExpression node) {
if (node.getParent() instanceof PyDecorator) return; //if it's in decorator, then we've already highlighted it as a decorator
final PyReferenceExpression callee = as(node.getCallee(), PyReferenceExpression.class);
if (callee != null) {
if (!callee.isQualified() && PyBuiltinCache.isInBuiltins(callee)) {
@@ -21,9 +21,10 @@ import com.intellij.psi.PsiElement;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.highlighting.PyHighlighter;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
* Highlights class definitions, functrion definitions, and decorators.
* User: dcheryasov
@@ -79,9 +80,8 @@ public class PyDefinitionsAnnotator extends PyAnnotator {
final PsiElement atSign = node.getFirstChild();
if (atSign != null) {
addHighlightingAnnotation(atSign, PyHighlighter.PY_DECORATOR);
final PsiElement refExpression = PyPsiUtils.getNextNonWhitespaceSibling(atSign);
if (refExpression != null) {
addHighlightingAnnotation(refExpression, PyHighlighter.PY_DECORATOR);
if (node.hasPlainReferenceCallee()) {
addHighlightingAnnotation(Objects.requireNonNull(node.getCallee()), PyHighlighter.PY_DECORATOR);
}
}
}
@@ -0,0 +1,12 @@
<info descr="PY.DECORATOR">@</info><info descr="PY.DECORATOR">foo.bar</info>
def <info descr="PY.FUNC_DEFINITION">f</info>():
pass
<info descr="PY.DECORATOR">@</info><info descr="PY.DECORATOR">foo</info>(1, 2, 3)
def <info descr="PY.FUNC_DEFINITION">f</info>():
pass
<info descr="PY.DECORATOR">@</info>foo[1].bar
def <info descr="PY.FUNC_DEFINITION">f</info>():
pass
@@ -0,0 +1,9 @@
<warning descr="Python version 2.6, 2.7, 3.4, 3.5, 3.6, 3.7, 3.8 do not support arbitrary expressions as a decorator">@x[0][1]</warning>
@my_decorator
def say_whee():
print("Whee!")
<warning descr="Python version 2.6, 2.7, 3.4, 3.5, 3.6, 3.7, 3.8 do not support arbitrary expressions as a decorator">@foo[0].wrapper</warning>
@foo.bar()
def say_whee():
print("Whee!")
@@ -40,6 +40,6 @@ def unused_inner_function_with_unknown_decorator():
pass
def unused_inner_function_with_incomplete_decorator():
@<EOLError descr="Identifier expected"></EOLError>
@<EOLError descr="expression expected"></EOLError>
def func(): # pass
pass
@@ -17,8 +17,6 @@ PyFile:BadDecoratorNotMethod.py
PsiElement(Py:AT)('@')
PyReferenceExpression: staticmethod
PsiElement(Py:IDENTIFIER)('staticmethod')
PyArgumentList
<empty list>
PsiErrorElement:'@' or 'def' expected
<empty list>
PsiWhiteSpace('\n ')
-4
View File
@@ -5,15 +5,11 @@ PyFile:ClassDecorators.py
PsiElement(Py:AT)('@')
PyReferenceExpression: foo
PsiElement(Py:IDENTIFIER)('foo')
PyArgumentList
<empty list>
PsiWhiteSpace('\n')
PyDecorator: @bar
PsiElement(Py:AT)('@')
PyReferenceExpression: bar
PsiElement(Py:IDENTIFIER)('bar')
PyArgumentList
<empty list>
PsiWhiteSpace('\n')
PsiElement(Py:CLASS_KEYWORD)('class')
PsiWhiteSpace(' ')
@@ -5,8 +5,6 @@ PyFile:CommentAfterDecorator.py
PsiElement(Py:AT)('@')
PyReferenceExpression: uncallable_deco
PsiElement(Py:IDENTIFIER)('uncallable_deco')
PyArgumentList
<empty list>
PsiWhiteSpace(' ')
PsiComment(Py:END_OF_LINE_COMMENT)('# some comment')
PsiWhiteSpace('\n')
+12 -13
View File
@@ -5,8 +5,6 @@ PyFile:DecoratedAsyncDef.py
PsiElement(Py:AT)('@')
PyReferenceExpression: foo
PsiElement(Py:IDENTIFIER)('foo')
PyArgumentList
<empty list>
PsiWhiteSpace('\n')
PsiElement(Py:ASYNC_KEYWORD)('async')
PsiWhiteSpace(' ')
@@ -36,17 +34,18 @@ PyFile:DecoratedAsyncDef.py
PyDecoratorList
PyDecorator: @baz
PsiElement(Py:AT)('@')
PyReferenceExpression: baz
PsiElement(Py:IDENTIFIER)('baz')
PyArgumentList
PsiElement(Py:LPAR)('(')
PyReferenceExpression: x
PsiElement(Py:IDENTIFIER)('x')
PsiElement(Py:COMMA)(',')
PsiWhiteSpace(' ')
PyReferenceExpression: y
PsiElement(Py:IDENTIFIER)('y')
PsiElement(Py:RPAR)(')')
PyCallExpression: baz
PyReferenceExpression: baz
PsiElement(Py:IDENTIFIER)('baz')
PyArgumentList
PsiElement(Py:LPAR)('(')
PyReferenceExpression: x
PsiElement(Py:IDENTIFIER)('x')
PsiElement(Py:COMMA)(',')
PsiWhiteSpace(' ')
PyReferenceExpression: y
PsiElement(Py:IDENTIFIER)('y')
PsiElement(Py:RPAR)(')')
PsiWhiteSpace('\n')
PsiElement(Py:ASYNC_KEYWORD)('async')
PsiWhiteSpace(' ')
+11 -12
View File
@@ -5,21 +5,20 @@ PyFile:DecoratedFunction.py
PsiElement(Py:AT)('@')
PyReferenceExpression: staticmethod
PsiElement(Py:IDENTIFIER)('staticmethod')
PyArgumentList
<empty list>
PsiWhiteSpace('\n')
PyDecorator: @xmlize
PsiElement(Py:AT)('@')
PyReferenceExpression: xmlize
PsiElement(Py:IDENTIFIER)('xmlize')
PyArgumentList
PsiElement(Py:LPAR)('(')
PyKeywordArgumentImpl: node
PsiElement(Py:IDENTIFIER)('node')
PsiElement(Py:EQ)('=')
PyStringLiteralExpression: foo
PsiElement(Py:SINGLE_QUOTED_STRING)('"foo"')
PsiElement(Py:RPAR)(')')
PyCallExpression: xmlize
PyReferenceExpression: xmlize
PsiElement(Py:IDENTIFIER)('xmlize')
PyArgumentList
PsiElement(Py:LPAR)('(')
PyKeywordArgumentImpl: node
PsiElement(Py:IDENTIFIER)('node')
PsiElement(Py:EQ)('=')
PyStringLiteralExpression: foo
PsiElement(Py:SINGLE_QUOTED_STRING)('"foo"')
PsiElement(Py:RPAR)(')')
PsiWhiteSpace('\n')
PsiElement(Py:DEF_KEYWORD)('def')
PsiWhiteSpace(' ')
@@ -0,0 +1,10 @@
@x[0]
@y:=x[0]
@my_decorator
def say_whee():
print("Whee!")
@y,x
def say_whee2():
print("Whee!")
@@ -0,0 +1,89 @@
PyFile:ExpressionsInDecorators.py
PyFunction('say_whee')
PyDecoratorList
PyDecorator: @x[0]
PsiElement(Py:AT)('@')
PySubscriptionExpression
PyReferenceExpression: x
PsiElement(Py:IDENTIFIER)('x')
PsiElement(Py:LBRACKET)('[')
PyNumericLiteralExpression
PsiElement(Py:INTEGER_LITERAL)('0')
PsiElement(Py:RBRACKET)(']')
PsiWhiteSpace('\n')
PyDecorator: @y:=x[0]
PsiElement(Py:AT)('@')
PyAssignmentExpression
PyTargetExpression: y
PsiElement(Py:IDENTIFIER)('y')
PsiElement(Py:COLONEQ)(':=')
PySubscriptionExpression
PyReferenceExpression: x
PsiElement(Py:IDENTIFIER)('x')
PsiElement(Py:LBRACKET)('[')
PyNumericLiteralExpression
PsiElement(Py:INTEGER_LITERAL)('0')
PsiElement(Py:RBRACKET)(']')
PsiWhiteSpace('\n')
PyDecorator: @my_decorator
PsiElement(Py:AT)('@')
PyReferenceExpression: my_decorator
PsiElement(Py:IDENTIFIER)('my_decorator')
PsiWhiteSpace('\n')
PsiElement(Py:DEF_KEYWORD)('def')
PsiWhiteSpace(' ')
PsiElement(Py:IDENTIFIER)('say_whee')
PyParameterList
PsiElement(Py:LPAR)('(')
PsiElement(Py:RPAR)(')')
PsiElement(Py:COLON)(':')
PsiWhiteSpace('\n ')
PyStatementList
PyExpressionStatement
PyCallExpression: print
PyReferenceExpression: print
PsiElement(Py:IDENTIFIER)('print')
PyArgumentList
PsiElement(Py:LPAR)('(')
PyStringLiteralExpression: Whee!
PsiElement(Py:SINGLE_QUOTED_STRING)('"Whee!"')
PsiElement(Py:RPAR)(')')
PsiWhiteSpace('\n\n\n')
PyFunction('null')
PyDecoratorList
PyDecorator: @y
PsiElement(Py:AT)('@')
PyReferenceExpression: y
PsiElement(Py:IDENTIFIER)('y')
PsiErrorElement:Statement break expected
<empty list>
PyParameterList
<empty list>
PyStatementList
<empty list>
PsiElement(Py:COMMA)(',')
PsiErrorElement:Statement expected, found Py:COMMA
<empty list>
PyExpressionStatement
PyReferenceExpression: x
PsiElement(Py:IDENTIFIER)('x')
PsiWhiteSpace('\n')
PyFunction('say_whee2')
PsiElement(Py:DEF_KEYWORD)('def')
PsiWhiteSpace(' ')
PsiElement(Py:IDENTIFIER)('say_whee2')
PyParameterList
PsiElement(Py:LPAR)('(')
PsiElement(Py:RPAR)(')')
PsiElement(Py:COLON)(':')
PsiWhiteSpace('\n ')
PyStatementList
PyExpressionStatement
PyCallExpression: print
PyReferenceExpression: print
PsiElement(Py:IDENTIFIER)('print')
PyArgumentList
PsiElement(Py:LPAR)('(')
PyStringLiteralExpression: Whee!
PsiElement(Py:SINGLE_QUOTED_STRING)('"Whee!"')
PsiElement(Py:RPAR)(')')
@@ -45,7 +45,7 @@ public class PyDecoratorTest extends PyTestCase {
}
public void testDecoParamCall() {
PsiElement targetElement = find().getParent();
PsiElement targetElement = find().getParent().getParent(); //first parent is PyCallExpression
assertTrue(targetElement instanceof PyDecorator);
PyDecorator deco = (PyDecorator)targetElement;
PyFunction decofun = deco.getTarget();
@@ -369,6 +369,11 @@ public class PythonHighlightingTest extends PyTestCase {
doTest(true, true);
}
// PY-41305
public void testExpressionAsDecorator() {
runWithLanguageLevel(LanguageLevel.getLatest(), this::doTest);
}
// PY-25381
public void testBuiltinDecorator() {
doTest(true, true);
@@ -257,6 +257,11 @@ public class PyCompatibilityInspectionTest extends PyInspectionTestCase {
doTest(LanguageLevel.PYTHON38);
}
// PY-41305
public void testExpressionInDecorators() {
doTest(LanguageLevel.PYTHON39);
}
private void doTest(@NotNull LanguageLevel level) {
runWithLanguageLevel(level, this::doTest);
}
@@ -936,6 +936,11 @@ public class PythonParsingTest extends ParsingTestCase {
doTest(LanguageLevel.PYTHON36);
}
// PY-41305
public void testExpressionsInDecorators() {
doTest(LanguageLevel.getLatest());
}
public void doTest() {
doTest(LanguageLevel.PYTHON26);
}