PY-83039 Don't trigger PyLiteralTypeCompletionContributor in cases like x = y.foo<caret>

It should be activated only for the simplest cases when the caret is either
inside an immediate string literal or its prefix is an unqualified reference
expression. It makes little sense trying to detect if something like
the literal string `"y.foobar"` is a possible value for `x`.

It's a relatively heavy completion contributor. It starts evaluating the type of
`x` flow-sensitively, analyzing all preceding function calls to take into
account `NoReturn`. It affects common workflows like typing out
`df = pd.` to create a new Pandas dataframe.


(cherry picked from commit f17fe2ce86ee100a3480a574c7f57a1bd67ec2d8)

IJ-CR-172165

GitOrigin-RevId: 4434488b1d2dae3ab7efd72e775b0c730268e51c
This commit is contained in:
Mikhail Golubev
2025-08-04 11:58:08 +03:00
committed by intellij-monorepo-bot
parent 679566f2c9
commit 4aff3ed154
4 changed files with 48 additions and 0 deletions

View File

@@ -18,6 +18,24 @@ import com.jetbrains.python.psi.types.PyType
import com.jetbrains.python.psi.types.PyTypeUtil
import com.jetbrains.python.psi.types.TypeEvalContext
/**
* Provides literal type variants in the following cases:
* ```python
* x: Literal["foo", "bar"]
* x = <caret>
* x = fo<caret>
* x = "fo<caret>"
* ```
*
* or
*
* ```python
* def f(x: Literal["foo", "bar"]): ...
* f(<caret>)
* f(fo<caret>)
* f("fo<caret>")
* ```
*/
class PyLiteralTypeCompletionContributor : CompletionContributor() {
init {
extend(CompletionType.BASIC, psiElement(), PyLiteralTypeCompletionProvider())
@@ -27,6 +45,7 @@ class PyLiteralTypeCompletionContributor : CompletionContributor() {
private class PyLiteralTypeCompletionProvider : CompletionProvider<CompletionParameters?>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val position = parameters.position.parent as? PyExpression ?: return
if (!(position is PyStringLiteralExpression || position is PyReferenceExpression && !position.isQualified)) return
val typeEvalContext = TypeEvalContext.codeCompletion(position.project, position.containingFile)
val mappedParameters = position.getMappedParameters(PyResolveContext.defaultContext(typeEvalContext))

View File

@@ -0,0 +1,5 @@
from typing import Literal
x: Literal["upper", "lower"]
y = ""
x = y.upp<caret>

View File

@@ -0,0 +1,8 @@
from typing import Literal
def f(x: Literal["upper", "lower"]):
pass
y = ""
f(y.up<caret>)

View File

@@ -75,6 +75,16 @@ class PyLiteralTypeCompletionTest : PyTestCase() {
myFixture.testCompletionVariants("nestedArgumentLists.py")
}
// PY-83039
fun testNoLiteralVariantsOnQualifiedReferenceInAssignmentValue() {
doTestCompletionVariantsDoesNotContain("noLiteralVariantsOnQualifiedReferenceInAssignmentValue.py", "\"upper\"")
}
// PY-83039
fun testNoLiteralVariantsOnQualifiedReferenceInCallArgument() {
doTestCompletionVariantsDoesNotContain("noLiteralVariantsOnQualifiedReferenceInCallArgument.py", "\"upper\"")
}
override fun getTestDataPath(): String {
return super.getTestDataPath() + "/completion/literalType"
}
@@ -84,4 +94,10 @@ class PyLiteralTypeCompletionTest : PyTestCase() {
assertNotNull(result)
assertContainsElements(result!!, *items)
}
private fun doTestCompletionVariantsDoesNotContain(fileBefore: String, vararg items: String) {
val result = myFixture.getCompletionVariants(fileBefore)
assertNotNull(result)
assertDoesntContain(result!!, *items)
}
}