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