Parse 'typing' types with parameters inside docstrings (PY-16303)

This commit is contained in:
Andrey Vlasovskikh
2015-10-20 21:05:55 +03:00
parent 32b4627905
commit 80595d0e39
5 changed files with 125 additions and 34 deletions
@@ -240,7 +240,7 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
if (unionType != null) {
return unionType;
}
final Ref<PyType> optionalType = getOptionalTypeFromDefaultNone(resolved, context);
final Ref<PyType> 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<PyType> 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<PyType> getClassType(@NotNull PsiElement element, @NotNull TypeEvalContext context) {
if (element instanceof PyTypedElement) {
@@ -293,7 +316,7 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
}
@Nullable
private static Ref<PyType> getOptionalTypeFromDefaultNone(@NotNull PsiElement element, @NotNull TypeEvalContext context) {
private static Ref<PyType> getOptionalType(@NotNull PsiElement element, @NotNull TypeEvalContext context) {
if (element instanceof PySubscriptionExpression) {
final PySubscriptionExpression subscriptionExpr = (PySubscriptionExpression)element;
final PyExpression operand = subscriptionExpr.getOperand();
@@ -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.<TextRange, PyType>emptyMap(), Collections.<PyType, TextRange>emptyMap(),
private static final ParseResult EMPTY_RESULT = new ParseResult(null, null, Collections.<TextRange, PyType>emptyMap(), Collections.<PyType, TextRange>emptyMap(),
Collections.<PyType, PyImportElement>emptyMap());
public static class ParseResult {
@Nullable private PyType myType;
@NotNull private Map<TextRange, ? extends PyType> myTypes;
@NotNull private Map<? extends PyType, TextRange> myFullRanges;
@Nullable private final PsiElement myElement;
@Nullable private final PyType myType;
@NotNull private final Map<TextRange, ? extends PyType> myTypes;
@NotNull private final Map<? extends PyType, TextRange> myFullRanges;
@NotNull private final Map<? extends PyType, PyImportElement> myImports;
ParseResult(@Nullable PyType type, @NotNull Map<TextRange, ? extends PyType> types,
ParseResult(@Nullable PsiElement element, @Nullable PyType type, @NotNull Map<TextRange, ? extends PyType> types,
@NotNull Map<? extends PyType, TextRange> fullRanges,
@NotNull Map<? extends PyType, PyImportElement> 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.<PyType, PyImportElement>of());
ParseResult(@Nullable PsiElement element, @NotNull PyType type, @NotNull TextRange range) {
this(element, type, ImmutableMap.of(range, type), ImmutableMap.of(type, range), ImmutableMap.<PyType, PyImportElement>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<ParseResult> third = value.getSecond();
final PyType firstType = first.getType();
if (firstType instanceof PyClassType) {
final List<PyType> typesInBrackets = new ArrayList<PyType>();
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<PyType> typesInBrackets = new ArrayList<PyType>();
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<PyType> 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<PyType> 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<PyType, PyImportElement> imports = new HashMap<PyType, PyImportElement>();
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<? extends RatedResolveResult> 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<Token<PyElementType>> 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;
@@ -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(<warning descr="Expected type 'Tuple[int, int]', got 'Tuple[int, str]' instead">(1, 'foo')</warning>)
@@ -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());
@@ -83,4 +83,9 @@ public class Py3TypeCheckerInspectionTest extends PyTestCase {
public void testTypingTypeVarWithUnresolvedBound() {
doTest();
}
// PY-16303
public void testTypingTupleInDocstring() {
doTest();
}
}