Skip to content

Commit

Permalink
feat(spring-criteria): support for in/nin operators INTELLIJ-104 (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
kmruiz authored Oct 16, 2024
1 parent df081b5 commit 212a300
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 72 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ MongoDB plugin for IntelliJ IDEA.
## [Unreleased]

### Added
* [INTELLIJ-104](https://jira.mongodb.org/browse/INTELLIJ-104) Add support for Spring Criteria
in/nin operator, like in `where(field).in(1, 2, 3)`
* [INTELLIJ-61](https://jira.mongodb.org/browse/INTELLIJ-61) Add support for Spring Criteria
not operator, like in `where(field).not().is(value)`
* [INTELLIJ-49](https://jira.mongodb.org/browse/INTELLIJ-49) Add support for Spring Criteria
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,77 +165,17 @@ object JavaDriverDialectParser : DialectParser<PsiElement> {
val valueReference = if (filter.argumentList.expressionCount == 2) {
var secondArg = filter.argumentList.expressions[1].meaningfulExpression() as PsiExpression
if (secondArg.type?.isJavaIterable() == true) { // case 3
HasValueReference.Runtime(
secondArg,
BsonArray(
secondArg.type?.guessIterableContentType(secondArg.project) ?: BsonAny
)
)
filter.argumentList.inferFromSingleVarArgElement(start = 1)
} else if (secondArg.type?.isArray() == false) { // case 1
val (constant, value) = secondArg.tryToResolveAsConstant()
if (constant) {
HasValueReference.Constant(
secondArg,
listOf(value),
BsonArray(value?.javaClass.toBsonType(value))
)
} else {
HasValueReference.Runtime(
secondArg,
BsonArray(
secondArg.type?.toBsonType() ?: BsonAny
)
)
}
filter.argumentList.inferFromSingleArrayArgument(start = 1)
} else { // case 2
HasValueReference.Runtime(
secondArg,
secondArg.type?.toBsonType() ?: BsonArray(BsonAny)
)
}
} else if (filter.argumentList.expressionCount > 2) {
val allConstants: List<Pair<Boolean, Any?>> = filter.argumentList.expressions.slice(
1..<filter.argumentList.expressionCount
)
.map { it.tryToResolveAsConstant() }

if (allConstants.isEmpty()) {
HasValueReference.Runtime(filter, BsonArray(BsonAny))
} else if (allConstants.all { it.first }) {
val eachType = allConstants.mapNotNull {
it.second?.javaClass?.toBsonType(it.second)
}.map {
flattenAnyOfReferences(it)
}.toSet()

if (eachType.size == 1) {
val type = eachType.first()
HasValueReference.Constant(
filter,
allConstants.map { it.second },
BsonArray(type)
)
} else {
val eachType = allConstants.mapNotNull {
it.second?.javaClass?.toBsonType(it.second)
}.toSet()
val schema = flattenAnyOfReferences(BsonAnyOf(eachType))
HasValueReference.Constant(
filter,
allConstants.map { it.second },
BsonArray(schema)
)
}
} else {
val eachType = allConstants.mapNotNull {
it.second?.javaClass?.toBsonType(it.second)
}.toSet()
val schema = BsonAnyOf(eachType)
HasValueReference.Runtime(
filter,
BsonArray(schema)
)
}
filter.argumentList.inferValueReferenceFromVarArg(start = 1)
} else {
HasValueReference.Runtime(filter, BsonArray(BsonAny))
}
Expand Down Expand Up @@ -450,11 +390,31 @@ object JavaDriverDialectParser : DialectParser<PsiElement> {
}
}

private fun PsiType.isArray(): Boolean {
fun PsiExpressionList.inferFromSingleArrayArgument(start: Int = 0): HasValueReference.ValueReference<PsiElement> {
val arrayArg = expressions[start]
val (constant, value) = arrayArg.tryToResolveAsConstant()

return if (constant) {
HasValueReference.Constant(
arrayArg,
listOf(value),
BsonArray(value?.javaClass.toBsonType(value))
)
} else {
HasValueReference.Runtime(
arrayArg,
BsonArray(
arrayArg.type?.toBsonType() ?: BsonAny
)
)
}
}

fun PsiType.isArray(): Boolean {
return this is PsiArrayType
}

private fun PsiType.isJavaIterable(): Boolean {
fun PsiType.isJavaIterable(): Boolean {
if (this !is PsiClassType) {
return false
}
Expand All @@ -474,7 +434,7 @@ private fun PsiType.isJavaIterable(): Boolean {
return return recursivelyCheckIsIterable(this)
}

private fun PsiType.guessIterableContentType(project: Project): BsonType {
fun PsiType.guessIterableContentType(project: Project): BsonType {
val text = canonicalText
val start = text.indexOf('<')
if (start == -1) {
Expand All @@ -492,3 +452,54 @@ private fun PsiType.guessIterableContentType(project: Project): BsonType {
GlobalSearchScope.everythingScope(project)
).toBsonType()
}

fun PsiExpressionList.inferValueReferenceFromVarArg(start: Int = 0): HasValueReference.ValueReference<PsiElement> {
val allConstants: List<Pair<Boolean, Any?>> = expressions.slice(
start..<expressionCount
).map { it.tryToResolveAsConstant() }

if (allConstants.isEmpty()) {
return HasValueReference.Runtime(parent, BsonArray(BsonAny))
} else if (allConstants.all { it.first }) {
val eachType = allConstants.mapNotNull {
it.second?.javaClass?.toBsonType(it.second)
}.map {
flattenAnyOfReferences(it)
}.toSet()

if (eachType.size == 1) {
val type = eachType.first()
return HasValueReference.Constant(
parent,
allConstants.map { it.second },
BsonArray(type)
)
} else {
val eachType = allConstants.mapNotNull {
it.second?.javaClass?.toBsonType(it.second)
}.toSet()
val schema = flattenAnyOfReferences(BsonAnyOf(eachType))
return HasValueReference.Constant(
parent,
allConstants.map { it.second },
BsonArray(schema)
)
}
} else {
return HasValueReference.Runtime(parent, BsonArray(BsonAny))
}
}

fun PsiExpressionList.inferFromSingleVarArgElement(start: Int = 0): HasValueReference.ValueReference<PsiElement> {
var secondArg = expressions[start].meaningfulExpression() as PsiExpression
return if (secondArg.type?.isJavaIterable() == true) { // case 3
HasValueReference.Runtime(
secondArg,
BsonArray(
secondArg.type?.guessIterableContentType(secondArg.project) ?: BsonAny
)
)
} else {
HasValueReference.Runtime(parent, BsonArray(BsonAny))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.mongodb.jbplugin.dialects.springcriteria.QueryTargetCollectionExtract
import com.mongodb.jbplugin.dialects.springcriteria.QueryTargetCollectionExtractor.extractCollectionFromQueryChain
import com.mongodb.jbplugin.dialects.springcriteria.QueryTargetCollectionExtractor.or
import com.mongodb.jbplugin.mql.BsonAny
import com.mongodb.jbplugin.mql.BsonArray
import com.mongodb.jbplugin.mql.Node
import com.mongodb.jbplugin.mql.components.*
import com.mongodb.jbplugin.mql.toBsonType
Expand Down Expand Up @@ -291,7 +292,10 @@ object SpringCriteriaDialectParser : DialectParser<PsiElement> {

// 1st scenario: vararg operations
// for example, andOperator/orOperator...
if (valueFilterMethod.isVarArgs) {
if (valueFilterMethod.isVarArgs &&
valueFilterMethod.name != "in" &&
valueFilterMethod.name != "nin"
) {
val childrenNodes = valueMethodCall.argumentList.expressions.flatMap {
parseFilterRecursively(it).reversed()
}
Expand Down Expand Up @@ -323,7 +327,7 @@ object SpringCriteriaDialectParser : DialectParser<PsiElement> {
// v--------------------- optional negation
// v------------- valueMethodCall
// v----- the value itself
// 2st scenario: $fieldRef$.$not$?.$filter$("abc")
// 2nd scenario: $fieldRef$.$not$?.$filter$("abc")
var negate = false
var fieldMethodCall =
valueMethodCall.firstChild.firstChild.meaningfulExpression() as? PsiMethodCallExpression
Expand Down Expand Up @@ -408,10 +412,41 @@ object SpringCriteriaDialectParser : DialectParser<PsiElement> {
}

private fun inferValueReference(valueMethodCall: PsiMethodCallExpression): HasValueReference<PsiElement> {
val method = valueMethodCall.fuzzyResolveMethod() ?: return HasValueReference(
HasValueReference.Unknown as HasValueReference.ValueReference<PsiElement>
)

if (method.name == "in" || method.name == "nin") {
return varargExpressionListToValueReference(valueMethodCall.argumentList)
}

val valuePsi = valueMethodCall.argumentList.expressions.getOrNull(0)
return psiExpressionToValueReference(valuePsi)
}

private fun varargExpressionListToValueReference(argumentList: PsiExpressionList, start: Int = 0): HasValueReference<PsiElement> {
val valueReference: HasValueReference.ValueReference<PsiElement> =
if (argumentList.expressionCount == (start + 1)) {
var secondArg = argumentList.expressions[start].meaningfulExpression() as PsiExpression
if (secondArg.type?.isJavaIterable() == true) { // case 3
argumentList.inferFromSingleVarArgElement(start)
} else if (secondArg.type?.isArray() == false) { // case 1
argumentList.inferFromSingleArrayArgument(start)
} else { // case 2
HasValueReference.Runtime(
secondArg,
secondArg.type?.toBsonType() ?: BsonArray(BsonAny)
)
}
} else if (argumentList.expressionCount > (start + 1)) {
argumentList.inferValueReferenceFromVarArg(start)
} else {
HasValueReference.Runtime(argumentList, BsonArray(BsonAny))
}

return HasValueReference<PsiElement>(valueReference)
}

private fun psiExpressionToValueReference(valuePsi: PsiExpression?): HasValueReference<PsiElement> {
val (_, value) = valuePsi?.tryToResolveAsConstant() ?: (false to null)
val valueReference = when (value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,13 +349,15 @@ inline fun <reified T : HasCollectionReference.CollectionReference<PsiElement>>
assertions: T.() -> Unit
) {
val ref = component<HasCollectionReference<PsiElement>>()
assertNotNull(ref)
assertNotEquals(null, ref) {
"Could not find a HasCollectionReference component in the query."
}

if (ref!!.reference is T) {
(ref.reference as T).assertions()
} else {
fail(
"Collection reference was not of type ${T::class.java.canonicalName} but ${ref::class.java.canonicalName}"
"Collection reference was not of type ${T::class.java.canonicalName} but ${ref.reference.javaClass.canonicalName}"
)
}
}
Expand All @@ -364,7 +366,9 @@ inline fun <reified T : HasFieldReference.FieldReference<PsiElement>> Node<PsiEl
assertions: T.() -> Unit
) {
val ref = component<HasFieldReference<PsiElement>>()
assertNotNull(ref)
assertNotEquals(null, ref) {
"Could not find a HasFieldReference component in the query."
}

if (ref!!.reference is T) {
(ref.reference as T).assertions()
Expand All @@ -379,7 +383,9 @@ inline fun <reified T : HasValueReference.ValueReference<PsiElement>> Node<PsiEl
assertions: T.() -> Unit
) {
val ref = component<HasValueReference<PsiElement>>()
assertNotNull(ref)
assertNotEquals(null, ref) {
"Could not find a HasValueReference component in the query."
}

if (ref!!.reference is T) {
(ref.reference as T).assertions()
Expand Down
Loading

0 comments on commit 212a300

Please sign in to comment.