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.

GitOrigin-RevId: f17fe2ce86ee100a3480a574c7f57a1bd67ec2d8
This commit is contained in:
Mikhail Golubev
2025-08-04 11:34:02 +00:00
committed by intellij-monorepo-bot
parent f7b08e1c3d
commit 6ad3c2af62
4 changed files with 48 additions and 0 deletions
@@ -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))
@@ -0,0 +1,5 @@
from typing import Literal
x: Literal["upper", "lower"]
y = ""
x = y.upp<caret>
@@ -0,0 +1,8 @@
from typing import Literal
def f(x: Literal["upper", "lower"]):
pass
y = ""
f(y.up<caret>)
@@ -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)
}
}