diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverCompletionContributorTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverCompletionContributorTest.kt index 23979f50..24b5b7e6 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverCompletionContributorTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverCompletionContributorTest.kt @@ -378,7 +378,7 @@ public class Repository { .aggregate(List.of( Aggregates.project( Projections.include("") - ) + ) )); } } @@ -429,6 +429,137 @@ import java.util.List; import static com.mongodb.client.model.Filters.*; import static com.mongodb.client.model.Updates.*; +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public void exampleFind() { + client.getDatabase("myDatabase").getCollection("myCollection") + .aggregate(List.of( + Aggregates.project( + Projections.include( + List.of("") + ) + ) + )); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in Projections#include built with List#of of an Aggregates#project stage`( + fixture: CodeInsightTestFixture, + ) { + fixture.specifyDialect(JavaDriverDialect) + + val (dataSource, readModelProvider) = fixture.setupConnection() + val namespace = Namespace("myDatabase", "myCollection") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + ), + ), + ), + ), + ) + + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections; +import org.bson.Document; +import org.bson.types.ObjectId; +import java.util.Arrays; +import static com.mongodb.client.model.Filters.*; +import static com.mongodb.client.model.Updates.*; + +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public void exampleFind() { + client.getDatabase("myDatabase").getCollection("myCollection") + .aggregate(List.of( + Aggregates.project( + Projections.include( + Arrays.asList("") + ) + ) + )); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in Projections#include built with Arrays#asList of an Aggregates#project stage`( + fixture: CodeInsightTestFixture, + ) { + fixture.specifyDialect(JavaDriverDialect) + + val (dataSource, readModelProvider) = fixture.setupConnection() + val namespace = Namespace("myDatabase", "myCollection") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + ), + ), + ), + ), + ) + + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections;import org.bson.Document; +import org.bson.types.ObjectId; +import java.util.List; +import static com.mongodb.client.model.Filters.*; +import static com.mongodb.client.model.Updates.*; + public class Repository { private final MongoClient client; @@ -492,6 +623,137 @@ import java.util.List; import static com.mongodb.client.model.Filters.*; import static com.mongodb.client.model.Updates.*; +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public void exampleFind() { + client.getDatabase("myDatabase").getCollection("myCollection") + .aggregate(List.of( + Aggregates.project( + Projections.exclude( + List.of("") + ) + ) + )); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in Projections#exclude built with List#of of an Aggregates#project stage`( + fixture: CodeInsightTestFixture, + ) { + fixture.specifyDialect(JavaDriverDialect) + + val (dataSource, readModelProvider) = fixture.setupConnection() + val namespace = Namespace("myDatabase", "myCollection") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + ), + ), + ), + ), + ) + + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections; +import org.bson.Document; +import org.bson.types.ObjectId; +import java.util.Arrays; +import static com.mongodb.client.model.Filters.*; +import static com.mongodb.client.model.Updates.*; + +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public void exampleFind() { + client.getDatabase("myDatabase").getCollection("myCollection") + .aggregate(List.of( + Aggregates.project( + Projections.exclude( + Arrays.asList("") + ) + ) + )); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in Projections#exclude built with Arrays#asList of an Aggregates#project stage`( + fixture: CodeInsightTestFixture, + ) { + fixture.specifyDialect(JavaDriverDialect) + + val (dataSource, readModelProvider) = fixture.setupConnection() + val namespace = Namespace("myDatabase", "myCollection") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + ), + ), + ), + ), + ) + + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections;import org.bson.Document; +import org.bson.types.ObjectId; +import java.util.List; +import static com.mongodb.client.model.Filters.*; +import static com.mongodb.client.model.Updates.*; + public class Repository { private final MongoClient client; @@ -551,6 +813,140 @@ import com.mongodb.client.FindIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections;import org.bson.Document; +import org.bson.types.ObjectId; +import java.util.List; +import static com.mongodb.client.model.Filters.*; +import static com.mongodb.client.model.Updates.*; + +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public void exampleFind() { + client.getDatabase("myDatabase").getCollection("myCollection") + .aggregate(List.of( + Aggregates.project( + Projections.fields( + List.of( + Projections.exclude("") + ) + ) + ) + )); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in Projections#fields built with List#of of an Aggregates#project stage`( + fixture: CodeInsightTestFixture, + ) { + fixture.specifyDialect(JavaDriverDialect) + + val (dataSource, readModelProvider) = fixture.setupConnection() + val namespace = Namespace("myDatabase", "myCollection") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + ), + ), + ), + ), + ) + + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections;import org.bson.Document; +import org.bson.types.ObjectId; +import java.util.Arrays; +import static com.mongodb.client.model.Filters.*; +import static com.mongodb.client.model.Updates.*; + +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public void exampleFind() { + client.getDatabase("myDatabase").getCollection("myCollection") + .aggregate(List.of( + Aggregates.project( + Projections.fields( + Arrays.asList( + Projections.exclude("") + ) + ) + ) + )); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in Projections#fields built with Arrays#asList of an Aggregates#project stage`( + fixture: CodeInsightTestFixture, + ) { + fixture.specifyDialect(JavaDriverDialect) + + val (dataSource, readModelProvider) = fixture.setupConnection() + val namespace = Namespace("myDatabase", "myCollection") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + ), + ), + ), + ), + ) + + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.Projections; import com.mongodb.client.model.Sorts; import org.bson.Document; 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 cea4bf71..0b94f87b 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 @@ -219,7 +219,7 @@ object JavaDriverDialectParser : DialectParser { private fun isInQuery(element: PsiElement): Boolean { val methodCall = element.parentOfType(false) ?: return false - val containingClass = methodCall.resolveMethod()?.containingClass ?: return false + val containingClass = methodCall.fuzzyResolveMethod()?.containingClass ?: return false return containingClass.qualifiedName == FILTERS_FQN || containingClass.qualifiedName == UPDATES_FQN @@ -227,7 +227,15 @@ object JavaDriverDialectParser : DialectParser { private fun isInAutoCompletableAggregation(element: PsiElement): Boolean { val methodCall = element.parentOfType(false) ?: return false - val containingClass = methodCall.resolveMethod()?.containingClass ?: return false + val containingClass = methodCall.fuzzyResolveMethod()?.containingClass ?: return false + + // A few method calls that accepts variable arguments also accepts an iterable as an + // argument, for example: Projections.include(List.of()) + // For such cases, we need to look at the parent of "methodCall". + if (methodCall.isJavaIterableCallExpression()) { + return isInAutoCompletableAggregation(methodCall) + } + if ( containingClass.qualifiedName == PROJECTIONS_FQN || containingClass.qualifiedName == SORTS_FQN || @@ -1039,20 +1047,26 @@ fun PsiMethodCallExpression.getVarArgsOrIterableArgs(): List { } } +fun PsiMethodCallExpression.isJavaIterableCallExpression( + method: PsiMethod? = fuzzyResolveMethod() +): Boolean { + val isListOfCall = method?.name == "of" && + method.containingClass?.qualifiedName == JAVA_LIST_FQN + + val isArrayAsListCall = method?.name == "asList" && + method.containingClass?.qualifiedName == JAVA_ARRAYS_FQN + + return isListOfCall || isArrayAsListCall +} + /** * Helper method to resolve an expression that points to an iterable, to its actual * MethodCallExpression that was used to create iterable. Particularly it targets iterables created * using `List.of` and `Arrays.asList` methods. */ fun PsiElement.resolveToIterableCallExpression(): PsiMethodCallExpression? { - return resolveToMethodCallExpression { _, method -> - val isListOfCall = method.name == "of" && - method.containingClass?.qualifiedName == JAVA_LIST_FQN - - val isArrayAsListCall = method.name == "asList" && - method.containingClass?.qualifiedName == JAVA_ARRAYS_FQN - - isListOfCall || isArrayAsListCall + return resolveToMethodCallExpression { callExpression, method -> + callExpression.isJavaIterableCallExpression(method) } }