From 44bff92759649f694588028e1d040aa5b0e0881b Mon Sep 17 00:00:00 2001 From: Daniil Kalinin Date: Thu, 5 Jun 2025 09:58:51 +0200 Subject: [PATCH] PY-76862 Report circular references in string literal type annotations GitOrigin-RevId: 793e8efe56d7e8f979cab0cdf53958383f6b60a4 --- .../resources/messages/PyPsiBundle.properties | 1 + .../inspections/PyTypeHintsInspection.kt | 22 +++++++++++ .../PyTypeHintsInspectionTest.java | 38 +++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index df0c9ce327ef..06cced4d0f9f 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -1198,6 +1198,7 @@ INSP.type.hints.type.alias.invalid.assigned.value=Assigned value of type alias m INSP.type.hints.type.alias.cannot.be.parameterized='TypeAlias' cannot be parameterized INSP.type.hints.type.hint.is.not.valid=Type hint is invalid or refers to the expression which is not a correct type INSP.type.hints.forward.reference.in.union=Union type annotations with forward references must be wrapped in quotes entirely +INSP.type.hints.circular.reference=Circular reference INSP.type.hints.typeIs.has.zero.parameters=User-defined TypeGuard or TypeIs functions must have at least one parameter INSP.type.hints.typeIs.does.not.match=Return type of TypeIs ''{0}'' is not consistent with the type of the first parameter ''{1}'' INSP.type.hints.self.use.in.staticmethod=Cannot use 'Self' in staticmethod diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt index 83f38ffb72d6..f8f0b087926a 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -17,6 +17,7 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFileFactory +import com.intellij.psi.impl.source.resolve.FileContextUtil import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.QualifiedName import com.intellij.psi.util.isAncestor @@ -366,6 +367,11 @@ class PyTypeHintsInspection : PyInspection() { checkAnnotatedNonSelfAttribute(node) checkTypeAliasTarget(node) + + val annotation = node.annotation + if (annotation != null) { + checkCircularReference(annotation, node) + } } private fun checkTypeAliasTarget(target: PyTargetExpression) { @@ -1257,6 +1263,22 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun checkCircularReference(annotation: PyAnnotation, targetExpr: PyTargetExpression) { + val annotationValue = annotation.value as? PyStringLiteralExpression ?: return + val stringValue = annotationValue.stringValue + if (stringValue != targetExpr.name) return + + val contextFile = FileContextUtil.getContextFile(annotation) ?: return + val referenceFromStringLiteral = PyUtil + .createExpressionFromFragment(stringValue, contextFile) as? PyReferenceExpression ?: return + val resolveResults = PyUtil.multiResolveTopPriority(referenceFromStringLiteral, + PyResolveContext.defaultContext(myTypeEvalContext)) + if (resolveResults.isEmpty() || (resolveResults.size == 1 && resolveResults.first() === targetExpr)) { + registerProblem(annotationValue, PyPsiBundle.message("INSP.type.hints.circular.reference"), + ProblemHighlightType.GENERIC_ERROR) + } + } + private fun checkTypingMemberParameters(index: PyExpression, isCallable: Boolean) { val parameters = if (index is PyTupleExpression) index.elements else arrayOf(index) diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java index 22ad6ede1cb8..aaa2950b24e5 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -2863,6 +2863,44 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { """); } + // PY-76862 + public void testCheckCircularReferences() { + doTestByText(""" + from typing import TypeAlias + class ClassA: + ... + + type ClassB = str + + ClassC = int + + ClassD: TypeAlias = bool + + circular: "circular" = None + + class Test: + ClassA: "ClassA" # OK + ClassB: "ClassB" # OK + ClassC: "ClassC" # OK + ClassD: "ClassD" # OK + + ClassE: "ClassE" # E: circular reference + + ClassG: "ClassG" = None # E: circular reference + + def foo(self): + Test: "Test" + ClassA: "ClassA" # OK + ClassB: "ClassB" # OK + ClassC: "ClassC" # OK + str: "str" # OK + def int(self) -> None: + ... + x: "int" = 0 # OK + var: "var" = None # E: circular reference + """); + } + @NotNull @Override