package org.elixir_lang.psi; import com.ericsson.otp.erlang.OtpErlangAtom; import com.ericsson.otp.erlang.OtpErlangLong; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangRangeException; import com.intellij.openapi.util.Pair; import com.intellij.psi.ElementDescriptionLocation; import com.intellij.psi.PsiElement; import com.intellij.usageView.UsageViewNodeTextLocation; import com.intellij.usageView.UsageViewTypeLocation; import com.intellij.util.Function; import gnu.trove.THashMap; import org.apache.commons.lang.math.IntRange; import org.elixir_lang.errorreport.Logger; import org.elixir_lang.psi.call.Call; 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.Map; import static org.elixir_lang.psi.call.name.Function.IMPORT; import static org.elixir_lang.psi.call.name.Module.KERNEL; import static org.elixir_lang.psi.impl.ElixirPsiImplUtil.*; import static org.elixir_lang.structure_view.element.CallDefinitionClause.nameArityRange; /** * An {@code import} call */ public class Import { /* * CONSTANTS */ private static final Function<Call, Boolean> TRUE = new Function<Call, Boolean>() { @Contract(pure = true) @NotNull @Override public Boolean fun(@NotNull @SuppressWarnings("unused") Call call) { return true; } }; /* * Public Static Methods */ /** * Calls {@code function} on each call definition clause imported by {@code importCall} while {@code function} * returns {@code true}. Stops the first time {@code function} returns {@code false} * * @param importCall an {@code import} {@link Call} (should have already been checked with {@link #is(Call)}. * @param function For {@code import Module}, called on all call definition clauses in {@code Module}; for * {@code import Module, only: [...]} called on only the call definition clauses matching names in * {@code :only} list; for {@code import Module, except: [...]} called on all call definition clauses expect those * matching names in {@code :except} list. */ public static void callDefinitionClauseCallWhile(@NotNull Call importCall, @NotNull final Function<Call, Boolean> function) { Call modularCall; try { modularCall = modular(importCall); } catch (StackOverflowError stackOverflowError) { Logger.error(Import.class, "StackvoerflowError while finding modular for import", importCall); modularCall = null; } if (modularCall != null) { final Function<Call, Boolean> optionsFilter = callDefinitionClauseCallFilter(importCall); Modular.callDefinitionClauseCallWhile( modularCall, new Function<Call, Boolean>() { @Override public Boolean fun(Call call) { return !optionsFilter.fun(call) || function.fun(call); } } ); } } @Nullable public static String elementDescription(@NotNull Call call, @NotNull ElementDescriptionLocation location) { String elementDescription = null; if (location == UsageViewTypeLocation.INSTANCE) { elementDescription = "import"; } else if (location == UsageViewNodeTextLocation.INSTANCE) { elementDescription = call.getText(); } return elementDescription; } /** * Whether {@code call} is an {@code import Module} or {@code import Module, opts} call */ public static boolean is(@NotNull Call call) { boolean is = false; if (call.isCalling(KERNEL, IMPORT)) { int resolvedFinalArity = call.resolvedFinalArity(); if (1 <= resolvedFinalArity && resolvedFinalArity <= 2) { is = true; } } return is; } /* * Private Static Methods */ @NotNull private static Map<String, List<Integer>> aritiesByNameFromNameByArityKeywordList(@NotNull final ElixirList list) { Map<String, List<Integer>> aritiesByName = new THashMap<String, List<Integer>>(); PsiElement[] children = list.getChildren(); if (children.length >= 1) { PsiElement lastChild = children[children.length - 1]; if (lastChild instanceof QuotableKeywordList) { QuotableKeywordList quotableKeywordList = (QuotableKeywordList) lastChild; for (QuotableKeywordPair quotableKeywordPair : quotableKeywordList.quotableKeywordPairList()) { String name = keywordKeyToName(quotableKeywordPair.getKeywordKey()); Integer arity = keywordValueToArity(quotableKeywordPair.getKeywordValue()); if (name != null && arity != null) { List<Integer> arities = aritiesByName.get(name); if (arities == null) { arities = new ArrayList<Integer>(); } arities.add(arity); aritiesByName.put(name, arities); } } } } return aritiesByName; } @NotNull private static Map<String, List<Integer>> aritiesByNameFromNameByArityKeywordList(PsiElement element) { Map<String, List<Integer>> aritiesByName = new THashMap<String, List<Integer>>(); PsiElement stripped = stripAccessExpression(element); if (stripped instanceof ElixirList) { aritiesByName = aritiesByNameFromNameByArityKeywordList((ElixirList) stripped); } return aritiesByName; } /** * A {@link Function} that returns {@code true} for call definition clauses that are imported by {@code importCall} * * @param importCall {@code import} call */ @NotNull private static Function<Call, Boolean> callDefinitionClauseCallFilter(@NotNull Call importCall) { PsiElement[] finalArguments = finalArguments(importCall); Function<Call, Boolean> filter = TRUE; if (finalArguments != null && finalArguments.length >= 2) { filter = optionsCallDefinitionClauseCallFilter(finalArguments[1]); } return filter; } @NotNull private static Function<Call, Boolean> exceptCallDefinitionClauseCallFilter(PsiElement element) { final Function<Call, Boolean> only = onlyCallDefinitionClauseCallFilter(element); return new Function<Call, Boolean>() { @Override public Boolean fun(Call call) { return !only.fun(call); } }; } @Nullable private static String keywordKeyToName(@NotNull final Quotable keywordKey) { OtpErlangObject quotedKeywordKey = keywordKey.quote(); String name = null; if (quotedKeywordKey instanceof OtpErlangAtom) { OtpErlangAtom keywordKeyAtom = (OtpErlangAtom) quotedKeywordKey; name = keywordKeyAtom.atomValue(); } return name; } @Nullable private static Integer keywordValueToArity(@NotNull final Quotable keywordValue) { OtpErlangObject quotedKeywordValue = keywordValue.quote(); Integer arity = null; if (quotedKeywordValue instanceof OtpErlangLong) { OtpErlangLong quotedKeywordValueLong = (OtpErlangLong) quotedKeywordValue; try { arity = quotedKeywordValueLong.intValue(); } catch (OtpErlangRangeException e) { Logger.error(Import.class, "Arity in OtpErlangLong could not be downcast to an int", keywordValue); } } return arity; } @NotNull private static Function<Call, Boolean> onlyCallDefinitionClauseCallFilter(PsiElement element) { final Map<String, List<Integer>> aritiesByName = aritiesByNameFromNameByArityKeywordList(element); return new Function<Call, Boolean>() { @Override public Boolean fun(Call call) { Pair<String, IntRange> callNameArityRange = nameArityRange(call); boolean include = false; if (callNameArityRange != null) { String callName = callNameArityRange.first; if (callName != null) { List<Integer> arities = aritiesByName.get(callName); if (arities != null) { IntRange callArityRange = callNameArityRange.second; for (int arity : arities) { if (callArityRange.containsInteger(arity)) { include = true; break; } } } } } return include; } }; } /** * The modular that is imported by {@code importCall}. * @param importCall a {@link Call} where {@link #is} is {@code true}. * @return {@code defmodule}, {@code defimpl}, or {@code defprotocol} imported by {@code importCall}. It can be * {@code null} if Alias passed to {@code importCall} cannot be resolved. */ @Nullable private static Call modular(@NotNull Call importCall) { PsiElement[] finalArguments = finalArguments(importCall); Call modular = null; if (finalArguments != null && finalArguments.length >= 1) { modular = maybeAliasToModular(finalArguments[0], importCall.getParent()); } return modular; } /** * A {@link Function} that returns {@code true} for call definition clauses that are imported by {@code importCall} * * @param options options (second argument) to an {@code import Module, ...} call. */ @Nullable private static Function<Call, Boolean> optionsCallDefinitionClauseCallFilter(@Nullable PsiElement options) { Function<Call, Boolean> filter = TRUE; if (options != null && options instanceof QuotableKeywordList) { QuotableKeywordList keywordList = (QuotableKeywordList) options; for (QuotableKeywordPair quotableKeywordPair : keywordList.quotableKeywordPairList()) { if (hasKeywordKey(quotableKeywordPair, "except")) { filter = exceptCallDefinitionClauseCallFilter(quotableKeywordPair.getKeywordValue()); } else if (hasKeywordKey(quotableKeywordPair, "only")) { filter = onlyCallDefinitionClauseCallFilter(quotableKeywordPair.getKeywordValue()); } } } return filter; } }