Skip to content

Commit

Permalink
Resolve reference variables in Ecto.Query.join/3-5
Browse files Browse the repository at this point in the history
  • Loading branch information
KronicDeth committed Jun 2, 2021
1 parent 5f22efb commit 9dda66f
Showing 1 changed file with 88 additions and 16 deletions.
104 changes: 88 additions & 16 deletions src/org/elixir_lang/ecto/Query.kt
Expand Up @@ -3,28 +3,38 @@ package org.elixir_lang.ecto
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiPolyVariantReference
import com.intellij.psi.ResolveState
import com.intellij.psi.impl.source.tree.CompositeElement
import com.intellij.psi.util.isAncestor
import com.intellij.psi.util.siblings
import org.elixir_lang.NameArityRange
import org.elixir_lang.errorreport.Logger
import org.elixir_lang.psi.*
import org.elixir_lang.psi.call.Call
import org.elixir_lang.psi.call.qualification.Qualified
import org.elixir_lang.psi.impl.ElixirPsiImplUtil.ENTRANCE
import org.elixir_lang.psi.impl.call.finalArguments
import org.elixir_lang.psi.impl.stripAccessExpression
import org.elixir_lang.psi.operation.In
import org.elixir_lang.psi.scope.WhileIn.whileIn
import org.elixir_lang.structure_view.element.CallDefinitionClause.Companion.enclosingModularMacroCall
import org.elixir_lang.safeMultiResolve

object Query {
fun isDeclaringMacro(call: Call, state: ResolveState): Boolean =
call.functionName()?.let { functionName ->
when (functionName) {
FROM -> call.resolvedFinalArity() == FROM_ARITY && resolvesToEctoQuery(call, state)
JOIN_NAME_ARITY_RANGE.name -> call.resolvedFinalArity() in JOIN_NAME_ARITY_RANGE.arityRange && resolvesToEctoQuery(call, state)
FROM_NAME_ARITY_RANGE.name -> call.resolvedFinalArity() in FROM_NAME_ARITY_RANGE.arityRange && resolvesToEctoQuery(call, state)
else -> false
}
} ?: false

private fun resolvesToEctoQuery(call: Call, state: ResolveState): Boolean =
// it is not safe to call `multiResolve` on the call's reference if that `call` is currently being resolved.
if (!call.isEquivalentTo(state.get(ENTRANCE))) {
call.reference.let { it as PsiPolyVariantReference }.multiResolve(false).any { resolveResult ->
if (!isBeingResolved(call, state)) {
val reference = call.reference as PsiPolyVariantReference

safeMultiResolve(reference, false).any { resolveResult ->
if (resolveResult.isValidResult) {
resolveResult.element?.let { it as? Call }?.let { resolved ->
CallDefinitionClause.isMacro(resolved) && enclosingModularMacroCall(resolved)?.name == "Ecto.Query"
Expand All @@ -37,22 +47,40 @@ object Query {
false
}

private fun isBeingResolved(call: Call, state: ResolveState): Boolean =
call.isEquivalentTo(state.get(ENTRANCE)) || qualifierIsBeingResolved(call, state)

private fun qualifierIsBeingResolved(call: Call, state: ResolveState): Boolean =
if (call is Qualified) {
call.qualifier().isAncestor(state.get(ENTRANCE), strict = false)
} else {
false
}

fun treeWalkUp(call: Call, state: ResolveState, keepProcessing: (element: PsiElement, state: ResolveState) -> Boolean): Boolean =
call.functionName()?.let { functionName ->
when (functionName) {
FROM -> {
when (call.resolvedFinalArity()) {
FROM_ARITY -> executeOnFrom(call, state, keepProcessing)
else -> true
FROM_NAME_ARITY_RANGE.name ->
if (call.resolvedFinalArity() in FROM_NAME_ARITY_RANGE.arityRange) {
executeOnFrom(call, state, keepProcessing)
} else {
true
}
JOIN_NAME_ARITY_RANGE.name ->
if (call.resolvedFinalArity() in JOIN_NAME_ARITY_RANGE.arityRange) {
executeOnJoin(call, state, keepProcessing)
} else {
true
}
}
else -> true
}
} ?: true

fun executeOnFrom(call: Call, state: ResolveState, keepProcessing: (element: PsiElement, state: ResolveState) -> Boolean): Boolean =
call.finalArguments()?.let { finalArguments ->
executeOnIn(finalArguments[0], state, keepProcessing) && executeOnFromKeywords(finalArguments[1], state, keepProcessing)
executeOnIn(finalArguments[0], state, keepProcessing) &&
((finalArguments.size < 2) ||
executeOnFromKeywords(finalArguments[1], state, keepProcessing))
} ?: true

fun executeOnIn(fromIn: PsiElement,
Expand All @@ -67,11 +95,8 @@ object Query {
state: ResolveState,
keepProcessing: (element: PsiElement, state: ResolveState) -> Boolean): Boolean =
when (inReference) {
is UnqualifiedNoArgumentsCall<*> -> if (inReference.resolvedFinalArity() == 0) {
keepProcessing(inReference, state)
} else {
true
}
is UnqualifiedNoArgumentsCall<*> ->
inReference.resolvedFinalArity() != 0 || keepProcessing(inReference, state)
else -> {
Logger.error(logger, "Don't know how to find reference variables in Ecto query", inReference)

Expand Down Expand Up @@ -108,8 +133,55 @@ object Query {
}
}

private const val FROM = "from"
private const val FROM_ARITY = 2
fun executeOnJoin(call: Call,
state: ResolveState,
keepProcessing: (element: PsiElement, state: ResolveState) -> Boolean): Boolean? =
call.finalArguments()?.let { arguments ->
// `join(query, qual, binding \\ [], expr, opts \\ [])`
when (call.resolvedFinalArity()) {
// `join(query, qual, expr)` or `|> join(qual, expr)`
3 -> executeOnIn(arguments[arguments.lastIndex], state, keepProcessing)
// `join(query, qual, binding, expr)` or `|> join(qual, binding, expr)`
4 -> executeOnBinding(arguments[arguments.lastIndex - 1], state, keepProcessing) &&
executeOnIn(arguments[arguments.lastIndex], state, keepProcessing)
// `join(query, qual, binding, expr, opts)` or `|> join(qual, binding, expr, opts)`
5 -> executeOnBinding(arguments[arguments.lastIndex - 2], state, keepProcessing) &&
executeOnIn(arguments[arguments.lastIndex - 1], state, keepProcessing)
else -> {
Logger.error(logger, "join arity outside of range (3..5)", call)
true
}
}
}

private tailrec fun executeOnBinding(
element: PsiElement,
state: ResolveState,
keepProcessing: (element: PsiElement, state: ResolveState) -> Boolean): Boolean =
when (element) {
is ElixirAccessExpression -> executeOnBinding(element.stripAccessExpression(), state, keepProcessing)
is ElixirList -> {
val elements = element.lastChild.siblings(forward = false).filter { it.node is CompositeElement }

whileIn(elements) { executeOnBinding(it, state, keepProcessing) }
}
is QuotableKeywordList -> {
val keywordValues = element.quotableKeywordPairList().mapNotNull(QuotableKeywordPair::getKeywordValue)

whileIn(keywordValues) {
executeOnBinding(it, state, keepProcessing)
}
}
is UnqualifiedNoArgumentsCall<*> -> element.resolvedFinalArity() != 0 || keepProcessing(element, state)
else -> {
Logger.error(logger, "Don't know how to find reference variables in binding", element)

true
}
}

private val FROM_NAME_ARITY_RANGE = NameArityRange("from", 1..2)
private val JOIN_NAME_ARITY_RANGE = NameArityRange("join", 3..5)

private val logger by lazy { com.intellij.openapi.diagnostic.Logger.getInstance(Query::class.java) }
}

0 comments on commit 9dda66f

Please sign in to comment.