From 212a3007dbc6e1235aa2ef60cc053316221b447b Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 16 Oct 2024 11:48:47 +0200 Subject: [PATCH] feat(spring-criteria): support for in/nin operators INTELLIJ-104 (#77) --- CHANGELOG.md | 2 + .../glossary/JavaDriverDialectParser.kt | 143 ++++++++-------- .../SpringCriteriaDialectParser.kt | 39 ++++- .../springcriteria/IntegrationTest.kt | 14 +- .../SpringCriteriaDialectParserTest.kt | 153 ++++++++++++++++++ 5 files changed, 279 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a389ee..5b9edbce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectParser.kt b/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectParser.kt index 78f85be5..f1dcefdf 100644 --- a/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectParser.kt +++ b/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectParser.kt @@ -165,28 +165,9 @@ object JavaDriverDialectParser : DialectParser { 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, @@ -194,48 +175,7 @@ object JavaDriverDialectParser : DialectParser { ) } } else if (filter.argumentList.expressionCount > 2) { - val allConstants: List> = filter.argumentList.expressions.slice( - 1.. { } } -private fun PsiType.isArray(): Boolean { +fun PsiExpressionList.inferFromSingleArrayArgument(start: Int = 0): HasValueReference.ValueReference { + 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 } @@ -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) { @@ -492,3 +452,54 @@ private fun PsiType.guessIterableContentType(project: Project): BsonType { GlobalSearchScope.everythingScope(project) ).toBsonType() } + +fun PsiExpressionList.inferValueReferenceFromVarArg(start: Int = 0): HasValueReference.ValueReference { + val allConstants: List> = expressions.slice( + start.. { + 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)) + } +} diff --git a/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParser.kt b/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParser.kt index 8b3c89c5..b365c98b 100644 --- a/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParser.kt +++ b/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParser.kt @@ -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 @@ -291,7 +292,10 @@ object SpringCriteriaDialectParser : DialectParser { // 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() } @@ -323,7 +327,7 @@ object SpringCriteriaDialectParser : DialectParser { // 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 @@ -408,10 +412,41 @@ object SpringCriteriaDialectParser : DialectParser { } private fun inferValueReference(valueMethodCall: PsiMethodCallExpression): HasValueReference { + val method = valueMethodCall.fuzzyResolveMethod() ?: return HasValueReference( + HasValueReference.Unknown as HasValueReference.ValueReference + ) + + 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 { + val valueReference: HasValueReference.ValueReference = + 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(valueReference) + } + private fun psiExpressionToValueReference(valuePsi: PsiExpression?): HasValueReference { val (_, value) = valuePsi?.tryToResolveAsConstant() ?: (false to null) val valueReference = when (value) { diff --git a/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/IntegrationTest.kt b/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/IntegrationTest.kt index 2ecdd460..3b391e89 100644 --- a/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/IntegrationTest.kt +++ b/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/IntegrationTest.kt @@ -349,13 +349,15 @@ inline fun > assertions: T.() -> Unit ) { val ref = component>() - 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}" ) } } @@ -364,7 +366,9 @@ inline fun > Node Unit ) { val ref = component>() - assertNotNull(ref) + assertNotEquals(null, ref) { + "Could not find a HasFieldReference component in the query." + } if (ref!!.reference is T) { (ref.reference as T).assertions() @@ -379,7 +383,9 @@ inline fun > Node Unit ) { val ref = component>() - assertNotNull(ref) + assertNotEquals(null, ref) { + "Could not find a HasValueReference component in the query." + } if (ref!!.reference is T) { (ref.reference as T).assertions() diff --git a/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParserTest.kt b/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParserTest.kt index 92320fc5..47137713 100644 --- a/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParserTest.kt +++ b/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParserTest.kt @@ -5,7 +5,9 @@ import com.intellij.openapi.command.WriteCommandAction import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.mongodb.jbplugin.mql.BsonAny import com.mongodb.jbplugin.mql.BsonAnyOf +import com.mongodb.jbplugin.mql.BsonArray import com.mongodb.jbplugin.mql.BsonBoolean import com.mongodb.jbplugin.mql.BsonInt32 import com.mongodb.jbplugin.mql.BsonNull @@ -787,4 +789,155 @@ class Repository { } } } + + @ParsingTest( + fileName = "Book.java", + """ +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.query.Criteria; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; +import static org.springframework.data.mongodb.core.query.Update.update; + +import java.util.List; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +@Document +record Book() {} + +class Repository { + private final MongoTemplate template; + + public Repository(MongoTemplate template) { + this.template = template; + } + + public Book randomBook() { + return template.find(query(where("field").in(1, 2, 3)), Book.class); + } +} + """ + ) + fun `supports the in operator with varargs`( + psiFile: PsiFile + ) { + val query = psiFile.getQueryAtMethod("Repository", "randomBook") + SpringCriteriaDialectParser.parse(query).assert(IsCommand.CommandType.FIND_MANY) { + collection> { + assertEquals("book", collection) + } + + filterN(0, Name.IN) { + field> { + assertEquals("field", fieldName) + } + value> { + assertEquals(listOf(1, 2, 3), value) + assertEquals(BsonArray(BsonAnyOf(BsonInt32, BsonNull)), type) + } + } + } + } + + @ParsingTest( + fileName = "Book.java", + """ +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.query.Criteria; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; +import static org.springframework.data.mongodb.core.query.Update.update; + +import java.util.List; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +@Document +record Book() {} + +class Repository { + private final MongoTemplate template; + + public Repository(MongoTemplate template) { + this.template = template; + } + + public Book randomBook() { + return template.find(query(where("field").in(new Int[] { 1, 2, 3 })), Book.class); + } +} + """ + ) + fun `supports the in operator with an array of values`( + psiFile: PsiFile + ) { + val query = psiFile.getQueryAtMethod("Repository", "randomBook") + SpringCriteriaDialectParser.parse(query).assert(IsCommand.CommandType.FIND_MANY) { + collection> { + assertEquals("book", collection) + } + + filterN(0, Name.IN) { + field> { + assertEquals("field", fieldName) + } + value> { + assertEquals(BsonArray(BsonAny), type) + } + } + } + } + + @ParsingTest( + fileName = "Book.java", + """ +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.query.Criteria; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; +import static org.springframework.data.mongodb.core.query.Update.update; + +import java.util.List; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +@Document +record Book() {} + +class Repository { + private final MongoTemplate template; + + public Repository(MongoTemplate template) { + this.template = template; + } + + public Book randomBook(List myFieldValues) { + return template.find(query(where("field").nin(myFieldValues)), Book.class); + } +} + """ + ) + fun `supports the in operator with a runtime list of parameters`( + psiFile: PsiFile + ) { + val query = psiFile.getQueryAtMethod("Repository", "randomBook") + SpringCriteriaDialectParser.parse(query).assert(IsCommand.CommandType.FIND_MANY) { + collection> { + assertEquals("book", collection) + } + + filterN(0, Name.NIN) { + field> { + assertEquals("field", fieldName) + } + value> { + assertEquals(BsonArray(BsonAnyOf(BsonString, BsonNull)), type) + } + } + } + } }