PY-76820 Improvements in PyTypingAliasStubType

1. Handle type aliases with the other names imported via `as`, e.g.
```
from typing import TypeAlias as TA
a: TA = list[int]
```
This is important for the conformance tests suite

2. Consider binary expressions with OR operators as type hints
3. Do not consider f-string and prefixed strings as type hints

GitOrigin-RevId: fcdf7adb7aa3b921091772c521f26c4474a3877e
This commit is contained in:
Daniil Kalinin
2024-12-16 09:42:09 +01:00
committed by intellij-monorepo-bot
parent 0c0380dc72
commit 16d35ed4ca
3 changed files with 46 additions and 6 deletions

View File

@@ -60,7 +60,7 @@ public class PyFileElementType extends IStubFileElementType<PyFileStub> {
@Override
public int getStubVersion() {
// Don't forget to update versions of indexes that use the updated stub-based elements
return 98;
return 99;
}
@Override

View File

@@ -17,16 +17,24 @@ package com.jetbrains.python.psi.impl.stubs;
import com.intellij.extapi.psi.ASTDelegatePsiElement;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.stubs.StubInputStream;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.ast.impl.PyUtilCore;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.stubs.PyTargetExpressionStub;
import com.jetbrains.python.psi.stubs.PyTargetExpressionStub.InitializerType;
import com.jetbrains.python.psi.stubs.PyTypingAliasStub;
import org.jetbrains.annotations.ApiStatus;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -96,18 +104,20 @@ public final class PyTypingAliasStubType extends CustomTargetExpressionStubType<
}
private static boolean isExplicitTypeAlias(@NotNull PyTargetExpression target) {
String typeHintText = "";
PyAnnotation annotation = target.getAnnotation();
if (annotation != null) {
PyExpression value = annotation.getValue();
if (value != null) {
typeHintText = value.getText();
if (value instanceof PyReferenceExpression referenceExpression) {
return StreamEx.of(PyResolveUtil.resolveImportedElementQNameLocally(referenceExpression))
.map(QualifiedName::toString)
.anyMatch(name -> name.equals(PyTypingTypeProvider.TYPE_ALIAS) || name.equals(PyTypingTypeProvider.TYPE_ALIAS_EXT));
}
}
else {
typeHintText = StringUtil.notNullize(target.getTypeCommentAnnotation());
String typeHintText = StringUtil.notNullize(target.getTypeCommentAnnotation());
return typeHintText.equals("TypeAlias") || typeHintText.endsWith(".TypeAlias");
}
return typeHintText.equals("TypeAlias") || typeHintText.endsWith(".TypeAlias");
return false;
}
@ApiStatus.Internal
@@ -123,9 +133,17 @@ public final class PyTypingAliasStubType extends CustomTargetExpressionStubType<
final PyStringLiteralExpression pyString = as(expression, PyStringLiteralExpression.class);
if (pyString != null) {
if (pyString.isInterpolated()) { // f-strings are not allowed
return false;
}
if (pyString.getStringNodes().size() != 1 || pyString.getTextLength() > STRING_LITERAL_LENGTH_THRESHOLD) {
return false;
}
else {
if (!pyString.getStringElements().get(0).getPrefix().isEmpty()) { // prefixed strings are not allowed
return false;
}
}
final String content = pyString.getStringValue();
return TYPE_ANNOTATION_LIKE.matcher(content).matches();
}
@@ -133,11 +151,22 @@ public final class PyTypingAliasStubType extends CustomTargetExpressionStubType<
if (expression instanceof PyReferenceExpression || expression instanceof PySubscriptionExpression) {
return isSyntacticallyValidAnnotation(expression);
}
if (expression instanceof PyBinaryExpression binaryExpression) {
if (binaryExpression.getOperator() == PyTokenTypes.OR) {
PyExpression leftOperand = binaryExpression.getLeftExpression();
PyExpression rightOperand = binaryExpression.getRightExpression();
return leftOperand != null && rightOperand != null &&
isSyntacticallyValidAnnotation(leftOperand) && isSyntacticallyValidAnnotation(rightOperand);
}
}
return false;
}
private static boolean isSyntacticallyValidAnnotation(@NotNull PyExpression expression) {
if (expression instanceof PyBinaryExpression) {
return looksLikeTypeHint(expression);
}
return PsiTreeUtil.processElements(expression, element -> {
// Check only composite elements
if (element instanceof ASTDelegatePsiElement) {

View File

@@ -3,6 +3,7 @@ __all__ = ['S1', 'S2']
__version__ = '0.1'
from typing_extensions import TypeAlias
from typing import TypeAlias as TA
S1_ok = "foo"
S2_ok = "foo.bar"
@@ -11,8 +12,18 @@ too_long_string = "foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.f
natural_text = "Foo is baz."
glued_string = 'foo' '.bar'
S4_notOk = f"int"
S5_notOk = u"int"
S6_notOk = b"int"
bin1_ok = int | str
bin2_ok = int | str | bool | None
bin3_ok = Union[str, bool] | None
bin4_notOk = str & int
explicit_alias1_ok: TypeAlias = 'Foo is bar.'
explicit_alias2_ok = 'Foo is bar.' # type: TypeAlias
explicit_alias_imported_via_as_ok: TA = int | str
# Such expressions are kept as qualified expressions in PyTargetExpressionStub
# with initializer type of ReferenceExpression instead of custom stubs for