package org.elixir_lang.psi.scope; import com.intellij.openapi.util.Key; import com.intellij.psi.*; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.scope.PsiScopeProcessor; import com.intellij.psi.util.PsiTreeUtil; import org.elixir_lang.errorreport.Logger; import org.elixir_lang.psi.*; import org.elixir_lang.psi.call.Call; import org.elixir_lang.psi.call.name.Function; import org.elixir_lang.psi.call.name.Module; import org.elixir_lang.psi.impl.ElixirPsiImplUtil; import org.elixir_lang.psi.operation.*; import org.elixir_lang.psi.operation.Type; import org.elixir_lang.reference.Callable; import org.elixir_lang.structure_view.element.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import static org.elixir_lang.grammar.parser.GeneratedParserUtilBase.DUMMY_BLOCK; import static org.elixir_lang.psi.call.name.Function.*; import static org.elixir_lang.psi.impl.ElixirPsiImplUtil.*; import static org.elixir_lang.psi.operation.Normalized.operatorIndex; import static org.elixir_lang.psi.operation.infix.Normalized.leftOperand; import static org.elixir_lang.psi.operation.infix.Normalized.rightOperand; public abstract class Variable implements PsiScopeProcessor { /* * CONSTANTS */ public static Key<Boolean> MAYBE_MACRO = new Key<Boolean>("MAYBE_MACRO"); /* * * Instance Methods * */ /* * Public Instance Methods */ /** * @param element candidate element. * @param state current state of resolver. * @return false to stop processing. */ @Override public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) { boolean keepProcessing = true; if (element instanceof Addition || element instanceof And) { keepProcessing = executeNonDeclaringScopeInfix((Infix) element, state); } else if (element instanceof ElixirAccessExpression || element instanceof ElixirAssociations || element instanceof ElixirAssociationsBase || element instanceof ElixirBitString || element instanceof ElixirList || element instanceof ElixirMapConstructionArguments || element instanceof ElixirMultipleAliases || element instanceof ElixirNoParenthesesArguments || element instanceof ElixirNoParenthesesOneArgument || element instanceof ElixirParenthesesArguments || element instanceof ElixirParentheticalStab || element instanceof ElixirStab || element instanceof ElixirStabBody || element instanceof ElixirTuple) { keepProcessing = execute(element.getChildren(), state); } else if (element instanceof ElixirContainerAssociationOperation) { keepProcessing = execute((ElixirContainerAssociationOperation) element, state); } else if (element instanceof ElixirMapArguments) { keepProcessing = execute((ElixirMapArguments) element, state); } else if (element instanceof ElixirMapOperation) { keepProcessing = execute((ElixirMapOperation) element, state); } else if (element instanceof ElixirMatchedWhenOperation) { keepProcessing = execute((ElixirMatchedWhenOperation) element, state); } else if (element instanceof ElixirStabOperation) { keepProcessing = execute((ElixirStabOperation) element, state); } else if (element instanceof ElixirStabNoParenthesesSignature) { keepProcessing = execute((ElixirStabNoParenthesesSignature) element, state); } else if (element instanceof ElixirStabParenthesesSignature) { keepProcessing = execute((ElixirStabParenthesesSignature) element, state); } else if (element instanceof ElixirStructOperation) { keepProcessing = execute((ElixirStructOperation) element, state); } else if (element instanceof ElixirVariable) { keepProcessing = executeOnVariable((PsiNamedElement) element, state); } else if (element instanceof In) { keepProcessing = execute((In) element, state); } else if (element instanceof InMatch) { // MUST be before Call as InMatch is a Call keepProcessing = execute((InMatch) element, state); } else if (element instanceof Match) { keepProcessing = execute((Match) element, state); } else if (element instanceof And || element instanceof Pipe || element instanceof Two) { keepProcessing = execute((Infix) element, state); } else if (element instanceof Type) { keepProcessing = execute((Type) element, state); } else if (element instanceof UnaryNonNumericOperation) { keepProcessing = execute((UnaryNonNumericOperation) element, state); } else if (element instanceof UnqualifiedNoArgumentsCall) { keepProcessing = executeOnMaybeVariable((UnqualifiedNoArgumentsCall) element, state); } else if (element instanceof Call) { keepProcessing = execute((Call) element, state); } else if (element instanceof QualifiedMultipleAliases) { /* Occurs when qualified call occurs over a line with assignment to a tuple, such as `Qualifier.\n{:ok, value} = call()` */ keepProcessing = execute((QualifiedMultipleAliases) element, state); } else if (element instanceof PsiFile) { // stop at file. No reason to look in directories keepProcessing = false; } else if (element instanceof QuotableKeywordList) { /* KeywordLists happen in map, struct and {@code do: <body>} matches, while KeywordKey happens only in bindQuoted */ keepProcessing = execute((QuotableKeywordList) element, state); } else { if (!(element instanceof AtNonNumericOperation || // a module attribute reference element instanceof AtUnqualifiedBracketOperation || // a module attribute reference with access element instanceof Heredoc || element instanceof BracketOperation || /* an anonymous function is a new scope, so it can't be used to declare a variable. This won't ever be hit if the element is declared in the {@code fn} signature because that upward resolution from resolveVariable stops before this level */ element instanceof ElixirAnonymousFunction || element instanceof ElixirAtom || element instanceof ElixirAtomKeyword || element instanceof ElixirCharToken || element instanceof ElixirDecimalFloat || element instanceof ElixirEmptyParentheses || element instanceof ElixirEndOfExpression || // noParenthesesManyStrictNoParenthesesExpression exists only to be marked as an error element instanceof ElixirNoParenthesesManyStrictNoParenthesesExpression || element instanceof LeafPsiElement || element instanceof Line || element instanceof PsiErrorElement || element instanceof PsiWhiteSpace || element instanceof QualifiableAlias || element instanceof QualifiedBracketOperation || element instanceof UnqualifiedBracketOperation || element instanceof WholeNumber || element.getNode().getElementType().equals(DUMMY_BLOCK))) { Logger.error(Callable.class, "Don't know how to resolve variable in match", element); } } return keepProcessing; } @Nullable @Override public <T> T getHint(@NotNull Key<T> hintKey) { return null; } @Override public void handleEvent(@NotNull Event event, @Nullable Object associated) { } /* * Protected Instance Methods */ /** * Decides whether {@code match} matches the criteria being searched for. All other {@link #execute} methods * eventually end here. * * @return {@code true} to keep processing; {@code false} to stop processing. */ protected abstract boolean executeOnVariable(@NotNull final PsiNamedElement match, @NotNull ResolveState state); protected boolean isInDeclaringScope(@NotNull Call call, @NotNull ResolveState state) { Boolean declaringScope = state.get(DECLARING_SCOPE); boolean inDeclaringScope; if (declaringScope != null) { inDeclaringScope = declaringScope; } else { inDeclaringScope = false; PsiElement maybeDeclaringScopeContext = PsiTreeUtil.getContextOfType( call, false, ElixirStabOperation.class, InMatch.class ); if (maybeDeclaringScopeContext != null) { if (maybeDeclaringScopeContext instanceof ElixirStabOperation) { ElixirStabOperation stabOperation = (ElixirStabOperation) maybeDeclaringScopeContext; PsiElement signature = stabOperation.leftOperand(); if (PsiTreeUtil.isAncestor(signature, call, false)) { inDeclaringScope = isDeclaringScope(stabOperation); } } else if (maybeDeclaringScopeContext instanceof InMatch) { InMatch inMatch = (InMatch) maybeDeclaringScopeContext; if (PsiTreeUtil.isAncestor(inMatch.leftOperand(), call, false)) { inDeclaringScope = true; } } } } return inDeclaringScope; } /* * Private Instance Methods */ private boolean execute(@NotNull final Call match, @NotNull ResolveState state) { boolean keepProcessing = true; if (org.elixir_lang.structure_view.element.CallDefinitionClause.is(match)) { PsiElement head = org.elixir_lang.structure_view.element.CallDefinitionClause.head(match); if (head != null) { PsiElement stripped = CallDefinitionHead.strip(head); if (stripped instanceof AtNonNumericOperation) { AtNonNumericOperation strippedAtNonNumericOperation = (AtNonNumericOperation) stripped; PsiElement operand = strippedAtNonNumericOperation.operand(); if (operand instanceof ElixirAccessExpression) { keepProcessing = execute(operand, state); } } else if (stripped instanceof Call) { keepProcessing = executeStrippedCallDefinitionHead((Call) stripped, state); } } } else if (Delegation.is(match)) { for (Call callDefinitionHead : Delegation.callDefinitionHeadCallList(match)) { keepProcessing = executeStrippedCallDefinitionHead(callDefinitionHead, state); if (!keepProcessing) { break; } } } else if (match.isCalling(Module.KERNEL, Function.DESTRUCTURE, 2)) { PsiElement[] finalArguments = ElixirPsiImplUtil.finalArguments(match); if (finalArguments != null) { keepProcessing = execute(finalArguments[0], state.put(DECLARING_SCOPE, true)); } } else if (match.isCallingMacro(Module.KERNEL, Function.FOR) || match.isCallingMacro(Module.KERNEL, "with")) { PsiElement[] finalArguments = ElixirPsiImplUtil.finalArguments(match); if (finalArguments != null) { PsiElement entrance = state.get(ENTRANCE); /* if the entrance isn't in the arguments, then it is part of the block and so search should start from from the last argument */ int entranceArgumentIndex = finalArguments.length - 1; // have to check in reverse order as a variable can be rebound in the `<-` and `=` of a `for` or `with` for (int i = finalArguments.length - 1; i >= 0; i--) { if (PsiTreeUtil.isAncestor(finalArguments[i], entrance, false)) { entranceArgumentIndex = i; break; } } for (int i = entranceArgumentIndex; i >= 0; i--) { PsiElement finalArgument = finalArguments[i]; /* force to be non-declaring scope, so that variable {@code do} body ({@code a} in {@code for a <- b, do: a}) will look in early arguments for declaration */ keepProcessing = execute(finalArgument, state.put(DECLARING_SCOPE, false)); if (!keepProcessing) { break; } } } } else if (match.isCallingMacro(Module.KERNEL, CASE) || match.isCallingMacro(Module.KERNEL, COND) || match.isCallingMacro(Module.KERNEL, IF) || match.isCallingMacro(Module.KERNEL, UNLESS)) { PsiElement[] finalArguments = ElixirPsiImplUtil.finalArguments(match); if (finalArguments != null && finalArguments.length > 0) { keepProcessing = execute( finalArguments[0], /* prevents variable condition (`if foo do .. end`) from counting as declaration of that variable (`foo`) */ state.put(DECLARING_SCOPE, false) ); } } else if (org.elixir_lang.structure_view.element.Quote.is(match)) { PsiElement bindQuoted = ElixirPsiImplUtil.keywordArgument(match, "bind_quoted"); if (bindQuoted instanceof ElixirAccessExpression) { PsiElement child = stripAccessExpression(bindQuoted); if (child instanceof ElixirList) { ElixirList list = (ElixirList) child; PsiElement[] listChildren = list.getChildren(); if (listChildren.length == 1) { PsiElement listChild = listChildren[0]; if (listChild instanceof ElixirKeywords) { ElixirKeywords bindQuotedKeywords = (ElixirKeywords) listChild; List<ElixirKeywordPair> keywordPairList = bindQuotedKeywords.getKeywordPairList(); for (ElixirKeywordPair keywordPair : keywordPairList) { keepProcessing = executeOnVariable(keywordPair.getKeywordKey(), state); } } } } } } else if (hasDoBlockOrKeyword(match)) { PsiElement[] finalArguments = finalArguments(match); if (finalArguments != null) { ResolveState macroArgumentsState = state.put(DECLARING_SCOPE, true); for (PsiElement finalArgument : finalArguments) { keepProcessing = execute(finalArgument, macroArgumentsState); if (!keepProcessing) { break; } } } } else { // unquote(var) can't declare var, only use it if (!match.isCalling(Module.KERNEL, UNQUOTE, 1)) { int resolvedFinalArity = match.resolvedFinalArity(); // UnqualifiedNorArgumentsCall prevents `foo()` from being treated as a variable. // resolvedFinalArity prevents `|> foo` from being counted as 0-arity if (match instanceof UnqualifiedNoArgumentsCall && resolvedFinalArity == 0) { keepProcessing = executeOnVariable((PsiNamedElement) match, state); } else if (maybeMacro(match, state)) { ResolveState maybeMacroState = state.put(MAYBE_MACRO, true); /* macros uses in stab signatures see @see https://github.com/elixir-lang/elixir/blob/0c9e72c8d7be3ee502c43762e0ccbbf244198aeb/lib/elixir/lib/stream/reducers.ex#L7 */ PsiElement[] finalArguments = finalArguments(match); if (finalArguments != null) { keepProcessing = execute(finalArguments, maybeMacroState); } } } } return keepProcessing; } /** * Only checks the right operand of the container association operation because the left operand is either a literal * or a pinned variable, which means the variable is being used and was declared elsewhere. */ private boolean execute(@NotNull final ElixirContainerAssociationOperation match, @NotNull ResolveState state) { boolean keepProcessing = true; PsiElement[] children = match.getChildren(); if (children.length > 1) { keepProcessing = execute(children[1], state); } return keepProcessing; } /** * Only checks {@link ElixirMapArguments#getMapConstructionArguments()} and not * {@link ElixirMapArguments#getMapUpdateArguments()} since an update is not valid in a pattern match. */ private boolean execute(@NotNull ElixirMapArguments match, @NotNull ResolveState state) { boolean keepProcessing = true; ElixirMapConstructionArguments mapConstructionArguments = match.getMapConstructionArguments(); if (mapConstructionArguments != null) { keepProcessing = execute(mapConstructionArguments, state); } return keepProcessing; } private boolean execute(@NotNull ElixirMapOperation match, @NotNull ResolveState state) { return execute(match.getMapArguments(), state); } private boolean execute(@NotNull ElixirMatchedWhenOperation match, @NotNull ResolveState state) { return executeLeftOperand(match, state); } private boolean execute(@NotNull ElixirStabNoParenthesesSignature match, @NotNull ResolveState state) { return execute(match.getNoParenthesesArguments(), state); } private boolean execute(@NotNull ElixirStabOperation match, @NotNull ResolveState state) { boolean keepProcessing = true; PsiElement[] children = match.getChildren(); int operatorIndex = operatorIndex(children); PsiElement leftOperand = leftOperand(children, operatorIndex); if (leftOperand != null) { keepProcessing = execute(leftOperand, state); } if (keepProcessing) { PsiElement rightOperand = rightOperand(children, operatorIndex); if (rightOperand != null) { keepProcessing = execute(rightOperand, state); } } return keepProcessing; } private boolean execute(@NotNull ElixirStabParenthesesSignature match, @NotNull ResolveState state) { return execute(match.getParenthesesArguments(), state); } private boolean execute(@NotNull ElixirStructOperation match, @NotNull ResolveState state) { return execute(match.getMapArguments(), state); } /** * {@code in} can declare variable for {@code rescue} clauses like {@code rescue e in RuntimeException ->} */ private boolean execute(@NotNull In match, @NotNull ResolveState state) { return executeLeftOperand(match, state); } /** * Infix operations where either side can declare a variable in a match */ private boolean execute(@NotNull Infix match, @NotNull ResolveState state) { boolean keepProcessing = executeLeftOperand(match, state); if (keepProcessing) { PsiElement rightOperand = match.rightOperand(); if (rightOperand != null) { keepProcessing = execute(rightOperand, state); } } return keepProcessing; } private boolean execute(@NotNull InMatch match, @NotNull ResolveState state) { Operator operator = match.operator(); String operatorText = operator.getText(); boolean keepProcessing = true; if (operatorText.equals(DEFAULT_OPERATOR)) { PsiElement defaulted = match.leftOperand(); if (defaulted instanceof PsiNamedElement) { keepProcessing = executeOnVariable((PsiNamedElement) defaulted, state); } } else if (operatorText.equals("<-")) { PsiElement entrance = state.get(ENTRANCE); PsiElement rightOperand = match.rightOperand(); // variable on right of <- can't be declared on left because right expression generates left expression if (!PsiTreeUtil.isAncestor(rightOperand, entrance, false)) { // counter {@code state.put(DECLARING_SCOPE, false)} in #boolean(Call, Resolve) for {@code for} keepProcessing = executeLeftOperand(match, state.put(DECLARING_SCOPE, true)); } } return keepProcessing; } private boolean execute(@NotNull Match match, @NotNull ResolveState state) { /* ensure DECLARING_SCOPE is `true` to counter `DECLARING_SCOPE` being `false` for -> signature under `after` or `cond` */ ResolveState matchState = state.put(DECLARING_SCOPE, true); return execute((Infix) match, matchState); } private boolean execute(@NotNull PsiElement[] parameters, @NotNull ResolveState state) { boolean keepProcessing = true; for (PsiElement parameter : parameters) { if (parameter != null) { keepProcessing = execute(parameter, state); if (!keepProcessing) { break; } } } return keepProcessing; } private boolean execute(@NotNull QualifiedMultipleAliases match, @NotNull ResolveState state) { PsiElement[] children = match.getChildren(); assert children.length == 3; // MultipleAliases assumed to be a tuple on next line return execute(children[children.length - 1], state); } private boolean execute(@NotNull QuotableKeywordList match, @NotNull ResolveState state) { List<QuotableKeywordPair> keywordPairList = match.quotableKeywordPairList(); boolean keepProcessing = true; for (QuotableKeywordPair keywordPair : keywordPairList) { keepProcessing = execute(keywordPair, state); if (!keepProcessing) { break; } } return keepProcessing; } private boolean execute(@NotNull QuotableKeywordPair match, @NotNull ResolveState state) { return execute(match.getKeywordValue(), state); } private boolean execute(@NotNull Type match, @NotNull ResolveState state) { return executeLeftOperand(match, state); } private boolean execute(@NotNull UnaryNonNumericOperation match, @NotNull ResolveState state) { Operator operator = match.operator(); String operatorText = operator.getText(); boolean keepProcessing = true; // pinned expressions cannot be declared at pin site if (!operatorText.equals("^")) { keepProcessing = execute((Call) match, state); } return keepProcessing; } private boolean executeLeftOperand(@NotNull Infix match, @NotNull ResolveState state) { boolean keepProcessing = true; PsiElement leftOperand = match.leftOperand(); if (leftOperand != null) { keepProcessing = execute(leftOperand, state); } return keepProcessing; } /** * Turns off any DECLARING_SCOPE because the {@link Infix} subclass can never be used to declare a variable in a * match. * * The rule is, if a lone variable by itself as an operand would declare that variable in a match then call, * {@link #execute(Infix, ResolveState)}; otherwise, call this method. */ private boolean executeNonDeclaringScopeInfix(@NotNull final Infix match, @NotNull ResolveState state) { return execute(match, state.put(DECLARING_SCOPE, false)); } private boolean executeOnMaybeVariable(@NotNull UnqualifiedNoArgumentsCall match, @NotNull ResolveState state) { boolean keepProcessing; // ignore piped no argument calls that really have arity-1 if (match.resolvedFinalArity() == 0) { keepProcessing = executeOnVariable(match, state); } else { keepProcessing = execute(match, state); } return keepProcessing; } private boolean executeStrippedCallDefinitionHead(@NotNull Call strippedCallDefinitionHead, @NotNull ResolveState state) { boolean keepProcessing = true; PsiElement[] finalArguments = ElixirPsiImplUtil.finalArguments(strippedCallDefinitionHead); if (finalArguments != null) { // set scope to declaring so that calls inside the arguments are treated as maybe macros keepProcessing = execute(finalArguments, state.put(DECLARING_SCOPE, true)); } return keepProcessing; } private boolean maybeMacro(@NotNull Call call, @NotNull ResolveState state) { return !hasDoBlockOrKeyword(call) && isInDeclaringScope(call, state); } }