package org.elixir_lang.reference;
import com.google.common.collect.Sets;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.usageView.UsageViewLongNameLocation;
import com.intellij.usageView.UsageViewShortNameLocation;
import com.intellij.usageView.UsageViewTypeLocation;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ProcessingContext;
import org.elixir_lang.annonator.Parameter;
import org.elixir_lang.errorreport.Logger;
import org.elixir_lang.psi.*;
import org.elixir_lang.psi.call.Call;
import org.elixir_lang.psi.call.Named;
import org.elixir_lang.psi.call.name.Function;
import org.elixir_lang.psi.call.name.Module;
import org.elixir_lang.psi.operation.*;
import org.elixir_lang.psi.qualification.Unqualified;
import org.elixir_lang.psi.scope.variable.Variants;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.elixir_lang.psi.impl.ElixirPsiImplUtil.*;
public class Callable extends PsiReferenceBase<Call> implements PsiPolyVariantReference {
/*
*
* CONSTANTS
*
*/
/*
* Public CONSTANTS
*/
public static final Set<String> BIT_STRING_TYPES = Sets.newHashSet(
"binary",
"bits",
"bitstring",
"bytes",
"float",
"integer",
"utf16",
"utf32",
"utf8"
);
/*
* Private CONSTANTS
*/
private static final Set<String> BIT_STRING_ENDIANNESS = Sets.newHashSet(
"big",
"little",
"native"
);
private static final Set<String> BIT_STRING_SIGNEDNESS = Sets.newHashSet(
"signed",
"unsigned"
);
public static final String IGNORED = "_";
/*
*
* Static Methods
*
*/
/*
* Public Static Methods
*/
@Contract(pure = true)
@Nullable
public static String bitStringSegmentOptionElementDescription(@NotNull Call call,
@NotNull ElementDescriptionLocation location) {
String elementDescription = null;
String name = call.getName();
if (name != null) {
if (BIT_STRING_ENDIANNESS.contains(name)) {
elementDescription = bitStringEndiannessElementDescription(call, location);
} else if (BIT_STRING_SIGNEDNESS.contains(name)) {
elementDescription = bitStringSignednessElementDescription(call, location);
} else if (BIT_STRING_TYPES.contains(name)) {
elementDescription = bitStringTypeElementDescription(call, location);
}
}
// getType is @NotNull, so must have fallback
if (elementDescription == null && location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "bitstring segment option";
}
return elementDescription;
}
@Contract(pure = true)
@Nullable
private static String bitStringEndiannessElementDescription(@NotNull @SuppressWarnings("unused") PsiElement element,
@NotNull ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "bitstring endianness";
}
return elementDescription;
}
@Contract(pure = true)
@Nullable
private static String bitStringSignednessElementDescription(@NotNull @SuppressWarnings("unused") PsiElement element,
@NotNull ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "bitstring signedness";
}
return elementDescription;
}
@Contract(pure = true)
@Nullable
private static String bitStringTypeElementDescription(@NotNull @SuppressWarnings("unused") PsiElement element,
@NotNull ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "bitstring type";
}
return elementDescription;
}
/**
* Callable for any of the following built-in definers
*
* <ul>
* <li>{@code def}</li>
* <li>{@code defimpl}</li>
* <li>{@code defmacro}</li>
* <li>{@code defmacrop}</li>
* <li>{@code defmodule}</li>
* <li>{@code defp}</li>
* <li>{@code defprotocol}</li>
* </ul>
*
* @param call definer call
*/
@NotNull
public static Callable definer(@NotNull Call call) {
PsiElement functionNameElement = call.functionNameElement();
assert functionNameElement != null;
// Can't use `getStartOffsetInParent` because `functionNameElement` doesn't have to be a direct child of `call`
// Can't use `getTextOffset` because that's the offset to the navigationElement, which is nameIdentifier
int functionNameElementStartOffset = functionNameElement.getTextRange().getStartOffset();
int callStartOffset = call.getTextRange().getStartOffset();
int startOffset = functionNameElementStartOffset - callStartOffset;
return new Callable(call, new TextRange(startOffset, startOffset + functionNameElement.getTextLength()));
}
public static String ignoredElementDescription(@SuppressWarnings("unused") Call call,
ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "ignored";
}
return elementDescription;
}
@Contract(pure = true)
@SuppressWarnings("unchecked")
public static boolean isBitStreamSegmentOption(@NotNull PsiElement element) {
boolean isBitStreamSegmentOption = false;
Type type = PsiTreeUtil.getContextOfType(element, Type.class);
if (type != null) {
PsiElement typeParent = type.getParent();
if (typeParent instanceof ElixirBitString) {
PsiElement rightOperand = type.rightOperand();
if (PsiTreeUtil.isAncestor(rightOperand, element, false)) {
isBitStreamSegmentOption = isBitStreamSegmentOptionDown(rightOperand, element);
}
}
}
return isBitStreamSegmentOption;
}
@Contract(pure = true)
public static boolean isIgnored(@NotNull PsiElement element) {
boolean isIgnored = false;
if (element instanceof ElixirKeywordKey ||
element instanceof UnqualifiedNoArgumentsCall) {
PsiNamedElement psiNamedElement = (PsiNamedElement) element;
String name = psiNamedElement.getName();
if (name != null && name.equals(IGNORED)) {
isIgnored = true;
}
}
return isIgnored;
}
@Contract(pure = true)
public static boolean isParameter(@NotNull PsiElement ancestor) {
Parameter parameter = new Parameter(ancestor);
return Parameter.putParameterized(parameter).type != null;
}
public static boolean isParameterWithDefault(@NotNull PsiElement element) {
PsiElement parent = element.getParent();
boolean isParameterWithDefault = false;
if (parent instanceof InMatch) {
Infix parentOperation = (InMatch) parent;
Operator operator = parentOperation.operator();
String operatorText = operator.getText();
if (operatorText.equals(DEFAULT_OPERATOR)) {
PsiElement defaulted = parentOperation.leftOperand();
if (defaulted != null && defaulted.isEquivalentTo(element)) {
isParameterWithDefault = isParameter(parentOperation);
}
}
}
return isParameterWithDefault;
}
@Contract(pure = true)
public static boolean isVariable(@NotNull PsiElement ancestor) {
boolean isVariable = false;
if (ancestor instanceof ElixirInterpolation ||
// bound quoted variable name in {@code quote bind_quoted: [name: value] do ... end}
ancestor instanceof ElixirKeywordKey ||
ancestor instanceof ElixirStabNoParenthesesSignature ||
/* if a StabOperation is encountered before
ElixirStabNoParenthesesSignature or
ElixirStabParenthesesSignature, then must have come from body */
ancestor instanceof ElixirStabOperation ||
ancestor instanceof ElixirStabParenthesesSignature ||
ancestor instanceof InMatch ||
ancestor instanceof Match) {
isVariable = true;
} else if (ancestor instanceof ElixirAccessExpression ||
ancestor instanceof ElixirAssociations ||
ancestor instanceof ElixirAssociationsBase ||
ancestor instanceof ElixirBitString ||
ancestor instanceof ElixirBlockItem ||
ancestor instanceof ElixirBlockList ||
ancestor instanceof ElixirBracketArguments ||
ancestor instanceof ElixirContainerAssociationOperation ||
ancestor instanceof ElixirDoBlock ||
ancestor instanceof ElixirKeywordPair ||
ancestor instanceof ElixirKeywords ||
ancestor instanceof ElixirList ||
ancestor instanceof ElixirMapArguments ||
ancestor instanceof ElixirMapConstructionArguments ||
ancestor instanceof ElixirMapOperation ||
ancestor instanceof ElixirMapUpdateArguments ||
/* parenthesesArguments can be used in @spec other type declarations, so may not be variable until
ancestor call is checked */
ancestor instanceof ElixirMatchedParenthesesArguments ||
/* Happens when tuple is after `MyAlias.` when add qualified call above line with pre-existing tuple */
ancestor instanceof ElixirMultipleAliases ||
ancestor instanceof ElixirNoParenthesesOneArgument ||
ancestor instanceof ElixirNoParenthesesArguments ||
ancestor instanceof ElixirNoParenthesesKeywordPair ||
ancestor instanceof ElixirNoParenthesesKeywords ||
/* ElixirNoParenthesesManyStrictNoParenthesesExpression and ElixirNoParenthesesStrict
indicates a syntax error, but it can also occur during typing, so try searching above the syntax
error to resolve whether a variable */
ancestor instanceof ElixirNoParenthesesManyStrictNoParenthesesExpression ||
ancestor instanceof ElixirNoParenthesesStrict ||
ancestor instanceof ElixirParenthesesArguments ||
ancestor instanceof ElixirParentheticalStab ||
ancestor instanceof ElixirStab ||
ancestor instanceof ElixirStabBody ||
ancestor instanceof ElixirStructOperation ||
ancestor instanceof ElixirTuple ||
ancestor instanceof ElixirVariable ||
ancestor instanceof QualifiedAlias ||
ancestor instanceof Type) {
isVariable = isVariable(ancestor.getParent());
} else if (ancestor instanceof Call) {
// MUST be after any operations because operations also implement Call
isVariable = isVariable((Call) ancestor);
} else {
if (!(ancestor instanceof AtNonNumericOperation ||
ancestor instanceof BracketOperation ||
ancestor instanceof PsiFile ||
ancestor instanceof QualifiedMultipleAliases)) {
error("Don't know how to check if variable", ancestor);
}
}
return isVariable;
}
public static String parameterElementDescription(Call call, ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewLongNameLocation.INSTANCE || location == UsageViewShortNameLocation.INSTANCE) {
elementDescription = call.getName();
} else if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "parameter";
}
return elementDescription;
}
public static String parameterWithDefaultElementDescription(@SuppressWarnings("unused") Call call,
ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "parameter with default";
}
return elementDescription;
}
public static String variableElementDescription(Call call, ElementDescriptionLocation location) {
String elementDescription = null;
if (location == UsageViewLongNameLocation.INSTANCE || location == UsageViewShortNameLocation.INSTANCE) {
elementDescription = call.getName();
} else if (location == UsageViewTypeLocation.INSTANCE) {
elementDescription = "variable";
}
return elementDescription;
}
public static LocalSearchScope variableUseScope(@NotNull UnqualifiedNoArgumentsCall match) {
return variableUseScope((PsiElement) match);
}
/*
* Private Static Methods
*/
private static void error(@NotNull String message, @NotNull PsiElement element) {
Logger.error(Callable.class, message, element);
}
/**
* Searches downward from {@code ancestor}, only returning true if {@code element} is a type, unit, size or
* modifier.
*
* @return {@code true} if {@code element} is a type, unit, size, or modifier. i.e. something separated by
* {@code -}. * {@code false} if {@code element} is a variable used in {@code size(variable)}.
*/
private static boolean isBitStreamSegmentOptionDown(@NotNull PsiElement ancestor, @NotNull PsiElement element) {
boolean is = false;
if (ancestor.isEquivalentTo(element)) {
is = true;
} else if (ancestor instanceof Addition) {
Addition ancestorAddition = (Addition) ancestor;
PsiElement leftOperand = ancestorAddition.leftOperand();
if (leftOperand != null) {
is = isBitStreamSegmentOptionDown(leftOperand, element);
}
if (!is) {
PsiElement rightOperand = ancestorAddition.rightOperand();
if (rightOperand != null) {
is = isBitStreamSegmentOptionDown(rightOperand, element);
}
}
} else if (ancestor instanceof UnqualifiedParenthesesCall) {
Call ancestorCall = (Call) ancestor;
PsiElement functionNameElement = ancestorCall.functionNameElement();
if (functionNameElement != null && functionNameElement.isEquivalentTo(element)) {
is = true;
}
}
return is;
}
@Contract(pure = true)
private static boolean isVariable(@NotNull Call call) {
boolean isVariable;
if (call instanceof UnqualifiedNoArgumentsCall) {
String name = call.getName();
// _ is an "ignored" not a variable
if (name == null || !name.equals(IGNORED)) {
PsiElement parent = call.getParent();
isVariable = isVariable(parent);
} else {
isVariable = false;
}
} else if (call instanceof AtUnqualifiedNoParenthesesCall) {
// module attribute, so original may be a unqualified no argument type name
isVariable = false;
} else if (call.isCalling(org.elixir_lang.psi.call.name.Module.KERNEL, Function.DESTRUCTURE)) {
isVariable = true;
} else if (call.isCallingMacro(org.elixir_lang.psi.call.name.Module.KERNEL, Function.FOR)) {
isVariable = true;
} else if (call.isCalling(Module.KERNEL, Function.VAR_BANG)) {
isVariable = true;
} else {
PsiElement parent = call.getParent();
isVariable = isVariable(parent);
}
return isVariable;
}
@Nullable
private static <T> List<T> merge(@Nullable List<T> maybeFirst, @Nullable List<T> maybeSecond) {
List<T> merged = null;
if (maybeFirst != null && maybeSecond != null) {
merged = new ArrayList<T>(maybeFirst);
merged.addAll(maybeSecond);
} else if (maybeFirst != null) {
merged = maybeFirst;
} else if (maybeSecond != null) {
merged = maybeSecond;
}
return merged;
}
@NotNull
private static LocalSearchScope variableUseScope(@NotNull Call call) {
LocalSearchScope useScope = null;
switch (useScopeSelector(call)) {
case PARENT:
useScope = variableUseScope(call.getParent());
break;
case SELF:
useScope = new LocalSearchScope(call);
break;
case SELF_AND_FOLLOWING_SIBLINGS:
List<PsiElement> selfAndFollowingSiblingList = new ArrayList<PsiElement>();
PsiElement sibling = call;
while (sibling != null) {
selfAndFollowingSiblingList.add(sibling);
sibling = sibling.getNextSibling();
}
useScope = new LocalSearchScope(
selfAndFollowingSiblingList.toArray(
new PsiElement[selfAndFollowingSiblingList.size()]
)
);
break;
}
return useScope;
}
private static LocalSearchScope variableUseScope(@NotNull Match match) {
LocalSearchScope useScope;
PsiElement parent = match.getParent();
if (parent instanceof ElixirStabBody) {
@SuppressWarnings("unchecked")
PsiElement ancestor = PsiTreeUtil.getContextOfType(
parent,
ElixirAnonymousFunction.class,
ElixirBlockItem.class,
ElixirDoBlock.class,
ElixirParentheticalStab.class,
ElixirStabOperation.class
);
if (ancestor instanceof ElixirParentheticalStab) {
useScope = variableUseScope(parent);
} else {
/* all non-ElixirParentheticalStab are block-like and so could have multiple statements after the match
where the match variable is used */
useScope = followingSiblingsSearchScope(match);
}
} else if (parent instanceof PsiFile) {
useScope = followingSiblingsSearchScope(match);
} else {
useScope = variableUseScope(parent);
}
return useScope;
}
private static LocalSearchScope variableUseScope(@NotNull PsiElement ancestor) {
LocalSearchScope useScope = null;
if (ancestor instanceof ElixirAccessExpression ||
ancestor instanceof ElixirAssociations ||
ancestor instanceof ElixirAssociationsBase ||
ancestor instanceof ElixirBitString ||
ancestor instanceof ElixirBlockItem ||
ancestor instanceof ElixirBlockList ||
ancestor instanceof ElixirContainerAssociationOperation ||
ancestor instanceof ElixirDoBlock ||
ancestor instanceof ElixirKeywordPair ||
ancestor instanceof ElixirKeywords ||
ancestor instanceof ElixirList ||
ancestor instanceof ElixirMapArguments ||
ancestor instanceof ElixirMapConstructionArguments ||
ancestor instanceof ElixirMapOperation ||
ancestor instanceof ElixirMatchedParenthesesArguments ||
ancestor instanceof ElixirNoParenthesesOneArgument ||
ancestor instanceof ElixirNoParenthesesArguments ||
ancestor instanceof ElixirNoParenthesesKeywordPair ||
ancestor instanceof ElixirNoParenthesesKeywords ||
ancestor instanceof ElixirParenthesesArguments ||
ancestor instanceof ElixirParentheticalStab ||
ancestor instanceof ElixirStab ||
ancestor instanceof ElixirStabBody ||
ancestor instanceof ElixirStabNoParenthesesSignature ||
ancestor instanceof ElixirStabParenthesesSignature ||
ancestor instanceof ElixirStructOperation ||
ancestor instanceof ElixirTuple ||
ancestor instanceof InMatch ||
ancestor instanceof Type ||
ancestor instanceof UnqualifiedNoArgumentsCall) {
useScope = variableUseScope(ancestor.getParent());
} else if (ancestor instanceof ElixirStabOperation ||
ancestor instanceof QualifiedAlias) {
useScope = new LocalSearchScope(ancestor);
} else if (ancestor instanceof Match) {
useScope = variableUseScope((Match) ancestor);
} else if (ancestor instanceof Call) {
useScope = variableUseScope((Call) ancestor);
} else if (ancestor instanceof ElixirMapUpdateArguments || ancestor instanceof ElixirInterpolation) {
/* no variable can be declared inside these classes, so this is a variable usage missing a declaration,
so it has no use scope */
useScope = LocalSearchScope.EMPTY;
} else {
error("Don't know how to find variable use scope", ancestor);
}
return useScope;
}
/*
* Constructors
*/
public Callable(@NotNull Call call) {
super(call);
}
private Callable(@NotNull Call call, @NotNull TextRange rangeInCall) {
super(call, rangeInCall);
}
@Override
public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
return ((NamedElement) myElement).setName(newElementName);
}
/*
*
* Instance Methods
*
*/
/*
* Public Instance Methods
*/
/**
* Returns the array of String, {@link PsiElement} and/or {@link com.intellij.codeInsight.lookup.LookupElement}
* instances representing all identifiers that are visible at the location of the reference. The contents
* of the returned array is used to build the lookup list for basic code completion. (The list
* of visible identifiers may not be filtered by the completion prefix string - the
* filtering is performed later by IDEA core.)
*
* Qualified completion is handled by
* {@link org.elixir_lang.code_insight.completion.provider.CallDefinitionClause#addCompletions(CompletionParameters, ProcessingContext, CompletionResultSet)}
*
* @return the array of available identifiers.
*/
@NotNull
@Override
public Object[] getVariants() {
List<LookupElement> lookupElementList = null;
if (myElement instanceof Unqualified) {
List<LookupElement> variableLookupElementList = null;
if (myElement instanceof UnqualifiedNoArgumentsCall) {
variableLookupElementList = Variants.lookupElementList(myElement);
}
List<LookupElement> callDefinitionClauseLookupElementList =
org.elixir_lang.psi.scope.call_definition_clause.Variants.lookupElementList(myElement);
lookupElementList = merge(variableLookupElementList, callDefinitionClauseLookupElementList);
}
Object[] variants;
if (lookupElementList == null) {
variants = new Object[0];
} else {
variants = lookupElementList.toArray(new Object[lookupElementList.size()]);
}
return variants;
}
/**
* Returns the results of resolving the reference.
*
* @param incompleteCode if true, the code in the context of which the reference is
* being resolved is considered incomplete, and the method may return additional
* invalid results.
* @return the array of results for resolving the reference.
*/
@NotNull
@Override
public ResolveResult[] multiResolve(boolean incompleteCode) {
final List<ResolveResult> resolveResultList = new ArrayList<ResolveResult>();
int resolvedFinalArity = myElement.resolvedFinalArity();
if (myElement instanceof org.elixir_lang.psi.call.qualification.Qualified) {
Call modular = qualifiedToModular((org.elixir_lang.psi.call.qualification.Qualified) myElement);
/* If modular cannot be found then it means that either the qualifier has a typo or its part of
.beam-only Module. Since .beam-only Modules aren't resolvable at this time, assume typo and mark all
ResolveResults with `validResult` `false. Finally, it could also be a variable. */
if (modular != null) {
Modular.forEachCallDefinitionClauseNameIdentifier(
modular,
myElement.functionName(),
resolvedFinalArity,
new com.intellij.util.Function<PsiElement, Boolean>() {
@Override
public Boolean fun(PsiElement nameIdentifier) {
resolveResultList.add(new PsiElementResolveResult(nameIdentifier, true));
return true;
}
}
);
}
} else {
/* DO NOT use `getName()` as it will return the NameIdentifier's text, which for `defmodule` is the Alias,
not `defmodule` */
String name = myElement.functionName();
if (name != null) {
// UnqualifiedNorArgumentsCall prevents `foo()` from being treated as a variable.
// resolvedFinalArity prevents `|> foo` from being counted as 0-arity
if (myElement instanceof UnqualifiedNoArgumentsCall && resolvedFinalArity == 0) {
List<ResolveResult> variableResolveList =
org.elixir_lang.psi.scope.variable.MultiResolve.resolveResultList(
name,
incompleteCode,
myElement
);
if (variableResolveList != null) {
resolveResultList.addAll(variableResolveList);
}
}
List<ResolveResult> callDefinitionClauseResolveResultList =
org.elixir_lang.psi.scope.call_definition_clause.MultiResolve.resolveResultList(
name,
resolvedFinalArity,
incompleteCode,
myElement
);
if (callDefinitionClauseResolveResultList != null) {
resolveResultList.addAll(callDefinitionClauseResolveResultList);
}
}
}
return resolveResultList.toArray(new ResolveResult[resolveResultList.size()]);
}
/**
* Returns the element which is the target of the reference.
*
* @return the target element, or null if it was not possible to resolve the reference to a valid target.
*/
@Nullable
@Override
public PsiElement resolve() {
ResolveResult[] resolveResults = multiResolve(false);
return resolveResults.length == 1 ? resolveResults[0].getElement() : null;
}
/*
* Protected Instance Methods
*/
@Override
protected TextRange calculateDefaultRangeInElement() {
TextRange defaultRangeInElement = null;
TextRange myElementRangeInDocument = myElement.getTextRange();
int myElementStartOffset = myElementRangeInDocument.getStartOffset();
if (myElement instanceof Named) {
Named named = (Named) myElement;
PsiElement nameIdentifier = named.getNameIdentifier();
if (nameIdentifier != null) {
TextRange nameIdentifierRangeInDocument = nameIdentifier.getTextRange();
defaultRangeInElement = new TextRange(
nameIdentifierRangeInDocument.getStartOffset() - myElementStartOffset,
nameIdentifierRangeInDocument.getEndOffset() - myElementStartOffset
);
}
}
if (defaultRangeInElement == null) {
defaultRangeInElement = new TextRange(
0,
myElementRangeInDocument.getEndOffset() - myElementStartOffset
);
}
return defaultRangeInElement;
}
}