package org.elixir_lang.structure_view.element; import com.intellij.ide.structureView.StructureViewTreeElement; import com.intellij.ide.util.treeView.smartTree.TreeElement; import com.intellij.navigation.ItemPresentation; import com.intellij.navigation.NavigationItem; import com.intellij.openapi.util.Pair; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.commons.lang.math.IntRange; import com.intellij.psi.PsiElement; import org.elixir_lang.navigation.item_presentation.NameArity; import org.elixir_lang.navigation.item_presentation.Parent; import org.elixir_lang.psi.AtUnqualifiedNoParenthesesCall; import org.elixir_lang.psi.call.Call; import org.elixir_lang.structure_view.element.modular.Modular; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; /** * A definition for a call: either a function or a macro */ public class CallDefinition implements StructureViewTreeElement, Timed, Visible, NavigationItem { /* * Classes */ /** * All arguments to the CallDefinition constructor */ public static class Tuple { /* * Fields */ public final int arity; public final Modular modular; public final String name; public final Time time; /* * Constructor */ public Tuple(@NotNull Modular modular, @NotNull Time time, @NotNull String name, int arity) { this.arity = arity; this.modular = modular; this.name = name; this.time = time; } /* * Instance Methods */ @Override public boolean equals(Object other) { boolean equals = false; if (other != null && Tuple.class.isAssignableFrom(other.getClass())) { final Tuple tuple = (Tuple) other; equals = new EqualsBuilder() .append(arity, tuple.arity) .append(modular, tuple.modular) .append(name, tuple.name) .append(time, tuple.time).isEquals(); } return equals; } @Override public int hashCode() { return new HashCodeBuilder(17, 31) .append(arity) .append(modular) .append(name) .append(time) .toHashCode(); } } /* * Fields */ private final int arity; // keeps track of total order of all children (clauses, heads, and specifications) @NotNull private final List<TreeElement> childList = new ArrayList<TreeElement>(); @NotNull private final List<CallDefinitionClause> clauseList = new ArrayList<CallDefinitionClause>(); @NotNull private final List<CallDefinitionHead> headList = new ArrayList<CallDefinitionHead>(); @NotNull private final Modular modular; @NotNull private final String name; // is allowed to be overridden by an override function private boolean overridable; // overrides an overridable function private boolean override; private List<CallDefinitionSpecification> specificationList = new ArrayList<CallDefinitionSpecification>(); @NotNull private final Time time; /* * Constructors */ /** * @param call a def(macro)?p? call */ @Nullable public static CallDefinition fromCall(@NotNull Call call) { Modular modular = CallDefinitionClause.enclosingModular(call); CallDefinition callDefinition = null; if (modular != null) { Pair<String, IntRange> nameArityRange = CallDefinitionClause.nameArityRange(call); if (nameArityRange != null) { String name = nameArityRange.first; /* arity is assumed to be max arity in the range because that's how {@code h} and ExDoc treat functions with defaults. */ int arity = nameArityRange.second.getMaximumInteger(); Time time = CallDefinitionClause.time(call); callDefinition = new CallDefinition(modular, time, name, arity); } } return callDefinition; } public CallDefinition(@NotNull Modular modular, @NotNull Time time, @NotNull String name, int arity) { this.arity = arity; this.modular = modular; this.name = name; this.time = time; } /* * Public Instance Methods */ public int arity() { return arity; } /** * Adds clause to macro * * @param clause the new clause for the macro */ public CallDefinitionClause clause(Call clause) { Pair<String, IntRange> nameArityRange = CallDefinitionClause.nameArityRange(clause); assert nameArityRange != null; assert nameArityRange.first.equals(name); assert nameArityRange.second.getMinimumInteger() <= arity && arity <= nameArityRange.second.getMaximumInteger(); CallDefinitionClause callDefinitionClause = new CallDefinitionClause(this, clause); childList.add(callDefinitionClause); clauseList.add(callDefinitionClause); return callDefinitionClause; } public List<CallDefinitionClause> clauseList() { return clauseList; } /** * Returns the clauses of the macro * * @return the list of {@link CallDefinitionClause} elements. */ @NotNull @Override public TreeElement[] getChildren() { return childList.toArray(new TreeElement[childList.size()]); } /** * The scoping module or quote * * @return The scoping module or quote */ @NotNull public Modular getModular() { return modular; } /** * Returns the data object (usually a PSI element) corresponding to the * structure view element. * * @return the data object instance. */ @Nullable @Override public Object getValue() { Object value = null; if (headList.size() > 0) { value = headList.get(0); } else if (clauseList.size() > 0) { value = clauseList.get(0); } return value; } /** * A macro groups together one or more {@link CallDefinitionClause} elements, so it can navigate if it has clauses. * * @return {@code true} if {@link #clauseList} size is greater than 0; otherwise, {@code false}. */ @Override public boolean canNavigate() { return clauseList.size() > 0; } /** * A macro groups together one or more {@link CallDefinitionClause} elements, so it can navigate if it has clauses. * * @return {@code true} if {@link #clauseList} size is greater than 0; otherwise, {@code false}. */ @Override public boolean canNavigateToSource() { return clauseList.size() > 0; } @NotNull @Override public String getName() { return name + "/" + arity; } /** * Returns the presentation of the tree element. * * @return the element presentation. */ @NotNull @Override public ItemPresentation getPresentation() { ItemPresentation itemPresentation = modular.getPresentation(); String location = null; if (itemPresentation instanceof Parent) { Parent parentPresentation = (Parent) itemPresentation; location = parentPresentation.getLocatedPresentableText(); } // pseudo-named-arguments boolean callback = false; //noinspection ConstantConditions return new NameArity( location, callback, time, visibility(), overridable, override, name, arity ); } /** * Unlike a clause, a head is just the name and arguments, without the outer macro calls. Heads occur in * {@code Kernel.defdelegate/2}. * * @param call */ public void head(Call head) { Pair<String, IntRange> nameArityRange = CallDefinitionHead.nameArityRange(head); assert nameArityRange != null; assert nameArityRange.first.equals(name); assert nameArityRange.second.getMinimumInteger() <= arity && arity <= nameArityRange.second.getMaximumInteger(); CallDefinitionHead callDefinitionHead = new CallDefinitionHead(this, Visibility.PUBLIC, head); childList.add(callDefinitionHead); headList.add(callDefinitionHead); } /** * @return {@code true} if this function can be overridden when the outer quote is {@code use}d */ public boolean isOverridable() { return overridable; } /** * The clause that matches the {@code arguments}. * * @param arguments the arguments the clause's arguments must match * @return {@code null} if no clauses match or if more than one clause match */ @Nullable public CallDefinitionClause matchingClause(PsiElement[] arguments) { CallDefinitionClause clause = null; List<CallDefinitionClause> clauseList = matchingClauseList(arguments); if (clauseList != null && clauseList.size() == 1) { clause = clauseList.get(0); } return clause; } /** * All clauses that match the {@code arguments}. * * @param arguments the arguments the clauses' arguments must match * @return {@code null} if no clauses match; multiple clauses if the types of arguments cannot be inferred and * simpler, relaxed matching has to be used. */ @Nullable public List<CallDefinitionClause> matchingClauseList(PsiElement[] arguments) { List<CallDefinitionClause> clauseList = null; for (CallDefinitionClause clause : this.clauseList) { if (clause.isMatch(arguments)) { if (clauseList == null) { clauseList = new ArrayList<CallDefinitionClause>(1); } clauseList.add(clause); } } return clauseList; } @NotNull public String name() { return name; } /** * Navigates to first clause in {@link #clauseList}. * * @param requestFocus <code>true</code> if focus requesting is necessary */ @Override public void navigate(@SuppressWarnings("unused") boolean requestFocus) { if (canNavigate()) { clauseList.get(0).navigate(requestFocus); } } /** * When the defined call is usable * * @return {@link Timed.Time#COMPILE} for compile time ({@code defmacro}, {@code defmacrop}); * {@link Timed.Time#RUN} for run time {@code def}, {@code defp}) */ @Override @NotNull public Time time() { return time; } /** * Set that this function overrides an overridable function * * @param override {@code true} to mark as an override of another function; {@code false} to mark as an independent * function */ public void setOverride(boolean override) { this.override = override; } /** * Set that this function can be overridden by another function of the same name and arity. * * @param overridable {@code true} to mark as overridable by another function of the same name and arity; * {@code false} to make as non-overridable. */ public void setOverridable(boolean overridable) { this.overridable = overridable; } /** * @param moduleAttributeDefinition */ public void specification(AtUnqualifiedNoParenthesesCall moduleAttributeDefinition) { Pair<String, Integer> nameArity = CallDefinitionSpecification.moduleAttributeNameArity( moduleAttributeDefinition ); assert nameArity != null; assert nameArity.first.equals(name); assert nameArity.second == arity; // pseudo-named-arguments boolean callback = false; Timed.Time time = Time.RUN; //noinspection ConstantConditions CallDefinitionSpecification callDefinitionSpecification = new CallDefinitionSpecification( modular, moduleAttributeDefinition, callback, time ); childList.add(callDefinitionSpecification); specificationList.add(callDefinitionSpecification); } /** * The visibility of the element. * * @return {@link Visibility#PUBLIC} for public call definitions ({@code def} and {@code defmacro}); * {@link Visibility#PRIVATE} for private call definitions ({@code defp} and {@code defmacrop}); {@code null} for * a mix of visibilities, such as when a call definition has a mix of call definition clause visibilities, which * is invalid code, but can occur temporarily while code is being edited. */ @Nullable @Override public Visibility visibility() { int privateCount = 0; int publicCount = 0; for (CallDefinitionHead callDefinitionHead : headList) { switch (callDefinitionHead.visibility()) { case PRIVATE: privateCount++; break; case PUBLIC: publicCount++; break; } } for (CallDefinitionClause callDefinitionClause : clauseList) { switch (callDefinitionClause.visibility()) { case PRIVATE: privateCount++; break; case PUBLIC: publicCount++; break; } } Visibility callDefinitionVisibility; if (privateCount > 0 && publicCount == 0) { callDefinitionVisibility = Visibility.PRIVATE; } else if (privateCount == 0 && publicCount > 0) { callDefinitionVisibility = Visibility.PUBLIC; } else { callDefinitionVisibility = null; } return callDefinitionVisibility; } }