diff --git a/python/psi-api/src/com/jetbrains/python/psi/types/PyOverridingModuleMembersProvider.java b/python/psi-api/src/com/jetbrains/python/psi/types/PyOverridingModuleMembersProvider.java new file mode 100644 index 000000000000..90869d3c7318 --- /dev/null +++ b/python/psi-api/src/com/jetbrains/python/psi/types/PyOverridingModuleMembersProvider.java @@ -0,0 +1,7 @@ +package com.jetbrains.python.psi.types; + +/** + * @author vlan + */ +public abstract class PyOverridingModuleMembersProvider extends PyModuleMembersProvider { +} diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index cfa88840b3da..7d31ba833638 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -497,6 +497,9 @@ + + + @@ -856,6 +859,10 @@ + + + + diff --git a/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsLineMarkerProvider.java b/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsLineMarkerProvider.java new file mode 100644 index 000000000000..624c67e7ce7a --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsLineMarkerProvider.java @@ -0,0 +1,70 @@ +package com.jetbrains.python.codeInsight.userSkeletons; + +import com.intellij.codeHighlighting.Pass; +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler; +import com.intellij.codeInsight.daemon.LineMarkerInfo; +import com.intellij.codeInsight.daemon.LineMarkerProvider; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.psi.PsiElement; +import com.intellij.util.Function; +import com.intellij.util.PsiNavigateUtil; +import com.jetbrains.python.psi.PyElement; +import com.jetbrains.python.psi.PyFunction; +import com.jetbrains.python.psi.PyTargetExpression; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.event.MouseEvent; +import java.util.Collection; +import java.util.List; + +/** + * @author vlan + */ +public class PyUserSkeletonsLineMarkerProvider implements LineMarkerProvider { + // TODO: Create an icon for a related user skeleton + public static final Icon ICON = AllIcons.Gutter.Unique; + + @Nullable + @Override + public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) { + return null; + } + + @Override + public void collectSlowLineMarkers(@NotNull List elements, @NotNull Collection result) { + for (PsiElement element : elements) { + final PyElement skeleton = getUserSkeleton(element); + if (skeleton != null) { + result.add(new LineMarkerInfo( + element, element.getTextRange(), ICON, Pass.VISIBLE_LINE_MARKERS, + new Function() { + @Override + public String fun(PsiElement e) { + return "Has user skeleton"; + } + }, + new GutterIconNavigationHandler() { + @Override + public void navigate(MouseEvent e, PsiElement elt) { + final PyElement s = getUserSkeleton(elt); + if (s != null) { + PsiNavigateUtil.navigate(s); + } + } + }, + GutterIconRenderer.Alignment.RIGHT)); + } + } + } + + @Nullable + private static PyElement getUserSkeleton(@NotNull PsiElement element) { + if (element instanceof PyFunction || element instanceof PyTargetExpression) { + return PyUserSkeletonsUtil.getUserSkeleton((PyElement)element); + } + return null; + } +} diff --git a/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsModuleMembersProvider.java b/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsModuleMembersProvider.java new file mode 100644 index 000000000000..59c0a79954c4 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsModuleMembersProvider.java @@ -0,0 +1,44 @@ +package com.jetbrains.python.codeInsight.userSkeletons; + +import com.intellij.psi.PsiElement; +import com.jetbrains.python.codeInsight.PyDynamicMember; +import com.jetbrains.python.psi.PyElement; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.types.PyModuleMembersProvider; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author vlan + */ +public class PyUserSkeletonsModuleMembersProvider extends PyModuleMembersProvider { + @Nullable + @Override + public PsiElement resolveMember(PyFile module, String name) { + final PyFile moduleSkeleton = PyUserSkeletonsUtil.getUserSkeleton(module); + if (moduleSkeleton != null) { + return moduleSkeleton.getElementNamed(name); + } + return null; + } + + @Override + protected Collection getMembersByQName(PyFile module, String qName) { + final PyFile moduleSkeleton = PyUserSkeletonsUtil.getUserSkeletonForModuleQName(qName, module); + if (moduleSkeleton != null) { + final List results = new ArrayList(); + for (PyElement element : moduleSkeleton.iterateNames()) { + final String name = element.getName(); + if (name != null) { + results.add(new PyDynamicMember(name, element)); + } + } + return results; + } + return Collections.emptyList(); + } +} diff --git a/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsTypeProvider.java b/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsTypeProvider.java new file mode 100644 index 000000000000..f93b3b22978c --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsTypeProvider.java @@ -0,0 +1,49 @@ +package com.jetbrains.python.codeInsight.userSkeletons; + +import com.intellij.psi.PsiElement; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.types.PyType; +import com.jetbrains.python.psi.types.PyTypeProviderBase; +import com.jetbrains.python.psi.types.TypeEvalContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author vlan + */ +public class PyUserSkeletonsTypeProvider extends PyTypeProviderBase { + @Override + public PyType getParameterType(@NotNull PyNamedParameter param, @NotNull PyFunction func, @NotNull TypeEvalContext context) { + final String name = param.getName(); + if (name != null) { + final PyFunction functionSkeleton = PyUserSkeletonsUtil.getUserSkeleton(func); + if (functionSkeleton != null) { + final PyNamedParameter paramSkeleton = functionSkeleton.getParameterList().findParameterByName(name); + if (paramSkeleton != null) { + return context.getType(paramSkeleton); + } + } + } + return null; + } + + @Override + public PyType getReturnType(@NotNull PyFunction function, @Nullable PyQualifiedExpression callSite, @NotNull TypeEvalContext context) { + final PyFunction functionSkeleton = PyUserSkeletonsUtil.getUserSkeleton(function); + if (functionSkeleton != null) { + return functionSkeleton.getReturnType(context, callSite); + } + return null; + } + + @Override + public PyType getReferenceType(@NotNull PsiElement target, TypeEvalContext context, @Nullable PsiElement anchor) { + if (target instanceof PyTargetExpression) { + final PyTargetExpression targetSkeleton = PyUserSkeletonsUtil.getUserSkeleton((PyTargetExpression)target); + if (targetSkeleton != null) { + return context.getType(targetSkeleton); + } + } + return null; + } +} diff --git a/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsUtil.java b/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsUtil.java new file mode 100644 index 000000000000..48b2ccedd8d2 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/userSkeletons/PyUserSkeletonsUtil.java @@ -0,0 +1,146 @@ +package com.jetbrains.python.codeInsight.userSkeletons; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.SdkModificator; +import com.intellij.openapi.roots.OrderRootType; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.jetbrains.python.PythonHelpersLocator; +import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; +import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyQualifiedName; +import com.jetbrains.python.psi.resolve.*; +import com.jetbrains.python.psi.types.PyClassLikeType; +import com.jetbrains.python.psi.types.PyType; +import com.jetbrains.python.psi.types.TypeEvalContext; +import com.jetbrains.python.sdk.PythonSdkType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +/** + * @author vlan + */ +public class PyUserSkeletonsUtil { + @Nullable private static VirtualFile ourUserSkeletonsDirectory; + + @NotNull + public static String getUserSkeletonsPath() { + if (ApplicationManager.getApplication().isInternal()) { + return StringUtil.join(new String[] {PathManager.getHomePath(), "python", "helpers", "user-skeletons"}, File.separator); + } + // TODO: Add the possibility to put skeletons into PathManager.getSystemPath() + "/user-skeletons" + return PythonHelpersLocator.getHelperPath("user-skeletons"); + } + + @Nullable + public static VirtualFile getUserSkeletonsDirectory() { + if (ourUserSkeletonsDirectory == null) { + ourUserSkeletonsDirectory = LocalFileSystem.getInstance().findFileByPath(getUserSkeletonsPath()); + } + return ourUserSkeletonsDirectory; + } + + @Nullable + public static T getUserSkeleton(@NotNull T element) { + final PsiFile file = element.getContainingFile(); + if (file instanceof PyFile) { + final PyFile skeletonFile = getUserSkeletonForFile((PyFile)file); + if (skeletonFile != null && skeletonFile != file) { + final PsiElement skeletonElement = getUserSkeleton(element, skeletonFile); + if (element.getClass().isInstance(skeletonElement) && skeletonElement != element) { + //noinspection unchecked + return (T)skeletonElement; + } + } + } + return null; + } + + @Nullable + public static PyFile getUserSkeletonForModuleQName(@NotNull String qName, @NotNull PsiElement foothold) { + final Sdk sdk = PythonSdkType.getSdk(foothold); + if (sdk != null) { + final Project project = foothold.getProject(); + final PythonSdkPathCache cache = PythonSdkPathCache.getInstance(project, sdk); + final PyQualifiedName cacheQName = PyQualifiedName.fromDottedString("user-skeletons." + qName); + final List results = cache.get(cacheQName); + if (results != null) { + final PsiElement element = results.isEmpty() ? null : results.get(0); + if (element instanceof PyFile) { + return (PyFile)element; + } + } + final VirtualFile directory = getUserSkeletonsDirectory(); + if (directory != null) { + final PsiDirectory psiDirectory = PsiManager.getInstance(project).findDirectory(directory); + final PsiElement fileSkeleton = new QualifiedNameResolverImpl(qName).resolveModuleAt(psiDirectory); + if (fileSkeleton instanceof PyFile) { + cache.put(cacheQName, Collections.singletonList(fileSkeleton)); + return (PyFile)fileSkeleton; + } + } + cache.put(cacheQName, Collections.emptyList()); + } + return null; + } + + public static void addUserSkeletonsRoot(@NotNull SdkModificator sdkModificator) { + final VirtualFile root = LocalFileSystem.getInstance().refreshAndFindFileByPath(getUserSkeletonsPath()); + if (root != null) { + sdkModificator.addRoot(root, OrderRootType.CLASSES); + } + } + + @Nullable + private static PsiElement getUserSkeleton(@NotNull PyElement element, @NotNull PyFile skeletonFile) { + if (element instanceof PyFile) { + return skeletonFile; + } + final ScopeOwner owner = ScopeUtil.getScopeOwner(element); + final String name = element.getName(); + if (owner != null && name != null) { + assert owner != element; + final PsiElement originalOwner = getUserSkeleton(owner, skeletonFile); + if (originalOwner instanceof PyClass) { + final PyType type = TypeEvalContext.codeInsightFallback().getType((PyClass)originalOwner); + if (type instanceof PyClassLikeType) { + final PyClassLikeType classType = (PyClassLikeType)type; + final List resolveResults = classType.resolveMember(name, null, AccessDirection.READ, + PyResolveContext.noImplicits(), false); + if (resolveResults != null && !resolveResults.isEmpty()) { + return resolveResults.get(0).getElement(); + } + } + } + else if (originalOwner instanceof NameDefiner) { + return ((NameDefiner)originalOwner).getElementNamed(name); + } + } + return null; + } + + @Nullable + private static PyFile getUserSkeletonForFile(@NotNull PyFile file) { + final VirtualFile moduleVirtualFile = file.getVirtualFile(); + if (moduleVirtualFile != null) { + final String moduleName = QualifiedNameFinder.findShortestImportableName(file, moduleVirtualFile); + if (moduleName != null) { + return getUserSkeletonForModuleQName(moduleName, file); + } + } + return null; + } +} diff --git a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java index 0b17deaaeecf..bf852b4d3a2f 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyTargetExpressionImpl.java @@ -120,7 +120,15 @@ public class PyTargetExpressionImpl extends PyPresentableElementImpl sysPath = PythonSdkType.getSysPath(sdk.getHomePath()); + sysPath.add(PyUserSkeletonsUtil.getUserSkeletonsPath()); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { diff --git a/python/testData/completion/reCompileMatch.after.py b/python/testData/completion/reCompileMatch.after.py new file mode 100644 index 000000000000..f795c16ae36a --- /dev/null +++ b/python/testData/completion/reCompileMatch.after.py @@ -0,0 +1,3 @@ +import re +p = re.compile('foo') +p.match() diff --git a/python/testData/completion/reCompileMatch.py b/python/testData/completion/reCompileMatch.py new file mode 100644 index 000000000000..0142c04723f2 --- /dev/null +++ b/python/testData/completion/reCompileMatch.py @@ -0,0 +1,3 @@ +import re +p = re.compile('foo') +p.ma diff --git a/python/testData/inspections/PyTypeCheckerInspection/FieldWithNoneInStub/a.py b/python/testData/inspections/PyTypeCheckerInspection/FieldWithNoneInStub/a.py new file mode 100644 index 000000000000..63273117f513 --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/FieldWithNoneInStub/a.py @@ -0,0 +1,12 @@ +from m1 import C + + +def f(x): + """ + :type x: int + """ + pass + + +def test(): + f(C.foo) diff --git a/python/testData/inspections/PyTypeCheckerInspection/FieldWithNoneInStub/m1.py b/python/testData/inspections/PyTypeCheckerInspection/FieldWithNoneInStub/m1.py new file mode 100644 index 000000000000..f2625742cd02 --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/FieldWithNoneInStub/m1.py @@ -0,0 +1,2 @@ +class C(object): + foo = None diff --git a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java index 6f72a5eb87c1..8a235a590666 100644 --- a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java +++ b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java @@ -644,4 +644,9 @@ public class PythonCompletionTest extends PyTestCase { public void testParameterFromUsages() { doTest(); } + + // PY-1219 + public void testReCompileMatch() { + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/PythonMockSdk.java b/python/testSrc/com/jetbrains/python/PythonMockSdk.java index 438d73ac4947..a1e1809d2c4a 100644 --- a/python/testSrc/com/jetbrains/python/PythonMockSdk.java +++ b/python/testSrc/com/jetbrains/python/PythonMockSdk.java @@ -7,6 +7,7 @@ import com.intellij.openapi.projectRoots.SdkType; import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.vfs.LocalFileSystem; +import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil; import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.NonNls; @@ -53,6 +54,8 @@ public class PythonMockSdk { sdkModificator.addRoot(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(libPath), OrderRootType.CLASSES); } + PyUserSkeletonsUtil.addUserSkeletonsRoot(sdkModificator); + String mock_stubs_path = mock_path + PythonSdkType.SKELETON_DIR_NAME; sdkModificator.addRoot(LocalFileSystem.getInstance().refreshAndFindFileByPath(mock_stubs_path), PythonSdkType.BUILTIN_ROOT_TYPE); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index f561664f9e4b..d84e8f9bb0a5 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -6,12 +6,22 @@ import com.jetbrains.python.fixtures.PyTestCase; * @author vlan */ public class PyTypeCheckerInspectionTest extends PyTestCase { + public static final String TEST_DIRECTORY = "inspections/PyTypeCheckerInspection/"; + private void doTest() { - myFixture.configureByFile("inspections/PyTypeCheckerInspection/" + getTestName(false) + ".py"); + myFixture.configureByFile(TEST_DIRECTORY + getTestName(false) + ".py"); myFixture.enableInspections(PyTypeCheckerInspection.class); myFixture.checkHighlighting(true, false, true); } + private void doMultiFileTest() { + final String testName = getTestName(false); + myFixture.copyDirectoryToProject(TEST_DIRECTORY + testName, ""); + myFixture.configureFromTempProjectFile("a.py"); + myFixture.enableInspections(PyTypeCheckerInspection.class); + myFixture.checkHighlighting(true, false, false); + } + public void testSimple() { doTest(); } @@ -165,4 +175,9 @@ public class PyTypeCheckerInspectionTest extends PyTestCase { public void testNegativeIsInstance() { doTest(); } + + // PY-7340 + public void testFieldWithNoneInStub() { + doMultiFileTest(); + } }