package org.elixir_lang.psi.scope.variable;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Key;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import org.elixir_lang.psi.*;
import org.elixir_lang.psi.call.Call;
import org.elixir_lang.psi.impl.ElixirPsiImplUtil;
import org.elixir_lang.psi.operation.Match;
import org.elixir_lang.psi.scope.Variable;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.elixir_lang.psi.impl.ElixirPsiImplUtil.DECLARING_SCOPE;
import static org.elixir_lang.psi.impl.ElixirPsiImplUtil.ENTRANCE;
import static org.elixir_lang.reference.Callable.IGNORED;
/**
* Finds
*/
public class MultiResolve extends Variable {
/*
* CONSTANTS
*/
private static final Key<PsiElement> LAST_BINDING_KEY = new Key<PsiElement>("LAST_BINDING_KEY");
/*
*
* Static Methods
*
*/
/*
* Public Static Methods
*/
@NotNull
private final String name;
private final boolean incompleteCode;
/*
* Private Static Methods
*/
@Nullable
private List<ResolveResult> resolveResultList = null;
public MultiResolve(@NotNull String name, boolean incompleteCode) {
this.incompleteCode = incompleteCode;
this.name = name;
}
@Nullable
public static List<ResolveResult> resolveResultList(@NotNull String name,
boolean incompleteCode,
@NotNull PsiElement entrance) {
List<ResolveResult> resolveResultList;
if (name.equals(IGNORED)) {
resolveResultList = Collections.<ResolveResult>singletonList(new PsiElementResolveResult(entrance));
} else {
resolveResultList = resolveResultList(name, incompleteCode, entrance, ResolveState.initial());
}
return resolveResultList;
}
/*
* Fields
*/
@Nullable
public static List<ResolveResult> resolveResultList(@NotNull String name,
boolean incompleteCode,
@NotNull PsiElement entrance,
@NotNull ResolveState resolveState) {
MultiResolve multiResolve = new MultiResolve(name, incompleteCode);
ResolveState treeWalkUpResolveState = resolveState;
if (treeWalkUpResolveState.get(ENTRANCE) == null) {
treeWalkUpResolveState = treeWalkUpResolveState.put(ENTRANCE, entrance);
}
PsiTreeUtil.treeWalkUp(
multiResolve,
entrance,
entrance.getContainingFile(),
treeWalkUpResolveState
);
return multiResolve.getResolveResultList();
}
@Contract(pure = true)
@Nullable
private static PsiElement previousExpression(@NotNull PsiElement element) {
PsiElement expression = ElixirPsiImplUtil.previousSiblingExpression(element);
if (expression == null) {
expression = previousParentExpression(element);
}
return expression;
}
@Contract(pure = true)
@Nullable
private static PsiElement previousParentExpression(@NotNull PsiElement element) {
PsiElement expression = element;
do {
expression = expression.getParent();
} while (expression instanceof Arguments ||
expression instanceof ElixirDoBlock ||
expression instanceof ElixirStab ||
expression instanceof ElixirStabBody);
return expression;
}
/*
*
* Instance Methods
*
*/
/*
* Public Instance Methods
*/
@Nullable
public List<ResolveResult> getResolveResultList() {
return resolveResultList;
}
/*
* Protected Instance Methods
*/
/**
* Decides whether {@code match} matches the criteria being searched for. All other {@link #executeOnVariable} methods
* eventually end here.
*
* @param match
* @param state
*/
@Override
protected boolean executeOnVariable(@NotNull PsiNamedElement match, @NotNull ResolveState state) {
addToResolveResultListIfMatchingName(match, state);
return org.elixir_lang.psi.scope.MultiResolve.keepProcessing(incompleteCode, resolveResultList);
}
/*
* Private Instance Methods
*/
/**
* Adds {@code resolveResult} to {@link #resolveResultList} if it exists; otherwise, create new
* {@code List<ResolveList>}, set {@link #resolveResultList} to it, and add {@code resolveResult} to it.
*
* @param resolveResult The element to add to {@code resolveResultList}
*/
private void addToResolveResultList(@NotNull ResolveResult resolveResult) {
if (resolveResultList == null) {
resolveResultList = new ArrayList<ResolveResult>();
}
resolveResultList.add(resolveResult);
}
private void addToResolveResultList(@NotNull PsiElement element, ResolveState state, boolean validResult) {
Boolean declaringScope = state.get(DECLARING_SCOPE);
if (declaringScope == null || declaringScope) {
PsiElement lastBinding = state.get(LAST_BINDING_KEY);
boolean added = false;
/* if LAST_BINDING_KEY is set, then we're checking if a right-hand match is bound higher up, so an effective
recursive call. If the recursive call got the same result, stop the recursion by not checking for
rebinding */
if (lastBinding == null || !element.isEquivalentTo(lastBinding)) {
if (!(element instanceof Call) ||
!isInDeclaringScope((Call) element, state) ||
/* if maybe a macro, then it could potentially not be a macro, in which case prefer the early
declaration */
Boolean.TRUE.equals(state.get(MAYBE_MACRO))) {
Match matchAncestor = PsiTreeUtil.getContextOfType(element, Match.class);
if (matchAncestor != null) {
PsiElement rightOperand = matchAncestor.rightOperand();
if (rightOperand != null) {
/* right-hand match can only be declarative if it is not already bound, so need to try to
resolve further up to try to find if {@code element} is already bound */
if (PsiTreeUtil.isAncestor(rightOperand, element, false)) {
// previous sibling or parent to search for earlier binding
PsiElement expression = previousExpression(matchAncestor);
if (expression != null) {
List<ResolveResult> preboundResolveResultList = resolveResultList(
name,
incompleteCode,
expression,
ResolveState
.initial()
.put(ENTRANCE, matchAncestor)
.put(LAST_BINDING_KEY, element)
);
if (preboundResolveResultList != null && preboundResolveResultList.size() > 0) {
if (resolveResultList == null) {
resolveResultList = preboundResolveResultList;
} else {
resolveResultList.addAll(preboundResolveResultList);
}
added = true;
}
}
}
}
}
}
}
// either non-right match declaration or recursive call didn't find a rebinding
if (!added) {
addToResolveResultList(new PsiElementResolveResult(element, validResult));
}
}
}
private void addToResolveResultListIfMatchingName(@NotNull PsiNamedElement match, ResolveState state) {
String matchName = match.getName();
if (matchName != null) {
if (matchName.equals(name)) {
addToResolveResultList(match, state, true);
} else if (incompleteCode && matchName.startsWith(name)) {
addToResolveResultList(match, state, false);
}
}
}
}