From 039326217e425b47a138e49af021bead0246ab0c Mon Sep 17 00:00:00 2001 From: "Aleksandr.Govenko" Date: Fri, 29 Aug 2025 00:06:23 +0300 Subject: [PATCH] PY-82818 PyCharm infers `None` for `dict.get(..., None)` if `dict` key/value types are unknown GitOrigin-RevId: d12ed4598d91f1ab4824f6e5953e275672027064 --- .../python/psi/types/PyTypeChecker.java | 45 ++++++++++--------- .../psi/types/PyTypeCheckerExtension.java | 5 ++- .../com/jetbrains/python/Py3TypeTest.java | 31 +++++++++++++ .../com/jetbrains/python/PyTypeTest.java | 2 +- 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java index f74ccc165f11..750370839c70 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -2,7 +2,6 @@ package com.jetbrains.python.psi.types; import com.intellij.openapi.util.*; -import com.intellij.openapi.util.registry.Registry; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.util.ArrayUtil; @@ -271,16 +270,16 @@ public final class PyTypeChecker { return false; } - PyType substituted = context.mySubstitutions.typeVars.get(expected); + Ref substitutedRef = context.mySubstitutions.typeVars.get(expected); Ref defaultTypeRef = expected.getDefaultType(); if (defaultTypeRef != null) { PyType defaultType = defaultTypeRef.get(); // Skip default substitution - if (defaultType != null && defaultType.equals(substituted)) { - substituted = null; + if (defaultType != null && defaultType.equals(Ref.deref(substitutedRef))) { + substitutedRef = null; } } - final PyType substitution = substituted; + final PyType substitution = Ref.deref(substitutedRef); PyType bound = expected.getBound(); List<@Nullable PyType> constraints = expected.getConstraints(); int matchedConstraintIndex = -1; @@ -306,8 +305,8 @@ public final class PyTypeChecker { } } - if (substitution != null) { - if (expected.equals(safeActual) || substitution.equals(expected)) { + if (substitutedRef != null) { + if (expected.equals(safeActual) || expected.equals(substitution)) { return true; } @@ -321,12 +320,12 @@ public final class PyTypeChecker { if (safeActual != null) { PyType type = constraints.isEmpty() ? safeActual : constraints.get(matchedConstraintIndex); - context.mySubstitutions.typeVars.put(expected, type); + context.mySubstitutions.typeVars.put(expected, Ref.create(type)); } else { PyType effectiveBound = PyTypeUtil.getEffectiveBound(expected); if (effectiveBound != null) { - context.mySubstitutions.typeVars.put(expected, PyUnionType.createWeakType(effectiveBound)); + context.mySubstitutions.typeVars.put(expected, Ref.create(PyUnionType.createWeakType(effectiveBound))); } } @@ -934,7 +933,7 @@ public final class PyTypeChecker { Map substitutionsFromClassDefinition = provider.getGenericSubstitutions(classType.getPyClass(), context); for (Map.Entry entry : substitutionsFromClassDefinition.entrySet()) { if (entry.getKey() instanceof PyTypeVarType typeVarType) { - result.typeVars.put(typeVarType, entry.getValue()); + result.typeVars.put(typeVarType, Ref.create(entry.getValue())); } else if (entry.getKey() instanceof PyTypeVarTupleType typeVarTuple) { assert entry.getValue() instanceof PyPositionalVariadicType; @@ -1092,7 +1091,7 @@ public final class PyTypeChecker { boolean isAlreadyBound = existingSubstitutions.typeVars.containsKey(returnTypeParam) || existingSubstitutions.typeVars.containsKey(invert(returnTypeParam)); if (canGetBoundFromArguments && !isAlreadyBound) { - existingSubstitutions.typeVars.put(returnTypeParam, Ref.deref(returnTypeParam.getDefaultType())); + existingSubstitutions.typeVars.put(returnTypeParam, (Ref)returnTypeParam.getDefaultType()); } } for (PyParamSpecType paramSpecType : typeParamsFromReturnType.paramSpecs) { @@ -1224,10 +1223,11 @@ public final class PyTypeChecker { } return typeVarType; } - PyType substitution = substitutions.typeVars.get(typeVarType); - if (substitution == null) { + Ref substitutionRef = substitutions.typeVars.get(typeVarType); + PyType substitution = Ref.deref(substitutionRef); + if (substitutionRef == null) { final PyInstantiableType invertedTypeVar = invert(typeVarType); - final PyInstantiableType invertedSubstitution = as(substitutions.typeVars.get(invertedTypeVar), PyInstantiableType.class); + final PyInstantiableType invertedSubstitution = as(Ref.deref(substitutions.typeVars.get(invertedTypeVar)), PyInstantiableType.class); if (invertedSubstitution != null) { substitution = invert(invertedSubstitution); } @@ -1548,7 +1548,7 @@ public final class PyTypeChecker { .select(PyClassType.class) .map(type -> collectTypeSubstitutions(type, context)) .forEach(newSubstitutions -> { - for (Map.Entry typeVarMapping : newSubstitutions.typeVars.entrySet()) { + for (Map.Entry> typeVarMapping : newSubstitutions.typeVars.entrySet()) { substitutions.typeVars.putIfAbsent(typeVarMapping.getKey(), typeVarMapping.getValue()); } for (Map.Entry typeVarMapping : newSubstitutions.typeVarTuples.entrySet()) { @@ -1738,7 +1738,7 @@ public final class PyTypeChecker { if (mapping != null) { for (Couple pair : mapping.getMappedTypes()) { if (pair.getFirst() instanceof PyTypeVarType typeVar) { - substitutions.typeVars.put(typeVar, pair.getSecond()); + substitutions.typeVars.put(typeVar, Ref.create(pair.getSecond())); } else if (pair.getFirst() instanceof PyTypeVarTupleType typeVarTuple) { substitutions.typeVarTuples.put(typeVarTuple, as(pair.getSecond(), PyPositionalVariadicType.class)); @@ -1806,11 +1806,13 @@ public final class PyTypeChecker { @ApiStatus.Experimental public static class GenericSubstitutions { - private final @NotNull Map typeVars; + + // Nullable-Nullable because of com.jetbrains.python.psi.types.PyTypeChecker.collectTypeSubstitutions + private final @NotNull Map> typeVars; - private final @NotNull Map typeVarTuples; + private final @NotNull Map typeVarTuples; - private final @NotNull Map paramSpecs; + private final @NotNull Map paramSpecs; private @Nullable PyType qualifierType; @@ -1818,6 +1820,7 @@ public final class PyTypeChecker { this( EntryStream.of(typeParameters) .selectKeys(PyTypeVarType.class) + .mapValues(Ref::create) .toCustomMap(LinkedHashMap::new), EntryStream.of(typeParameters) .selectKeys(PyTypeVarTupleType.class) @@ -1835,7 +1838,7 @@ public final class PyTypeChecker { this(new LinkedHashMap<>(), new LinkedHashMap<>(), new LinkedHashMap<>(), null); } - private GenericSubstitutions(@NotNull Map typeVars, + private GenericSubstitutions(@NotNull Map> typeVars, @NotNull Map typeVarTuples, @NotNull Map paramSpecs, @Nullable PyType qualifierType) { @@ -1849,7 +1852,7 @@ public final class PyTypeChecker { return Collections.unmodifiableMap(paramSpecs); } - public @NotNull Map getTypeVars() { + public @NotNull Map> getTypeVars() { return Collections.unmodifiableMap(typeVars); } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeCheckerExtension.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeCheckerExtension.java index e2fe7f712211..1e467ff04f97 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeCheckerExtension.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeCheckerExtension.java @@ -2,6 +2,7 @@ package com.jetbrains.python.psi.types; import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.util.Ref; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -23,9 +24,9 @@ public interface PyTypeCheckerExtension { @NotNull TypeEvalContext context, @NotNull PyTypeChecker.GenericSubstitutions substitutions) { Map legacyTypeVarSubs = new HashMap<>(); - for (Map.Entry entry : substitutions.getTypeVars().entrySet()) { + for (Map.Entry> entry : substitutions.getTypeVars().entrySet()) { if (entry.getKey() instanceof PyGenericType legacyTypeVar) { - legacyTypeVarSubs.put(legacyTypeVar, entry.getValue()); + legacyTypeVarSubs.put(legacyTypeVar, Ref.deref(entry.getValue())); } } return match(expected, actual, context, legacyTypeVarSubs); diff --git a/python/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index 7de5b1a4f9b9..1ff40a63a834 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -18,6 +18,37 @@ import java.util.Map; public class Py3TypeTest extends PyTestCase { public static final String TEST_DIRECTORY = "/types/"; + /** + Overload signatures for dict.get and dict.pop in builtins.pyi differ slightly, + dict.get has default value for "default" parameter. This affect the logic of overload resolution. + Therefore it makes sense to test both. +

+

{@code
+  @overload
+  def get(self, key: _KT, default: None = None, /) -> _VT | None: ...
+  # mode overloads...
+   }
+

+

{@code
+  @overload
+  def pop(self, key: _KT, /) -> _VT: ...
+  # mode overloads...
+   }
+ */ + // PY-82818 + public void testGetFromDictWithDefaultNoneValue() { + doTest("Any | None", """ + d = {} + expr = d.get("abc", None)"""); + } + + // PY-82818 + public void testPopFromDictWithDefaultNoneValue() { + doTest("Any", """ + d = {} + expr = d.pop("abc", None)"""); + } + public void testYieldInsideLambda() { // Checks that foo is not a generator doTest("int", """ diff --git a/python/testSrc/com/jetbrains/python/PyTypeTest.java b/python/testSrc/com/jetbrains/python/PyTypeTest.java index 02e3c0263d02..8ac6f3138c0f 100644 --- a/python/testSrc/com/jetbrains/python/PyTypeTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypeTest.java @@ -1775,7 +1775,7 @@ public class PyTypeTest extends PyTestCase { // PY-20409 public void testGetFromDictWithDefaultNoneValue() { - doTest("Optional[Any]", + doTest("Any", "d = {}\n" + "expr = d.get(\"abc\", None)"); }