[python] Make PyResolveUtil.resolveQualifiedNameInScope automatically traverse scopes

(cherry picked from commit 6013fdabf6967a193a60849ec0e81e153380b4b7)

IJ-CR-151380

GitOrigin-RevId: 7f238ba2bee90f15dbe9c76cbf4e089a48f52870
This commit is contained in:
Mikhail Golubev
2024-12-11 12:21:30 +02:00
committed by intellij-monorepo-bot
parent 651f6c3310
commit 66f10b6356
4 changed files with 64 additions and 54 deletions

View File

@@ -566,7 +566,7 @@ private fun resolveDataclassParameters(
if (dataclassTransformStub != null) {
val resolvedFieldSpecifiers = dataclassTransformStub.fieldSpecifiers
.flatMap { PyResolveUtil.resolveQualifiedNameInScope(it, dataclassTransformDecorator.containingFile as ScopeOwner, context) }
.flatMap { PyResolveUtil.resolveQualifiedNameInScope(it, ScopeUtil.getScopeOwner(dataclassTransformDecorator)!!, context) }
.filterIsInstance<PyQualifiedNameOwner>()
.mapNotNull { it.qualifiedName }
.map { QualifiedName.fromDottedString(it) }

View File

@@ -43,12 +43,7 @@ class PyFunctoolsWrapsDecoratedFunctionTypeProvider : PyTypeProviderBase() {
if (it == null) return@overStub emptyList<PsiElement>()
var scopeOwner = ScopeUtil.getScopeOwner(decorator)
val wrappedQName = QualifiedName.fromDottedString(it.wrapped)
val resolved = mutableListOf<PsiElement>()
while (scopeOwner != null) {
resolved.addAll(PyResolveUtil.resolveQualifiedNameInScope(wrappedQName, scopeOwner, context))
scopeOwner = ScopeUtil.getScopeOwner(scopeOwner)
}
resolved
PyResolveUtil.resolveQualifiedNameInScope(wrappedQName, scopeOwner!!, context)
}
.overAst {
val wrappedExpr = it.argumentList?.getValueExpressionForParam(PyKnownDecoratorUtil.FunctoolsWrapsParameters.WRAPPED)

View File

@@ -2125,12 +2125,7 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
}
if (scopeOwner != null && qualifiedName != null) {
List<PsiElement> results = new ArrayList<>();
while (scopeOwner != null) {
results.addAll(PyResolveUtil.resolveQualifiedNameInScope(qualifiedName, scopeOwner, context));
scopeOwner = ScopeUtil.getScopeOwner(scopeOwner);
}
return results;
return PyResolveUtil.resolveQualifiedNameInScope(qualifiedName, scopeOwner, context);
}
return Collections.singletonList(expression);
}

View File

@@ -240,8 +240,10 @@ public final class PyResolveUtil {
}
/**
* Resolve a symbol by its qualified name, starting from the specified scope and then following the chain of type members.
* This type of resolve is stub-safe, i.e. it's not supposed to cause any un-stubbing of external files unless it explicitly
* Resolve a symbol by its qualified name, climbing up from the specified scope until the first component
* of the qualified is resolved and then following the chain of type members.
* <p>
* This type of resolve is stub-safe, i.e. it's not supposed to cause any un-stubbing of external files unless it is explicitly
* allowed by the given type evaluation context.
*
* @param qualifiedName name of a symbol to resolve
@@ -266,46 +268,21 @@ public final class PyResolveUtil {
final PyResolveContext resolveContext = PyResolveContext.defaultContext(context);
final List<? extends RatedResolveResult> unqualifiedResults;
if (scopeOwner instanceof PyiFile fileScope) {
// pyi-stubs are special cased because
// `resolveMember` delegates to `multiResolveName(..., true)` and
// it skips elements that are imported without `as`
unqualifiedResults = fileScope.multiResolveName(firstName, false);
List<RatedResolveResult> unqualifiedResults = new ArrayList<>();
ScopeOwner curScope = scopeOwner;
// Ideally, we should do `while (curScope != null && unqualifiedResults.isEmpty())` but
// because of flow insensitivity it will break down for cases like the following in
// flask_sqlalchemy/__init__.pyi:
//
// from .model import DefaultMeta as DefaultMeta, Model as Model
//
// class SQLAlchemy:
// Model: Model # Model in the type hints resolves to its own target expression
while (curScope != null) {
unqualifiedResults.addAll(resolveShortNameInSingleScope(curScope, firstName, resolveContext));
curScope = ScopeUtil.getScopeOwner(curScope);
}
else if (scopeOwner instanceof PyFunction functionScope) {
final Stream<PsiNamedElement> targets = StreamEx
.of(PsiTreeUtil.getStubChildrenOfTypeAsList(scopeOwner, PyTargetExpression.class))
.filter(it -> !it.isQualified())
.select(PsiNamedElement.class);
final Stream<PsiNamedElement> parameters = StreamEx
.of(functionScope.getParameterList().getParameters())
.select(PsiNamedElement.class);
unqualifiedResults = StreamEx
.of(targets)
.append(parameters)
.filter(it -> firstName.equals(it.getName()))
.map(it -> new RatedResolveResult(RatedResolveResult.RATE_NORMAL, it))
.append(resolveTypeParameters(functionScope, firstName))
.toList();
}
else if (scopeOwner instanceof PyTypeAliasStatement) {
unqualifiedResults = resolveTypeParameters((PyTypeParameterListOwner)scopeOwner, firstName);
}
else {
final PyType scopeType = context.getType((PyTypedElement)scopeOwner);
if (scopeType == null) return Collections.emptyList();
List<? extends RatedResolveResult> typeMembers = scopeType.resolveMember(firstName, null, AccessDirection.READ, resolveContext);
if (scopeOwner instanceof PyClass pyClass) {
unqualifiedResults = ContainerUtil.concat(ContainerUtil.notNullize(typeMembers), resolveTypeParameters(pyClass, firstName));
}
else {
unqualifiedResults = typeMembers;
}
}
final StreamEx<RatedResolveResult> initialResults;
if (ContainerUtil.isEmpty(unqualifiedResults)) {
final PsiElement builtin = PyBuiltinCache.getInstance(scopeOwner).getByName(firstName);
@@ -335,6 +312,49 @@ public final class PyResolveUtil {
return Collections.unmodifiableList(PyUtil.filterTopPriorityResults(result.toArray(RatedResolveResult[]::new)));
}
private static @NotNull List<? extends RatedResolveResult> resolveShortNameInSingleScope(@NotNull ScopeOwner scopeOwner,
@NotNull String name,
@NotNull PyResolveContext resolveContext) {
if (scopeOwner instanceof PyiFile fileScope) {
// pyi-stubs are special cased because
// `resolveMember` delegates to `multiResolveName(..., true)` and
// it skips elements that are imported without `as`
return fileScope.multiResolveName(name, false);
}
else if (scopeOwner instanceof PyFunction functionScope) {
final Stream<PsiNamedElement> targets = StreamEx
.of(PsiTreeUtil.getStubChildrenOfTypeAsList(scopeOwner, PyTargetExpression.class))
.filter(it -> !it.isQualified())
.select(PsiNamedElement.class);
final Stream<PsiNamedElement> parameters = StreamEx
.of(functionScope.getParameterList().getParameters())
.select(PsiNamedElement.class);
return StreamEx
.of(targets)
.append(parameters)
.filter(it -> name.equals(it.getName()))
.map(it -> new RatedResolveResult(RatedResolveResult.RATE_NORMAL, it))
.append(resolveTypeParameters(functionScope, name))
.toList();
}
else if (scopeOwner instanceof PyTypeAliasStatement) {
return resolveTypeParameters((PyTypeParameterListOwner)scopeOwner, name);
}
else {
final PyType scopeType = resolveContext.getTypeEvalContext().getType((PyTypedElement)scopeOwner);
if (scopeType == null) return Collections.emptyList();
List<? extends RatedResolveResult> typeMembers = scopeType.resolveMember(name, null, AccessDirection.READ, resolveContext);
if (scopeOwner instanceof PyClass pyClass) {
return ContainerUtil.concat(ContainerUtil.notNullize(typeMembers), resolveTypeParameters(pyClass, name));
}
else {
return ContainerUtil.notNullize(typeMembers);
}
}
}
@Nullable
public static String resolveStrArgument(@NotNull PyCallExpression callExpression, int index, @NotNull String keyword) {
// SUPPORTED CASES: