From 8ccd3203c229861a4ff81dbd61ca4b28888a1551 Mon Sep 17 00:00:00 2001 From: Marcus Mews Date: Mon, 10 Nov 2025 15:42:15 +0000 Subject: [PATCH] PY-84484 Pycharm hangs when trying to call specific constructor (cherry picked from commit b15a3bab218f72c1e9c717facfbff3e91eeea13f) IJ-MR-180106 GitOrigin-RevId: 88614fddf666e170b71ef38fa4adffa9fa75da0a --- .../typing/PyTypingTypeProvider.java | 27 +++++++-- .../RecursiveTypeInDict/SpanQuery.py | 56 +++++++++++++++++++ .../RecursiveTypeInDict/a.py | 7 +++ .../PyTypeCheckerInspectionTest.java | 5 ++ 4 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 python/testData/inspections/PyTypeCheckerInspection/RecursiveTypeInDict/SpanQuery.py create mode 100644 python/testData/inspections/PyTypeCheckerInspection/RecursiveTypeInDict/a.py diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java index 6c91a008fa64..86e5399240bb 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java @@ -54,7 +54,6 @@ import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Function; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; import static com.dynatrace.hash4j.hashing.Hashing.xxh3_128; @@ -861,6 +860,11 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< } context.addTypeAlias(alias); } + if (resolved instanceof PyClass pyClass && !context.addClassDeclaration(pyClass)) { + // Resolving to normal classes shouldn't cause recursive evaluation of type hints, + // but constructing recursive PyTypedDictTypes will trigger that. + return null; + } try { final Ref typeHintFromProvider = PyTypeHintProvider.Companion.parseTypeHint( typeHint, @@ -994,6 +998,9 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< return null; } finally { + if (resolved instanceof PyClass pyClass) { + context.removeClassDeclaration(pyClass); + } if (alias != null) { context.removeTypeAlias(alias); } @@ -2359,6 +2366,7 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< public static final class Context { private final @NotNull TypeEvalContext myContext; private final @NotNull Stack myTypeAliasStack = new Stack<>(); + private final @NotNull Set myClassSet = new HashSet<>(); private boolean myComputeTypeParameterScope = true; private final boolean myUseFqn; @@ -2416,6 +2424,14 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< return res; } + public boolean addClassDeclaration(@NotNull PyClass pyClass) { + return myClassSet.add(pyClass); + } + + public void removeClassDeclaration(@NotNull PyClass pyClass) { + myClassSet.remove(pyClass); + } + public boolean isComputeTypeParameterScopeEnabled() { return myComputeTypeParameterScope; } @@ -2439,9 +2455,12 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< private @NotNull HashValue128 myContextStrongHashValue; private void recomputeStrongHashValue() { - myContextStrongHashValue = xxh3_128().hashCharsTo128Bits(Stream.concat(Stream.of(myComputeTypeParameterScope ? "1" : "0"), - myTypeAliasStack.stream().map(it -> it.getQualifiedName())) - .collect(Collectors.joining("#"))); + myContextStrongHashValue = xxh3_128().hashCharsTo128Bits( + StreamEx.of(myComputeTypeParameterScope ? "1" : "0") + .append(myTypeAliasStack.stream().map(it -> it.getQualifiedName())) + .append(myClassSet.stream().map(it -> it.getQualifiedName())) + .joining("#") + ); } private @NotNull HashValue128 getContextStrongHashValue() { diff --git a/python/testData/inspections/PyTypeCheckerInspection/RecursiveTypeInDict/SpanQuery.py b/python/testData/inspections/PyTypeCheckerInspection/RecursiveTypeInDict/SpanQuery.py new file mode 100644 index 000000000000..b54140a11b5b --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/RecursiveTypeInDict/SpanQuery.py @@ -0,0 +1,56 @@ +from typing_extensions import TypedDict + +class SpanQuery(TypedDict, total=False): + """A serializable query for filtering SpanNodes based on various conditions. + + All fields are optional and combined with AND logic by default. + """ + + # These fields are ordered to match the implementation of SpanNode.matches_query for easy review. + # * Individual span conditions come first because these are generally the cheapest to evaluate + # * Logical combinations come next because they may just be combinations of individual span conditions + # * Related-span conditions come last because they may require the most work to evaluate + + # Individual span conditions + ## Name conditions + name_equals: str + name_contains: str + name_matches_regex: str # regex pattern + + ## Attribute conditions + has_attributes: dict[str, Any] + has_attribute_keys: list[str] + + ## Timing conditions + min_duration: timedelta | float + max_duration: timedelta | float + + # Logical combinations of conditions + not_: SpanQuery + and_: list[SpanQuery] + or_: list[SpanQuery] + + # Child conditions + min_child_count: int + max_child_count: int + some_child_has: SpanQuery + all_children_have: SpanQuery + no_child_has: SpanQuery + + # Recursive conditions + stop_recursing_when: SpanQuery + """If present, stop recursing through ancestors or descendants at nodes that match this condition.""" + + ## Descendant conditions + min_descendant_count: int + max_descendant_count: int + some_descendant_has: SpanQuery + all_descendants_have: SpanQuery + no_descendant_has: SpanQuery + + ## Ancestor conditions + min_depth: int # depth is equivalent to ancestor count; roots have depth 0 + max_depth: int + some_ancestor_has: SpanQuery + all_ancestors_have: SpanQuery + no_ancestor_has: SpanQuery \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/RecursiveTypeInDict/a.py b/python/testData/inspections/PyTypeCheckerInspection/RecursiveTypeInDict/a.py new file mode 100644 index 000000000000..175372278e64 --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/RecursiveTypeInDict/a.py @@ -0,0 +1,7 @@ +from SpanQuery import SpanQuery + +def fun(sq: SpanQuery) : + pass + +mySQ = SpanQuery() +fun(mySQ) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index 9d536c6a7dff..95256beee45e 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -237,6 +237,11 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-84484 + public void testRecursiveTypeInDict() { + runWithLanguageLevel(LanguageLevel.getLatest(), this::doMultiFileTest); + } + // PY-13394 public void testContainsArguments() { doTest();