From a6383951ff101c4e2a7613a3e69622ceef59ca53 Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Wed, 18 Jan 2017 16:49:34 +0300 Subject: [PATCH] PY-22005 Fixed: Show types in parameter hints Append expected argument type to parameter info Details: `Any` is excluded from `Union[Any, ...]`-like types `None` default value is hidden because it will be listed in the type as `Optional[...]` or `Union[..., None, ...]` --- .../python/psi/PyNamedParameter.java | 31 +++++++- .../python/PyParameterInfoHandler.java | 16 ++-- .../documentation/PyTypeModelBuilder.java | 57 ++++++++++---- .../PythonDocumentationProvider.java | 11 +++ .../inspections/PyTypeCheckerInspection.java | 10 +-- .../python/psi/impl/PyNamedParameterImpl.java | 49 +++++++++++- .../python/psi/types/PyTypeChecker.java | 26 +------ .../testData/paramInfo/WithSpecifiedType.py | 7 ++ .../jetbrains/python/PyParameterInfoTest.java | 76 +++++++++++-------- 9 files changed, 196 insertions(+), 87 deletions(-) create mode 100644 python/testData/paramInfo/WithSpecifiedType.py diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyNamedParameter.java b/python/psi-api/src/com/jetbrains/python/psi/PyNamedParameter.java index 48b5e6ec473b..8c9d4bccdf0c 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/PyNamedParameter.java +++ b/python/psi-api/src/com/jetbrains/python/psi/PyNamedParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,10 @@ import com.intellij.psi.PsiNameIdentifierOwner; import com.intellij.psi.PsiNamedElement; import com.intellij.psi.StubBasedPsiElement; import com.jetbrains.python.psi.stubs.PyNamedParameterStub; +import com.jetbrains.python.psi.types.PyType; +import com.jetbrains.python.psi.types.TypeEvalContext; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Represents a named parameter, as opposed to a tuple parameter. @@ -39,10 +42,30 @@ public interface PyNamedParameter extends PyParameter, PsiNamedElement, PsiNameI boolean isKeywordOnly(); /** - * @param includeDefaultValue if true, include the default value after an " = ". - * @return Canonical representation of parameter. Includes asterisks for *param and **param, and name. + * @param includeDefaultValue if true, include the default value after an "=". + * @return canonical representation of parameter. + * Includes asterisks for *param and **param, and name. */ @NotNull - String getRepr(boolean includeDefaultValue); + default String getRepr(boolean includeDefaultValue) { + return getRepr(includeDefaultValue, null); + } + + /** + * @param includeDefaultValue if true, include the default value after an "=". + * @param context context to be used to resolve argument type + * @return canonical representation of parameter. + * Includes asterisks for *param and **param, and name. + * Also includes argument type if {@code context} is not null and resolved type is not unknown. + */ + @NotNull + String getRepr(boolean includeDefaultValue, @Nullable TypeEvalContext context); + + /** + * @param context context to be used to resolve argument type + * @return argument type. Returns element type for *param and value type for **param. + */ + @Nullable + PyType getArgumentType(@NotNull TypeEvalContext context); } diff --git a/python/src/com/jetbrains/python/PyParameterInfoHandler.java b/python/src/com/jetbrains/python/PyParameterInfoHandler.java index 4307c6d6d0fa..54f429490642 100644 --- a/python/src/com/jetbrains/python/PyParameterInfoHandler.java +++ b/python/src/com/jetbrains/python/PyParameterInfoHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -166,7 +166,7 @@ public class PyParameterInfoHandler implements ParameterInfoHandler flags. this includes flags for parens. final Map> hintFlags = new HashMap<>(); - final List hintsList = buildParameterListHint(parameterList, namedParameters, parameterToIndex, hintFlags); + final List hintsList = buildParameterListHint(parameterList, namedParameters, parameterToIndex, hintFlags, typeEvalContext); final int currentParamOffset = context.getCurrentParameterIndex(); // in Python mode, we get an offset here, not an index! @@ -324,15 +324,17 @@ public class PyParameterInfoHandler implements ParameterInfoHandler buildParameterListHint(@NotNull List parameters, @NotNull final List namedParameters, @NotNull final Map parameterToIndex, - @NotNull final Map> hintFlags) { + @NotNull final Map> hintFlags, + @NotNull TypeEvalContext context) { final List hintsList = new ArrayList<>(); ParamHelper.walkDownParamArray( parameters.toArray(new PyParameter[parameters.size()]), @@ -350,7 +352,7 @@ public class PyParameterInfoHandler implements ParameterInfoHandler body, @NotNull PsiElement anchor) { - TypeToBodyWithLinksVisitor visitor = new TypeToBodyWithLinksVisitor(body, anchor); - this.accept(visitor); + final TypeToBodyWithLinksVisitor visitor = new TypeToBodyWithLinksVisitor(body, anchor); + accept(visitor); + } + + @NotNull + public String asDescription() { + final TypeToDescriptionVisitor visitor = new TypeToDescriptionVisitor(); + accept(visitor); + return visitor.getDescription(); } } @@ -65,7 +73,7 @@ public class PyTypeModelBuilder { } @Override - void accept(TypeVisitor visitor) { + void accept(@NotNull TypeVisitor visitor) { visitor.oneOf(this); } } @@ -80,7 +88,7 @@ public class PyTypeModelBuilder { } @Override - void accept(TypeVisitor visitor) { + void accept(@NotNull TypeVisitor visitor) { visitor.collectionOf(this); } } @@ -93,7 +101,7 @@ public class PyTypeModelBuilder { } @Override - void accept(TypeVisitor visitor) { + void accept(@NotNull TypeVisitor visitor) { visitor.name(this.name); } } @@ -106,7 +114,7 @@ public class PyTypeModelBuilder { } @Override - void accept(TypeVisitor visitor) { + void accept(@NotNull TypeVisitor visitor) { visitor.unknown(this); } } @@ -119,7 +127,7 @@ public class PyTypeModelBuilder { } @Override - void accept(TypeVisitor visitor) { + void accept(@NotNull TypeVisitor visitor) { visitor.optional(this); } } @@ -134,7 +142,7 @@ public class PyTypeModelBuilder { } @Override - void accept(TypeVisitor visitor) { + void accept(@NotNull TypeVisitor visitor) { visitor.tuple(this); } } @@ -158,7 +166,7 @@ public class PyTypeModelBuilder { } @Override - void accept(TypeVisitor visitor) { + void accept(@NotNull TypeVisitor visitor) { visitor.function(this); } } @@ -174,7 +182,7 @@ public class PyTypeModelBuilder { } @Override - void accept(TypeVisitor visitor) { + void accept(@NotNull TypeVisitor visitor) { visitor.param(this); } } @@ -359,6 +367,27 @@ public class PyTypeModelBuilder { } } + private static class TypeToDescriptionVisitor extends TypeNameVisitor { + + @NotNull + private final StringBuilder myResult = new StringBuilder(); + + @Override + protected void add(String s) { + myResult.append(s); + } + + @Override + protected void addType(String name) { + add(name); + } + + @NotNull + public String getDescription() { + return myResult.toString(); + } + } + private abstract static class TypeNameVisitor implements TypeVisitor { private int myDepth = 0; private final static int MAX_DEPTH = 6; diff --git a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java index 7c92aa1433b5..52736b30c3af 100644 --- a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java +++ b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java @@ -214,6 +214,17 @@ public class PythonDocumentationProvider extends AbstractDocumentationProvider i buildTypeModel(type, context).toBodyWithLinks(body, anchor); } + /** + * @param type type which description will be calculated + * @param context type evaluation context + * @return more user-friendly description than result of {@link PythonDocumentationProvider#getTypeName(PyType, TypeEvalContext)}. + * Any is excluded from Union[Any, ...]-like types. + */ + @NotNull + public static String getTypeDescription(@Nullable PyType type, @NotNull TypeEvalContext context) { + return buildTypeModel(type, context).asDescription(); + } + @NotNull private static PyTypeModelBuilder.TypeModel buildTypeModel(@Nullable PyType type, @NotNull TypeEvalContext context) { return new PyTypeModelBuilder(context).build(type, true); diff --git a/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java b/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java index 114ad867e10d..7af8821c0375 100644 --- a/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -189,10 +189,8 @@ public class PyTypeCheckerInspection extends PyInspection { problemsSet.add(checkMapping(results.getReceiver(), results.getArguments())); } if (!problemsSet.isEmpty()) { - Map> minProblems = Collections.min( - problemsSet, - (o1, o2) -> o1.size() - o2.size() - ); + final Map> minProblems = Collections.min(problemsSet, + Comparator.comparingInt(Map::size)); for (Map.Entry> entry : minProblems.entrySet()) { registerProblem(entry.getKey(), entry.getValue().getFirst(), entry.getValue().getSecond()); } @@ -222,7 +220,7 @@ public class PyTypeCheckerInspection extends PyInspection { for (Map.Entry entry : mapping.entrySet()) { final PyNamedParameter param = entry.getValue(); final PyExpression arg = entry.getKey(); - final PyType expectedArgType = PyTypeChecker.getExpectedArgumentType(param, myTypeEvalContext); + final PyType expectedArgType = param.getArgumentType(myTypeEvalContext); if (expectedArgType == null) { continue; } diff --git a/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java b/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java index 326cbf8ca852..5b6c6cd1fdfd 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; import com.intellij.util.PlatformIcons; import com.intellij.util.Processor; +import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; @@ -35,6 +36,7 @@ import com.jetbrains.python.PythonDialectsTokenSetProvider; import com.jetbrains.python.codeInsight.PyTypingTypeProvider; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; +import com.jetbrains.python.documentation.PythonDocumentationProvider; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.stubs.PyNamedParameterStub; @@ -166,13 +168,25 @@ public class PyNamedParameterImpl extends PyBaseElementImpl quotes = PyStringLiteralUtil.getQuotes(defaultValue.getText()); @@ -182,9 +196,36 @@ public class PyNamedParameterImpl extends PyBaseElementImpl) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java index 819c89ac53a3..0650215715d5 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -29,6 +29,7 @@ import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.HashSet; import com.jetbrains.python.fixtures.LightMarkedTestCase; +import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.psi.PyArgumentList; import com.jetbrains.python.psi.PyCallExpression; import org.jetbrains.annotations.NotNull; @@ -151,7 +152,7 @@ public class PyParameterInfoTest extends LightMarkedTestCase { public void testKwdArgInClass() { Map marks = loadTest(1); - feignCtrlP(marks.get("").getTextOffset()).check("self, **kw", new String[]{"**kw"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, **kw", new String[]{"**kw"}, new String[]{"self: A, "}); } public void testKwdArgOutOfOrder() { @@ -222,14 +223,14 @@ public class PyParameterInfoTest extends LightMarkedTestCase { public void testSimpleMethod() { Map marks = loadTest(1); - feignCtrlP(marks.get("").getTextOffset()).check("self, a", new String[]{"a"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, a", new String[]{"a"}, new String[]{"self: A, "}); } public void testSimpleClassFunction() { Map marks = loadTest(2); - feignCtrlP(marks.get("").getTextOffset()).check("self, a", new String[]{"self, "}); - feignCtrlP(marks.get("").getTextOffset()).check("self, a", new String[]{"a"}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, a", new String[]{"self: A, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, a", new String[]{"a"}); } public void testReassignedFunction() { @@ -242,65 +243,65 @@ public class PyParameterInfoTest extends LightMarkedTestCase { public void testReassignedInstanceMethod() { Map marks = loadTest(3); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b, c", new String[]{"a, "}, new String[]{"self, "}); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b, c", new String[]{"b, "}, new String[]{"self, "}); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b, c", new String[]{"c"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: Foo, a, b, c", new String[]{"a, "}, new String[]{"self: Foo, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: Foo, a, b, c", new String[]{"b, "}, new String[]{"self: Foo, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: Foo, a, b, c", new String[]{"c"}, new String[]{"self: Foo, "}); } public void testReassignedClassInit() { Map marks = loadTest(2); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b", new String[]{"a, "}, new String[]{"self, "}); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b", new String[]{"b"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: Bar, a, b", new String[]{"a, "}, new String[]{"self: Bar, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: Bar, a, b", new String[]{"b"}, new String[]{"self: Bar, "}); } public void testInheritedClassInit() { Map marks = loadTest(2); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b", new String[]{"a, "}, new String[]{"self, "}); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b", new String[]{"b"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: Bar, a, b", new String[]{"a, "}, new String[]{"self: Bar, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: Bar, a, b", new String[]{"b"}, new String[]{"self: Bar, "}); } public void testRedefinedNewConstructorCall() { Map marks = loadTest(2); - feignCtrlP(marks.get("").getTextOffset()).check("cls, a, b", new String[]{"a, "}, new String[]{"cls, "}); - feignCtrlP(marks.get("").getTextOffset()).check("cls, a, b", new String[]{"b"}, new String[]{"cls, "}); + feignCtrlP(marks.get("").getTextOffset()).check("cls: A, a, b", new String[]{"a, "}, new String[]{"cls: A, "}); + feignCtrlP(marks.get("").getTextOffset()).check("cls: A, a, b", new String[]{"b"}, new String[]{"cls: A, "}); } public void testRedefinedNewDirectCall() { Map marks = loadTest(3); - feignCtrlP(marks.get("").getTextOffset()).check("cls, a, b", new String[]{"cls, "}); - feignCtrlP(marks.get("").getTextOffset()).check("cls, a, b", new String[]{"a, "}); - feignCtrlP(marks.get("").getTextOffset()).check("cls, a, b", new String[]{"b"}); + feignCtrlP(marks.get("").getTextOffset()).check("cls: A, a, b", new String[]{"cls: A, "}); + feignCtrlP(marks.get("").getTextOffset()).check("cls: A, a, b", new String[]{"a, "}); + feignCtrlP(marks.get("").getTextOffset()).check("cls: A, a, b", new String[]{"b"}); } public void testIgnoreNewInOldStyleClass() { Map marks = loadTest(1); - feignCtrlP(marks.get("").getTextOffset()).check("self, one", new String[]{"one"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, one", new String[]{"one"}, new String[]{"self: A, "}); } public void testBoundMethodSimple() { Map marks = loadTest(2); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b", new String[]{"a, "}, new String[]{"self, "}); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b", new String[]{"b"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, a, b", new String[]{"a, "}, new String[]{"self: A, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, a, b", new String[]{"b"}, new String[]{"self: A, "}); } public void testBoundMethodReassigned() { Map marks = loadTest(2); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b", new String[]{"a, "}, new String[]{"self, "}); - feignCtrlP(marks.get("").getTextOffset()).check("self, a, b", new String[]{"b"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, a, b", new String[]{"a, "}, new String[]{"self: A, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: A, a, b", new String[]{"b"}, new String[]{"self: A, "}); } public void testConstructorFactory() { Map marks = loadTest(1); - feignCtrlP(marks.get("").getTextOffset()).check("self, color", new String[]{"color"}, new String[]{"self, "}); + feignCtrlP(marks.get("").getTextOffset()).check("self: Foo, color", new String[]{"color"}, new String[]{"self: Foo, "}); } @@ -385,16 +386,16 @@ public class PyParameterInfoTest extends LightMarkedTestCase { public void testPartialNamed() { Map marks = loadTest(2); - feignCtrlP(marks.get("").getTextOffset()).check("a, b, c=1, d=2, e=3", new String[]{"d=2, "}); - feignCtrlP(marks.get("").getTextOffset()).check("a, b, c=1, d=2, e=3", new String[]{"e=3"}); // no logical next + feignCtrlP(marks.get("").getTextOffset()).check("a, b, c: int=1, d: int=2, e: int=3", new String[]{"d: int=2, "}); + feignCtrlP(marks.get("").getTextOffset()).check("a, b, c: int=1, d: int=2, e: int=3", new String[]{"e: int=3"}); // no logical next } public void testPy3kPastTupleArg() { Map marks = loadTest(4); - feignCtrlP(marks.get("").getTextOffset()).check("*arg, a=1, b=2", new String[]{"*arg, "}); - feignCtrlP(marks.get("").getTextOffset()).check("*arg, a=1, b=2", new String[]{"*arg, "}); - feignCtrlP(marks.get("").getTextOffset()).check("*arg, a=1, b=2", new String[]{"b=2"}); - feignCtrlP(marks.get("").getTextOffset()).check("*arg, a=1, b=2", new String[]{"a=1, "}); + feignCtrlP(marks.get("").getTextOffset()).check("*arg, a: int=1, b: int=2", new String[]{"*arg, "}); + feignCtrlP(marks.get("").getTextOffset()).check("*arg, a: int=1, b: int=2", new String[]{"*arg, "}); + feignCtrlP(marks.get("").getTextOffset()).check("*arg, a: int=1, b: int=2", new String[]{"b: int=2"}); + feignCtrlP(marks.get("").getTextOffset()).check("*arg, a: int=1, b: int=2", new String[]{"a: int=1, "}); } public void testNoArgs() { @@ -409,11 +410,26 @@ public class PyParameterInfoTest extends LightMarkedTestCase { public void testMultilineStringDefault() { final int offset = loadTest(1).get("").getTextOffset(); - feignCtrlP(offset).check("length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'", - new String[]{"allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'"}, + feignCtrlP(offset).check("length: int=12, allowed_chars: str='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'", + new String[]{"allowed_chars: str='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'"}, ArrayUtil.EMPTY_STRING_ARRAY); } + // PY-22005 + public void testWithSpecifiedType() { + myFixture.copyDirectoryToProject("typing", ""); + + runWithLanguageLevel( + LanguageLevel.PYTHON35, + () -> { + final int offset = loadTest(1).get("").getTextOffset(); + final String expectedInfo = "a1: str, a2: Optional[str], a3: Union[str, int, None], a4: int, *args: int, **kwargs: int"; + + feignCtrlP(offset).check(expectedInfo, new String[]{"a1: str, "}); + } + ); + } + /** * Imitates pressing of Ctrl+P; fails if results are not as expected. * @param offset offset of 'cursor' where ^P is pressed.