From ea6c0181dfacead769af4c624444baa3ee64797e Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Mon, 23 Mar 2020 21:27:03 +0300 Subject: [PATCH] Completion for string passed as an argument to a parameter type hinted as str or PathLike (PY-33254, PY-12339, PY-40834) GitOrigin-RevId: 87e0aef80a2b458b888c4e8ad072efd94f8bafc6 --- .../PySoftFileReferenceContributor.kt | 39 +++++++++++++++++-- .../typedParameterStringPath/a.after.py | 6 +++ .../completion/typedParameterStringPath/a.py | 6 +++ .../typedParameterStringPath/foobar.txt | 0 .../jetbrains/python/Py3CompletionTest.java | 4 ++ 5 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 python/testData/completion/typedParameterStringPath/a.after.py create mode 100644 python/testData/completion/typedParameterStringPath/a.py create mode 100644 python/testData/completion/typedParameterStringPath/foobar.txt diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PySoftFileReferenceContributor.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PySoftFileReferenceContributor.kt index 42802e819b6e..63cb4999f97e 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PySoftFileReferenceContributor.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/PySoftFileReferenceContributor.kt @@ -10,10 +10,14 @@ import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferen import com.intellij.psi.util.QualifiedName import com.intellij.util.ProcessingContext import com.intellij.util.SystemProperties +import com.jetbrains.python.PyNames import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.impl.PyBuiltinCache import com.jetbrains.python.psi.impl.PyCallExpressionHelper import com.jetbrains.python.psi.resolve.PyResolveContext import com.jetbrains.python.psi.resolve.PyResolveUtil +import com.jetbrains.python.psi.types.PyTypeChecker +import com.jetbrains.python.psi.types.PyUnionType import com.jetbrains.python.psi.types.TypeEvalContext /** @@ -30,7 +34,8 @@ open class PySoftFileReferenceContributor : PsiReferenceContributor() { .andOr(stringLiteral.with(HardCodedCalleeName), stringLiteral.with(AssignmentMatchingNamePattern), stringLiteral.with(KeywordArgumentMatchingNamePattern), - stringLiteral.with(CallArgumentMatchingParameterNamePattern)) + stringLiteral.with(CallArgumentMatchingParameterNamePattern), + stringLiteral.with(CallArgumentMatchingParameterType)) registrar.registerReferenceProvider(pattern, createSoftFileReferenceProvider()) } @@ -74,6 +79,33 @@ open class PySoftFileReferenceContributor : PsiReferenceContributor() { } } + private object CallArgumentMatchingParameterType : PatternCondition("callArgumentMatchingPattern") { + override fun accepts(expr: PyStringLiteralExpression, context: ProcessingContext?): Boolean { + val argList = expr.parent as? PyArgumentList ?: return false + val callExpr = argList.parent as? PyCallExpression ?: return false + + val builtinCache = PyBuiltinCache.getInstance(expr) + val strOrUnicodeType = builtinCache.strOrUnicodeType ?: return false + val osPathLikeType = builtinCache.getObjectType(PyNames.BUILTIN_PATH_LIKE) ?: return false + + val typeEvalContext = TypeEvalContext.codeInsightFallback(expr.project) + + return callExpr.multiResolveCallee(PyResolveContext.defaultContext().withTypeEvalContext(typeEvalContext)) + .asSequence() + .mapNotNull { + val mapping = PyCallExpressionHelper.mapArguments(callExpr, it, typeEvalContext) + mapping.mappedParameters[expr]?.getArgumentType(typeEvalContext) + } + .toList() + .let { PyUnionType.union(it) } + .let { + it != null && + PyTypeChecker.match(strOrUnicodeType, it, typeEvalContext) && + PyTypeChecker.match(osPathLikeType, it, typeEvalContext) + } + } + } + /** * Matches string literals used as function keyword arguments where the keyword has a name that has something about files or paths. */ @@ -96,9 +128,8 @@ open class PySoftFileReferenceContributor : PsiReferenceContributor() { } private val PATTERNS = listOf( - Pattern("open", 0, isBuiltin = true), - Pattern("os.walk", 0), - Pattern("os.scandir", 0), + Pattern("open", 0, isBuiltin = true), // could be covered by CallArgumentMatchingParameterType in Py3+ + Pattern("os.walk", 0), // could be covered by CallArgumentMatchingParameterType in Py3+ Pattern("pandas.read_csv", 0) ) private val SIMPLE_NAMES = PATTERNS.associateBy { it.qualifiedName.lastComponent } diff --git a/python/testData/completion/typedParameterStringPath/a.after.py b/python/testData/completion/typedParameterStringPath/a.after.py new file mode 100644 index 000000000000..1cec4fe2ac0c --- /dev/null +++ b/python/testData/completion/typedParameterStringPath/a.after.py @@ -0,0 +1,6 @@ +from os import PathLike + +def baz(akjlkgjdfsakglkd: PathLike) -> None: + pass + +baz("foobar.txt") \ No newline at end of file diff --git a/python/testData/completion/typedParameterStringPath/a.py b/python/testData/completion/typedParameterStringPath/a.py new file mode 100644 index 000000000000..b06ee38ccbac --- /dev/null +++ b/python/testData/completion/typedParameterStringPath/a.py @@ -0,0 +1,6 @@ +from os import PathLike + +def baz(akjlkgjdfsakglkd: PathLike) -> None: + pass + +baz("foo") \ No newline at end of file diff --git a/python/testData/completion/typedParameterStringPath/foobar.txt b/python/testData/completion/typedParameterStringPath/foobar.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testSrc/com/jetbrains/python/Py3CompletionTest.java b/python/testSrc/com/jetbrains/python/Py3CompletionTest.java index 6ecf52b8d02e..b88ce5d2b5c5 100644 --- a/python/testSrc/com/jetbrains/python/Py3CompletionTest.java +++ b/python/testSrc/com/jetbrains/python/Py3CompletionTest.java @@ -400,6 +400,10 @@ public class Py3CompletionTest extends PyTestCase { assertContainsElements(suggested, PyNamedTupleType.NAMEDTUPLE_SPECIAL_ATTRIBUTES); } + // PY-33254, PY-12339, PY-40834 + public void testTypedParameterStringPath() { + doMultiFileTest(); + } @Override protected String getTestDataPath() {