PY-74257 Unresolved method reference is not highlighted for a derived class when the superclass is three levels package deep

- improve resolve algorithm wrt. some corner cases related to packages

GitOrigin-RevId: 67268a2d8f8a4614ea4bbdbbdaa613c0fa165ef4
This commit is contained in:
Marcus Mews
2025-09-02 10:34:46 +00:00
committed by intellij-monorepo-bot
parent 76479bf81d
commit 718d7187f3
21 changed files with 172 additions and 34 deletions

View File

@@ -61,10 +61,10 @@ public final class ScopeUtil {
}
/**
* Return the scope owner for the element.
*
* Return the scope owner for the element. This also applies for elements of instance {@code AstScopeOwner}.
* <br/>
* Scope owner is not always the first ScopeOwner parent of the element. Some elements are resolved in outer scopes.
*
* <br/>
* This method does not access AST if underlying PSI is stub based.
*/
public static @Nullable ScopeOwner getScopeOwner(final @Nullable PsiElement element) {

View File

@@ -35,10 +35,7 @@ import com.jetbrains.python.psi.impl.*;
import com.jetbrains.python.psi.search.PySearchUtilBase;
import com.jetbrains.python.psi.stubs.PyClassAttributesIndex;
import com.jetbrains.python.psi.stubs.PyFunctionNameIndex;
import com.jetbrains.python.psi.types.PyClassLikeType;
import com.jetbrains.python.psi.types.PyClassType;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.pyi.PyiFile;
import com.jetbrains.python.pyi.PyiUtil;
import one.util.streamex.StreamEx;
@@ -285,19 +282,24 @@ public final class PyResolveUtil {
final List<String> remainingNames = qualifiedName.removeHead(1).getComponents();
final List<RatedResolveResult> result = StreamEx.of(remainingNames).foldLeft(StreamEx.of(unqualifiedResults), (prev, name) ->
prev
.map(RatedResolveResult::getElement)
.select(PyTypedElement.class)
.map(context::getType)
.nonNull()
.flatMap(type -> {
// An instance type has access to instance attributes defined in __init__, a class type does not.
final PyType instanceType = type instanceof PyClassLikeType ? ((PyClassLikeType)type).toInstance() : type;
final List<? extends RatedResolveResult> results = instanceType.resolveMember(name, null, AccessDirection.READ, resolveContext);
return results != null ? StreamEx.of(results) : StreamEx.<RatedResolveResult>empty();
}))
.toList();
prev
.map(RatedResolveResult::getElement)
.select(PyTypedElement.class)
.map(context::getType)
.nonNull()
.flatMap(type -> {
assert type != null; // see filter nonNull()
// An instance type has access to instance attributes defined in __init__, a class type does not.
final PyType instanceType = type instanceof PyClassLikeType ? ((PyClassLikeType)type).toInstance() : type;
final List<? extends RatedResolveResult> results = instanceType instanceof PyModuleType moduleType
? moduleType.resolveModuleMember(name, scopeOwner, AccessDirection.READ,
resolveContext)
: instanceType.resolveMember(name, null, AccessDirection.READ,
resolveContext);
return results != null ? StreamEx.of(results) : StreamEx.<RatedResolveResult>empty();
}))
.toList();
return PyUtil.filterTopPriorityElements(result);
}

View File

@@ -26,8 +26,10 @@ import com.jetbrains.python.codeInsight.mlcompletion.PyCompletionMlElementInfo;
import com.jetbrains.python.codeInsight.mlcompletion.PyCompletionMlElementKind;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyImportedModule;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.impl.ResolveResultList;
import com.jetbrains.python.psi.resolve.*;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -61,19 +63,28 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
@Nullable PyExpression location,
@NotNull AccessDirection direction,
@NotNull PyResolveContext resolveContext) {
return resolveModuleMember(name, location, direction, resolveContext);
}
@ApiStatus.Internal
public @Nullable List<? extends RatedResolveResult> resolveModuleMember(final @NotNull String name,
@Nullable PsiElement scopeOwner,
@NotNull AccessDirection direction,
@NotNull PyResolveContext resolveContext) {
if (MODULE_MEMBERS.contains(name)) {
var type = getModuleClassType();
if (type != null) {
return type.resolveMember(name, location, direction, resolveContext);
PyExpression pyExpression = scopeOwner instanceof PyExpression expression ? expression : null;
return type.resolveMember(name, pyExpression, direction, resolveContext);
}
}
return resolveMemberInPackageOrModule(null, myModule, name, location, resolveContext);
return resolveMemberInPackageOrModule(null, myModule, name, scopeOwner, resolveContext);
}
public static @Nullable List<? extends RatedResolveResult> resolveMemberInPackageOrModule(@Nullable PyImportedModule importedModule,
@NotNull PsiFileSystemItem anchor,
@NotNull String name,
@Nullable PyExpression location,
@Nullable PsiElement location,
@NotNull PyResolveContext resolveContext) {
@@ -96,7 +107,7 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
processImplicitPackageMembers(anchor, location, importedModule, n -> name.endsWith(n), results -> {
implicitMembers.addAll(convertDirsToInit(results));
return implicitMembers.isEmpty();
});
}, resolveContext);
if (!implicitMembers.isEmpty()) {
return implicitMembers;
}
@@ -115,30 +126,37 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
@Nullable PsiElement location,
@Nullable PyImportedModule importedModule,
@NotNull Predicate<String> filter,
@NotNull Processor<List<? extends RatedResolveResult>> resultProcessor) {
@NotNull Processor<List<? extends RatedResolveResult>> resultProcessor,
@Nullable PyResolveContext resolveContext) {
final PsiElement realLocation = location == null ? null : PyPsiUtils.getRealContext(location);
final List<PyImportElement> importElements = new ArrayList<>();
final PyFile module = PyUtil.as(PyUtil.turnDirIntoInit(anchor), PyFile.class);
if (anchor.getVirtualFile() == null) {
return;
}
final PsiElement footHold = location != null ? location.getContainingFile() : module;
final PsiElement footHold = realLocation != null ? realLocation.getContainingFile() : (module != null ? module : anchor);
if (footHold == null) {
return;
}
final PyImportElement origImportElement = importedModule != null ? importedModule.getImportElement() : null;
if (importedModule != null && (location == null || !inSameFile(location, importedModule))) {
if (importedModule != null && (realLocation == null || !inSameFile(realLocation, importedModule))) {
if (origImportElement != null) {
importElements.add(origImportElement);
}
}
else if (location != null) {
final ScopeOwner owner = ScopeUtil.getScopeOwner(location);
if (owner != null) {
else if (realLocation != null) {
final ScopeOwner owner = ScopeUtil.getScopeOwner(realLocation);
final boolean crawlScope =
owner != null && (resolveContext == null || resolveContext.getTypeEvalContext().maySwitchToAST(realLocation));
if (crawlScope) {
importElements.addAll(getVisibleImports(owner));
}
else if (realLocation instanceof PyFile pyFile) {
importElements.addAll(getTopImports(pyFile));
}
if (module != null) {
if (!inSameFile(location, module)) {
if (!inSameFile(realLocation, module)) {
importElements.addAll(module.getImportTargets());
}
final List<PyFromImportStatement> imports = module.getFromImports();
@@ -198,7 +216,7 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
if (location.getContainingFile().getVirtualFile() == null) {
return;
}
final ScopeOwner owner = ScopeUtil.getScopeOwner(location);
final ScopeOwner owner = location instanceof ScopeOwner ? (ScopeOwner)location : ScopeUtil.getScopeOwner(location);
if (owner == null) {
return;
}
@@ -215,8 +233,8 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
if (directChild != null && filter.test(directChild)) {
final QualifiedName mainPackage = QualifiedName.fromComponents(locationQName.getFirstComponent());
final PyImportElement packageImportElement =
visibleImports.stream().filter(el -> getImportedQNames(el).stream().anyMatch(qName -> qName.matchesPrefix(mainPackage)))
.findFirst().orElse(null);
ContainerUtil.find(visibleImports,
el -> ContainerUtil.exists(getImportedQNames(el), qName -> qName.matchesPrefix(mainPackage)));
if (packageImportElement != null) {
final List<RatedResolveResult> results =
@@ -336,6 +354,14 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
return importedQNames;
}
private static @NotNull List<PyImportElement> getTopImports(@NotNull PyFile file) {
List<PyImportElement> importElements = new ArrayList<>(file.getImportTargets());
for (PyFromImportStatement fromImportStatement : file.getFromImports()) {
importElements.addAll(Arrays.asList(fromImportStatement.getImportElements()));
}
return importElements;
}
private static @NotNull List<PyImportElement> getVisibleImports(@NotNull ScopeOwner owner) {
final List<PyImportElement> visibleImports = new ArrayList<>();
PyResolveUtil.scopeCrawlUp(new PsiScopeProcessor() {
@@ -487,7 +513,7 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
elements.addAll(ResolveResultList.getElements(results));
return true;
});
}, null);
return ContainerUtil.mapNotNull(elements,
element -> {
if (element instanceof PsiFileSystemItem) {

View File

@@ -0,0 +1,7 @@
import deriv
a1 = deriv.Simple()
a1.<warning descr="Unresolved attribute reference 'xxx' for class 'Simple'">xxx</warning>()
b2 = deriv.DerivedBaseInit()
b2.<warning descr="Unresolved attribute reference 'yyy' for class 'DerivedBaseInit'">yyy</warning>()

View File

@@ -0,0 +1,8 @@
import project.base
class Simple:
pass
class DerivedBaseInit(project.base.BaseInit):
pass

View File

@@ -0,0 +1,10 @@
import project.deriv
a1 = project.deriv.Simple()
a1.<warning descr="Unresolved attribute reference 'xxx' for class 'Simple'">xxx</warning>()
b2 = project.deriv.DerivedBase()
b2.<warning descr="Unresolved attribute reference 'yyy' for class 'DerivedBase'">yyy</warning>()
c3 = project.deriv.DerivedSub()
c3.<warning descr="Unresolved attribute reference 'zzz' for class 'DerivedSub'">zzz</warning>()

View File

@@ -0,0 +1,14 @@
import project.base
import project.base.sub
class Simple:
pass
class DerivedBase(project.base.BaseInit):
pass
class DerivedSub(project.base.sub.BaseSub):
pass

View File

@@ -0,0 +1,10 @@
import project.deriv
a1 = project.deriv.Simple()
a1.<warning descr="Unresolved attribute reference 'xxx' for class 'Simple'">xxx</warning>()
b2 = project.deriv.DerivedBase()
b2.<warning descr="Unresolved attribute reference 'yyy' for class 'DerivedBase'">yyy</warning>()
c3 = project.deriv.DerivedSub()
c3.<warning descr="Unresolved attribute reference 'zzz' for class 'DerivedSub'">zzz</warning>()

View File

@@ -0,0 +1,14 @@
import project.base
import project.base.sub
class Simple:
pass
class DerivedBase(project.base.BaseInit):
pass
class DerivedSub(project.base.sub.BaseSub):
pass

View File

@@ -0,0 +1,5 @@
import pkg
def f() -> "pkg.subpkg.mod.MyClass":
pass

View File

@@ -0,0 +1,2 @@
class MyClass:
pass

View File

@@ -3966,6 +3966,15 @@ public class Py3TypeTest extends PyTestCase {
""");
}
// PY-74257
public void testNotProperlyImportedQualifiedNameInTypeHint() {
doMultiFileTest("Any", """
from lib import f
expr = f()
""");
}
private void doTest(final String expectedType, final String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);

View File

@@ -891,6 +891,27 @@ public class PyUnresolvedReferencesInspectionTest extends PyInspectionTestCase {
});
}
// PY-74257 simple example, depth two, no packages (__init__.py files)
public void testImportFromNestedDirectory1() {
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
doMultiFileTest("Main.py");
});
}
// PY-74257 depth three with packages, but implicit package 'project', i.e., no __init__.py in directory 'project'
public void testImportFromNestedDirectory2() {
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
doMultiFileTest("Main.py");
});
}
// PY-74257 depth three with packages, like before but with an explicit package 'project'
public void testImportFromNestedDirectory3() {
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {
doMultiFileTest("Main.py");
});
}
// PY-78413
public void testAsyncAwaitWarningOnImportedFun() {
runWithLanguageLevel(LanguageLevel.getLatest(), () -> {