From 414397c7ea9787ae3190f321e9ec36c7b95e48b2 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Fri, 1 Nov 2024 18:06:57 +0200 Subject: [PATCH] PY-77167 Simplify resolve logic for overloads, get rid of RatedResolveResult#RATE_LIFTED_PY_FILE_OVERLOAD If there is an overload not followed by an implementation, which is already an error, always resolve to the first overload, regardless of whether it's a .py file, or a .pyi stub. It allows us to eliminate the special RatedResolveResult#RATE_LIFTED_PY_FILE_OVERLOAD rate in .py files, because we no longer need to duplicate the last, closest reachable overload (normally an implementation should be reachable) with a higher priority, and then filter it out during overload resolution. Meanwhile, this filtering out didn't work right before because some type inference logic, e.g., PyCallExpressionHelper.getCalleeType used in PyReferenceExpressionImpl.getCallableType bypassed it. It should have been done at the level of PyCallExpressionHelper.forEveryScopeTakeOverloadsOtherwiseImplementations. GitOrigin-RevId: 99a624ab85957d7a2d3c2c0ced596e472f9d615b --- .../psi/resolve/RatedResolveResult.java | 3 --- .../psi/impl/PyCallExpressionHelper.java | 5 +---- .../psi/impl/references/PyReferenceImpl.java | 12 +++++----- .../testData/quickdoc/HoverOverOverloads.html | 2 +- python/testData/quickdoc/Overloads.html | 2 +- .../com/jetbrains/python/Py3ResolveTest.java | 9 ++++---- .../python/PyOverloadsResolutionTest.java | 22 +++++++++++++++++++ 7 files changed, 36 insertions(+), 19 deletions(-) diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/resolve/RatedResolveResult.java b/python/python-psi-api/src/com/jetbrains/python/psi/resolve/RatedResolveResult.java index 3461c987a2a2..9c33888db686 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/resolve/RatedResolveResult.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/resolve/RatedResolveResult.java @@ -61,9 +61,6 @@ public class RatedResolveResult implements ResolveResult { */ public static final int RATE_NORMAL = 0; - @ApiStatus.Experimental - public static final int RATE_LIFTED_PY_FILE_OVERLOAD = -100; - @ApiStatus.Experimental public static final int RATE_PY_FILE_OVERLOAD = -200; diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java index 5b573a3a2fcb..091ac3e8658d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java @@ -170,10 +170,7 @@ public final class PyCallExpressionHelper { final TypeEvalContext context = resolveContext.getTypeEvalContext(); final var results = forEveryScopeTakeOverloadsOtherwiseImplementations( - // Remove the artificial latest overloads from results so as not to spoil their original order - ContainerUtil.filter(subscription.getReference(resolveContext).multiResolve(false), result -> - !(result instanceof RatedResolveResult rrr) || rrr.getRate() != RatedResolveResult.RATE_LIFTED_PY_FILE_OVERLOAD - ), + List.of(subscription.getReference(resolveContext).multiResolve(false)), context ); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/references/PyReferenceImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/references/PyReferenceImpl.java index 163140b46e97..9b0560c47f97 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/references/PyReferenceImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/references/PyReferenceImpl.java @@ -252,13 +252,13 @@ public class PyReferenceImpl implements PsiReferenceEx, PsiPolyVariantReference if (r.getClass() != RatedResolveResult.class || !(r.getElement() instanceof PyFunction pyFunction)) { return StreamEx.of(r); } - int adjustedRate = r.getRate() == RatedResolveResult.RATE_PY_FILE_OVERLOAD ? - RatedResolveResult.RATE_LIFTED_PY_FILE_OVERLOAD : r.getRate(); - return StreamEx.of(PyiUtil.getOverloads(pyFunction, typeEvalContext)) + List overloads = PyiUtil.getOverloads(pyFunction, typeEvalContext); + if (overloads.isEmpty()) { + return StreamEx.of(r); + } + return StreamEx.of(overloads) .map(overload -> new RatedResolveResult(getRate(overload, typeEvalContext), overload)) - .prepend(StreamEx.ofNullable( - PyiUtil.isInsideStub(myElement) ? null : new RatedResolveResult(adjustedRate, pyFunction) - )); + .prepend(StreamEx.ofNullable(PyiUtil.isOverload(pyFunction, typeEvalContext) ? null : r)); }) .toImmutableList(); } diff --git a/python/testData/quickdoc/HoverOverOverloads.html b/python/testData/quickdoc/HoverOverOverloads.html index e9e6f4c0a7c1..bf9ba7a4b22a 100644 --- a/python/testData/quickdoc/HoverOverOverloads.html +++ b/python/testData/quickdoc/HoverOverOverloads.html @@ -1 +1 @@ -@typing.overload
def foo(p: str) -> str \ No newline at end of file +@typing.overload
def foo(p: int) -> int \ No newline at end of file diff --git a/python/testData/quickdoc/Overloads.html b/python/testData/quickdoc/Overloads.html index fdc473a6fce7..3eca99d04eed 100644 --- a/python/testData/quickdoc/Overloads.html +++ b/python/testData/quickdoc/Overloads.html @@ -1 +1 @@ -
@typing.overload
def foo(p: str) -> str
\ No newline at end of file +
@typing.overload
def foo(p: int) -> int
\ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/Py3ResolveTest.java b/python/testSrc/com/jetbrains/python/Py3ResolveTest.java index 6ebe47013c29..cea425ea65b4 100644 --- a/python/testSrc/com/jetbrains/python/Py3ResolveTest.java +++ b/python/testSrc/com/jetbrains/python/Py3ResolveTest.java @@ -379,15 +379,16 @@ public class Py3ResolveTest extends PyResolveTestCase { // PY-22971 public void testTopLevelOverloadsAndNoImplementation() { - // resolve to the last overload + // resolve to the first overload final PyFunction foo = assertResolvesTo(PyFunction.class, "foo"); final TypeEvalContext context = TypeEvalContext.codeAnalysis(myFixture.getProject(), myFixture.getFile()); + assertTrue(PyiUtil.isOverload(foo, context)); PyiUtil .getOverloads(foo, context) .forEach( overload -> { - if (overload != foo) assertTrue(PyPsiUtils.isBefore(overload, foo)); + if (overload != foo) assertTrue(PyPsiUtils.isBefore(foo, overload)); } ); } @@ -402,7 +403,7 @@ public class Py3ResolveTest extends PyResolveTestCase { // PY-22971 public void testTopLevelOverloadsAndImplementations() { - // resolve to the last overload + // resolve to the first overload because there is no subsequent implementation before the reference final PyFunction foo = assertResolvesTo(PyFunction.class, "foo"); final TypeEvalContext context = TypeEvalContext.codeAnalysis(myFixture.getProject(), myFixture.getFile()); assertTrue(PyiUtil.isOverload(foo, context)); @@ -410,7 +411,7 @@ public class Py3ResolveTest extends PyResolveTestCase { ((PyFile)foo.getContainingFile()) .getTopLevelFunctions() .forEach( - function -> assertTrue(function == foo || PyPsiUtils.isBefore(function, foo)) + function -> assertTrue(function == foo || PyPsiUtils.isBefore(foo, function)) ); } diff --git a/python/testSrc/com/jetbrains/python/PyOverloadsResolutionTest.java b/python/testSrc/com/jetbrains/python/PyOverloadsResolutionTest.java index 87252bf68997..d80ecc8d2811 100644 --- a/python/testSrc/com/jetbrains/python/PyOverloadsResolutionTest.java +++ b/python/testSrc/com/jetbrains/python/PyOverloadsResolutionTest.java @@ -159,6 +159,28 @@ public class PyOverloadsResolutionTest extends PyTestCase { """); } + // PY-77167 + public void testFirstMatchingFunctionOverloadWhenNotFollowedByImplementationInPyFile() { + doTest("str", + """ + from typing import overload + + @overload + def func(x: int) -> int: + pass + + @overload + def func(x: str) -> str: + pass + + @overload + def func(x: object) -> object: + pass + + expr = func("foo") + """); + } + private void doTest(@NotNull String expectedType, @NotNull String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);