From 80595d0e39f436c1baa2f61a430c95e87f97522b Mon Sep 17 00:00:00 2001 From: Andrey Vlasovskikh Date: Tue, 20 Oct 2015 21:05:55 +0300 Subject: [PATCH] Parse 'typing' types with parameters inside docstrings (PY-16303) --- .../codeInsight/PyTypingTypeProvider.java | 27 +++++- .../python/psi/types/PyTypeParser.java | 96 ++++++++++++------- .../TypingTupleInDocstring.py | 19 ++++ .../com/jetbrains/python/PyTypingTest.java | 12 +++ .../Py3TypeCheckerInspectionTest.java | 5 + 5 files changed, 125 insertions(+), 34 deletions(-) create mode 100644 python/testData/inspections/PyTypeCheckerInspection/TypingTupleInDocstring.py diff --git a/python/src/com/jetbrains/python/codeInsight/PyTypingTypeProvider.java b/python/src/com/jetbrains/python/codeInsight/PyTypingTypeProvider.java index c1d524bb8f77..f2ba8501238a 100644 --- a/python/src/com/jetbrains/python/codeInsight/PyTypingTypeProvider.java +++ b/python/src/com/jetbrains/python/codeInsight/PyTypingTypeProvider.java @@ -240,7 +240,7 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { if (unionType != null) { return unionType; } - final Ref optionalType = getOptionalTypeFromDefaultNone(resolved, context); + final Ref optionalType = getOptionalType(resolved, context); if (optionalType != null) { return optionalType.get(); } @@ -271,6 +271,29 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { return null; } + @Nullable + public static PyType getType(@NotNull PsiElement resolved, @NotNull List elementTypes) { + final String qualifiedName = getQualifiedName(resolved); + if ("typing.Union".equals(qualifiedName)) { + return PyUnionType.union(elementTypes); + } + if ("typing.Optional".equals(qualifiedName) && elementTypes.size() == 1) { + return PyUnionType.union(elementTypes.get(0), PyNoneType.INSTANCE); + } + if ("typing.Callable".equals(qualifiedName) && elementTypes.size() == 2) { + return new PyCallableTypeImpl(null, elementTypes.get(1)); + } + if ("typing.Tuple".equals(qualifiedName)) { + return PyTupleType.create(resolved, elementTypes.toArray(new PyType[elementTypes.size()])); + } + final PyType builtinCollection = getBuiltinCollection(resolved); + if (builtinCollection instanceof PyClassType) { + final PyClassType classType = (PyClassType)builtinCollection; + return new PyCollectionTypeImpl(classType.getPyClass(), false, elementTypes); + } + return null; + } + @Nullable private static Ref getClassType(@NotNull PsiElement element, @NotNull TypeEvalContext context) { if (element instanceof PyTypedElement) { @@ -293,7 +316,7 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { } @Nullable - private static Ref getOptionalTypeFromDefaultNone(@NotNull PsiElement element, @NotNull TypeEvalContext context) { + private static Ref getOptionalType(@NotNull PsiElement element, @NotNull TypeEvalContext context) { if (element instanceof PySubscriptionExpression) { final PySubscriptionExpression subscriptionExpr = (PySubscriptionExpression)element; final PyExpression operand = subscriptionExpr.getOperand(); diff --git a/python/src/com/jetbrains/python/psi/types/PyTypeParser.java b/python/src/com/jetbrains/python/psi/types/PyTypeParser.java index 740588b8ea10..e516aef231cb 100644 --- a/python/src/com/jetbrains/python/psi/types/PyTypeParser.java +++ b/python/src/com/jetbrains/python/psi/types/PyTypeParser.java @@ -25,6 +25,7 @@ import com.intellij.psi.util.QualifiedName; import com.intellij.util.Function; import com.intellij.util.containers.hash.HashMap; import com.jetbrains.python.PyNames; +import com.jetbrains.python.codeInsight.PyTypingTypeProvider; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.resolve.PyResolveContext; @@ -49,26 +50,33 @@ import static com.jetbrains.python.psi.types.functionalParser.FunctionalParserBa * @author vlan */ public class PyTypeParser { - private static final ParseResult EMPTY_RESULT = new ParseResult(null, Collections.emptyMap(), Collections.emptyMap(), + private static final ParseResult EMPTY_RESULT = new ParseResult(null, null, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap()); public static class ParseResult { - @Nullable private PyType myType; - @NotNull private Map myTypes; - @NotNull private Map myFullRanges; + @Nullable private final PsiElement myElement; + @Nullable private final PyType myType; + @NotNull private final Map myTypes; + @NotNull private final Map myFullRanges; @NotNull private final Map myImports; - ParseResult(@Nullable PyType type, @NotNull Map types, + ParseResult(@Nullable PsiElement element, @Nullable PyType type, @NotNull Map types, @NotNull Map fullRanges, @NotNull Map imports) { + myElement = element; myType = type; myTypes = types; myFullRanges = fullRanges; myImports = imports; } - ParseResult(@NotNull PyType type, @NotNull TextRange range) { - this(type, ImmutableMap.of(range, type), ImmutableMap.of(type, range), ImmutableMap.of()); + ParseResult(@Nullable PsiElement element, @NotNull PyType type, @NotNull TextRange range) { + this(element, type, ImmutableMap.of(range, type), ImmutableMap.of(type, range), ImmutableMap.of()); + } + + @Nullable + private PsiElement getElement() { + return myElement; } @Nullable @@ -101,11 +109,11 @@ public class PyTypeParser { fullRanges.putAll(result.getFullRanges()); imports.putAll(myImports); imports.putAll(result.getImports()); - return new ParseResult(myType, types, fullRanges, imports); + return new ParseResult(myElement, myType, types, fullRanges, imports); } private ParseResult withType(@Nullable PyType type) { - return new ParseResult(type, myTypes, myFullRanges, myImports); + return new ParseResult(myElement, type, myTypes, myFullRanges, myImports); } } @@ -166,10 +174,10 @@ public class PyTypeParser { final ParseResult boundResult = value.getSecond(); if (boundResult != null) { final PyGenericType type = new PyGenericType(name, boundResult.getType()); - final ParseResult result = new ParseResult(type, range); + final ParseResult result = new ParseResult(null, type, range); return result.merge(boundResult).withType(type); } - return new ParseResult(new PyGenericType(name, null), range); + return new ParseResult(null, new PyGenericType(name, null), range); } }) .named("type-parameter"); @@ -190,17 +198,24 @@ public class PyTypeParser { final ParseResult second = firstPair.getSecond(); final List third = value.getSecond(); final PyType firstType = first.getType(); - if (firstType instanceof PyClassType) { - final List typesInBrackets = new ArrayList(); - typesInBrackets.add(second.getType()); - ParseResult result = first; - result = result.merge(second); - for (ParseResult r : third) { - typesInBrackets.add(r.getType()); - result = result.merge(r); + final List typesInBrackets = new ArrayList(); + typesInBrackets.add(second.getType()); + ParseResult result = first; + result = result.merge(second); + for (ParseResult r : third) { + typesInBrackets.add(r.getType()); + result = result.merge(r); + } + final List elementTypes = third.isEmpty() ? Collections.singletonList(second.getType()) : typesInBrackets; + final PsiElement resolved = first.getElement(); + if (resolved != null) { + final PyType typingType = PyTypingTypeProvider.getType(resolved, elementTypes); + if (typingType != null) { + return result.withType(typingType); } - final List elementTypes = third.isEmpty() ? Collections.singletonList(second.getType()) : typesInBrackets; - final PyCollectionTypeImpl type = new PyCollectionTypeImpl(((PyClassType)firstType).getPyClass(), false, elementTypes); + } + if (firstType instanceof PyClassType) { + final PyType type = new PyCollectionTypeImpl(((PyClassType)firstType).getPyClass(), false, elementTypes); return result.withType(type); } return EMPTY_RESULT; @@ -354,6 +369,7 @@ public class PyTypeParser { final Map imports = new HashMap(); PyType type = resolveQualifierType(tokens, pyFile, context, types, fullRanges, imports); + PsiElement resolved = type != null ? getElement(type) : null; if (type != null) { final PyResolveContext resolveContext = PyResolveContext.defaultContext().withTypeEvalContext(context); @@ -365,7 +381,7 @@ public class PyTypeParser { final List results = qualifierType.resolveMember(token.getText().toString(), expression, AccessDirection.READ, resolveContext); if (results != null && !results.isEmpty()) { - final PsiElement resolved = results.get(0).getElement(); + resolved = results.get(0).getElement(); if (resolved instanceof PyTypedElement) { type = context.getType((PyTypedElement)resolved); if (type != null && !allowResolveToType(type)) { @@ -384,7 +400,7 @@ public class PyTypeParser { fullRanges.put(type, TextRange.create(firstRange.getStartOffset(), token.getRange().getEndOffset())); } if (type != null) { - return new ParseResult(type, types, fullRanges, imports); + return new ParseResult(resolved, type, types, fullRanges, imports); } } } @@ -392,6 +408,22 @@ public class PyTypeParser { return EMPTY_RESULT; } + @Nullable + private static PsiElement getElement(@NotNull PyType type) { + if (type instanceof PyModuleType) { + return ((PyModuleType)type).getModule(); + } + else if (type instanceof PyImportedModuleType) { + return ((PyImportedModuleType)type).getImportedModule(); + } + else if (type instanceof PyClassType) { + return ((PyClassType)type).getPyClass(); + } + else { + return null; + } + } + @Nullable private PyType resolveQualifierType(@NotNull List> tokens, @NotNull PyFile file, @@ -478,40 +510,40 @@ public class PyTypeParser { return EMPTY_RESULT; } else if (PyNames.NONE.equals(name)) { - return new ParseResult(PyNoneType.INSTANCE, range); + return new ParseResult(null, PyNoneType.INSTANCE, range); } else if ("integer".equals(name) || ("long".equals(name) && LanguageLevel.forElement(myAnchor).isPy3K())) { final PyClassType type = builtinCache.getIntType(); - return type != null ? new ParseResult(type, range) : EMPTY_RESULT; + return type != null ? new ParseResult(null, type, range) : EMPTY_RESULT; } else if ("string".equals(name)) { final PyType type = builtinCache.getStringType(LanguageLevel.forElement(myAnchor)); - return type != null ? new ParseResult(type, range) : EMPTY_RESULT; + return type != null ? new ParseResult(null, type, range) : EMPTY_RESULT; } else if ("bytestring".equals(name)) { final PyType type = builtinCache.getByteStringType(LanguageLevel.forElement(myAnchor)); - return type != null ? new ParseResult(type, range) : EMPTY_RESULT; + return type != null ? new ParseResult(null, type, range) : EMPTY_RESULT; } else if ("bytes".equals(name)) { final PyClassType type = builtinCache.getBytesType(LanguageLevel.forElement(myAnchor)); - return type != null ? new ParseResult(type, range) : EMPTY_RESULT; + return type != null ? new ParseResult(null, type, range) : EMPTY_RESULT; } else if ("unicode".equals(name)) { final PyClassType type = builtinCache.getUnicodeType(LanguageLevel.forElement(myAnchor)); - return type != null ? new ParseResult(type, range) : EMPTY_RESULT; + return type != null ? new ParseResult(null, type, range) : EMPTY_RESULT; } else if ("boolean".equals(name)) { final PyClassType type = builtinCache.getBoolType(); - return type != null ? new ParseResult(type, range) : EMPTY_RESULT; + return type != null ? new ParseResult(null, type, range) : EMPTY_RESULT; } else if ("dictionary".equals(name)) { final PyClassType type = builtinCache.getDictType(); - return type != null ? new ParseResult(type, range) : EMPTY_RESULT; + return type != null ? new ParseResult(null, type, range) : EMPTY_RESULT; } final PyType builtinType = builtinCache.getObjectType(name); if (builtinType != null) { - return new ParseResult(builtinType, range); + return new ParseResult(null, builtinType, range); } return null; diff --git a/python/testData/inspections/PyTypeCheckerInspection/TypingTupleInDocstring.py b/python/testData/inspections/PyTypeCheckerInspection/TypingTupleInDocstring.py new file mode 100644 index 000000000000..b763df609a5c --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/TypingTupleInDocstring.py @@ -0,0 +1,19 @@ +from typing import Tuple + + +def test1(name: Tuple[int, int]): + pass + + +test1((1, 2)) + + +def test2(name): + """ + :type name: Tuple[int, int] + """ + pass + + +test2((1, 2)) +test2((1, 'foo')) diff --git a/python/testSrc/com/jetbrains/python/PyTypingTest.java b/python/testSrc/com/jetbrains/python/PyTypingTest.java index 2d767ae01891..50cd09d9c7f7 100644 --- a/python/testSrc/com/jetbrains/python/PyTypingTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypingTest.java @@ -365,6 +365,18 @@ public class PyTypingTest extends PyTestCase { "expr = foo()\n"); } + // PY-16303 + public void testUnionInDocstring() { + doTest("Optional[int]", + "from typing import Union\n" + + "\n" + + "def foo(expr):\n" + + " '''\n" + + " :type expr: Union[int, None]\n" + + " '''\n" + + " pass\n"); + } + private void doTestNoInjectedText(@NotNull String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject()); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index 84f2290e8a4a..29ed47b59101 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -83,4 +83,9 @@ public class Py3TypeCheckerInspectionTest extends PyTestCase { public void testTypingTypeVarWithUnresolvedBound() { doTest(); } + + // PY-16303 + public void testTypingTupleInDocstring() { + doTest(); + } }