Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: check ability to detect abstractions and patterns in code INTELLIJ-29 #14

Merged
merged 34 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e715487
chore: poc of basic abstractions
kmruiz Jul 3, 2024
4446cb0
chore: check constructor injection
kmruiz Jul 3, 2024
35ebbc4
chore: support for more abstractions
kmruiz Jul 3, 2024
e034bd3
chore: fix test
kmruiz Jul 3, 2024
431f85b
chore: support custom query dsl abstractions
kmruiz Jul 4, 2024
4f2c6a3
chore: support for mms patterns
kmruiz Jul 4, 2024
3a70ce8
chore: add more basic cases
kmruiz Jul 4, 2024
ae9927c
Merge branch 'main' into chore/intellij-29
kmruiz Jul 4, 2024
b356340
chore: merge with main
kmruiz Jul 4, 2024
b228c7e
chore: run test when the driver is indexed
kmruiz Jul 4, 2024
29c9dfc
chore: we need the driver for testing
kmruiz Jul 4, 2024
b23e237
chore: clean up, we don't need the loop
kmruiz Jul 4, 2024
d23ba1f
chore: add example from the basic project
kmruiz Jul 4, 2024
ba7755b
chore: fix compilation error
kmruiz Jul 4, 2024
41ae81e
chore: fix name of the file
kmruiz Jul 4, 2024
82cce34
chore: add clarifying comments, this is a bit complex
kmruiz Jul 5, 2024
d6fd3d8
chore: simplify tests
kmruiz Jul 5, 2024
3dddb52
chore: support for constants
kmruiz Jul 5, 2024
d38bc5b
chore: show namespace of query
kmruiz Jul 5, 2024
babdee4
chore: refactored so we don't need to check abstractions
kmruiz Jul 8, 2024
2507f6a
chore: add more tests and cleanup
kmruiz Jul 8, 2024
cc99300
chore: remove unused code
kmruiz Jul 8, 2024
bc689c5
chore: make sure nodes support valid components
kmruiz Jul 8, 2024
5cbccbf
chore: add inlay tests
kmruiz Jul 8, 2024
1064a3a
chore: fix reference
kmruiz Jul 8, 2024
402188a
chore: fix test that does not depend on application
kmruiz Jul 9, 2024
3665f20
chore: remove driver binary from project and use gradle dependency fo…
kmruiz Jul 9, 2024
ae05842
Merge branch 'main' into chore/intellij-29
kmruiz Jul 9, 2024
af4594e
chore: add new func to changelog
kmruiz Jul 9, 2024
65820a9
chore: rename to a likely better name
kmruiz Jul 9, 2024
4539fbc
chore: add anna's suggestions and comments
kmruiz Jul 10, 2024
198e7a6
chore: run only coverage if tests were successful
kmruiz Jul 10, 2024
5412bb5
chore: fix test flakiness
kmruiz Jul 10, 2024
4d33199
chore: fix references to new files
kmruiz Jul 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ packages/jetbrains-plugin/src/main/resources/build.properties
/**/video
/**/.DS_Store

FAKE_BROWSER_OUTPUT
FAKE_BROWSER_OUTPUT
/.kotlin

# We need the driver for tests
!packages/mongodb-dialects/java-driver/src/test/resources/mongodb-driver-sync-5.1.1.jar
!packages/jetbrains-plugin/src/test/resources/mongodb-driver-sync-5.1.1.jar
1 change: 1 addition & 0 deletions packages/jetbrains-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
implementation(project(":packages:mongodb-access-adapter:datagrip-access-adapter"))
implementation(project(":packages:mongodb-autocomplete-engine"))
implementation(project(":packages:mongodb-dialects"))
implementation(project(":packages:mongodb-dialects:java-driver"))
implementation(project(":packages:mongodb-linting-engine"))
implementation(project(":packages:mongodb-mql-model"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ class EditorToolbarDecorator : EditorFactoryListener {

private fun isEditingJavaFileWithMongoDbRelatedCode(): Boolean {
val project = editor.project ?: return false
val psiFile = PsiManager.getInstance(project).findFile(editor.virtualFile) ?: return false
val psiFileResult = runCatching { PsiManager.getInstance(project).findFile(editor.virtualFile) }

if (psiFileResult.isFailure) {
return false
}

val psiFile = psiFileResult.getOrThrow()!!

if (psiFile.language != JavaLanguage.INSTANCE) {
return false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.mongodb.jbplugin.inlays

import com.intellij.codeInsight.hints.declarative.*
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.util.parentOfType
import com.mongodb.jbplugin.dialects.javadriver.glossary.NamespaceExtractor
import com.mongodb.jbplugin.dialects.javadriver.glossary.findContainingClass
import com.mongodb.jbplugin.dialects.javadriver.glossary.isMongoDbCollectionClass

/**
* This inlay shows for the current query in which namespace is going to run, if possible,
* according to the extraction rules of NamespaceExtractor.
*
* @see NamespaceExtractor
*/
class JavaDriverQueryNamespaceInlay : InlayHintsProvider {
override fun createCollector(
file: PsiFile,
editor: Editor,
): InlayHintsCollector = QueryNamespaceInlayHintsCollector()

class QueryNamespaceInlayHintsCollector : SharedBypassCollector {
override fun collectFromElement(
element: PsiElement,
sink: InlayTreeSink,
) {
val asMethodCall = element as? PsiMethodCallExpression

val callsAcollection =
asMethodCall
?.methodExpression
?.resolve()
?.findContainingClass()
?.isMongoDbCollectionClass(
element.project,
) == true

val callsSuperClass =
asMethodCall?.findContainingClass() != null &&
asMethodCall.findContainingClass().superClass != null &&
(asMethodCall.methodExpression.resolve() as? PsiMethod)?.containingClass ==
asMethodCall.findContainingClass().superClass

if (callsAcollection || callsSuperClass) {
val namespace =
runCatching {
NamespaceExtractor.extractNamespace(element)
?: NamespaceExtractor.extractNamespace(element.parentOfType<PsiMethod>()!!)
}.getOrNull()

namespace ?: return

val documentManager = PsiDocumentManager.getInstance(element.project)
val document = documentManager.getDocument(element.containingFile)!!
val lineOfElement = document.getLineNumber(element.textOffset)

sink.addPresentation(
EndOfLinePosition(lineOfElement),
emptyList(),
"Inferred MongoDB namespace for this query.",
true,
) {
text(namespace.toString())
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
bundle="messages.TelemetryBundle"
key="notification.group.name"
/>
<codeInsight.declarativeInlayProvider
group="com.mongodb.jbplugin"
implementationClass="com.mongodb.jbplugin.inlays.JavaDriverQueryNamespaceInlay"
isEnabledByDefault="true"
language="JAVA"
nameKey="inlay.namespace.name"
bundle="messages.InspectionsAndInlaysBundle"
providerId="com.mongodb.jbplugin#JAVA" />
</extensions>
<applicationListeners>
</applicationListeners>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
notification.group.inspection.results=MongoDB Inspection Results
inlay.namespace.name=MongoDB Namespace Inlay
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class JavaDriverToolbarVisibilityUiTest {
@Test
@RequiresProject("basic-java-project-with-mongodb")
fun `shows the toolbar in a java file with references to the driver`(remoteRobot: RemoteRobot) {
remoteRobot.ideaFrame().openFile("/src/main/java/alt/mongodb/javadriver/JavaDriverRepositoryExample.java")
remoteRobot.ideaFrame().openFile("/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java")
val toolbar = remoteRobot.findJavaEditorToolbar()
assertTrue(toolbar.isShowing)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* JUnit extension to run tests that depend on code insights without building the whole IDE.
* They are more lightweight than UI tests.
*/

package com.mongodb.jbplugin.fixtures

import com.intellij.java.library.JavaLibraryUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiFile
import com.intellij.testFramework.PsiTestUtil
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
import com.intellij.testFramework.fixtures.impl.TempDirTestFixtureImpl
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.*
import java.lang.reflect.Method
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString

/**
* Annotation to add to the test function.
*/
@Retention(AnnotationRetention.RUNTIME)
@Test
annotation class ParsingTest(
val fileName: String,
@Language("java") val value: String,
)

/**
* Annotation to be used in the test, at the class level.
*
* @see com.mongodb.jbplugin.accessadapter.datagrip.adapter.DataGripMongoDbDriverTest
*/
@ExtendWith(CodeInsightTestExtension::class)
annotation class CodeInsightTest

/**
* Extension implementation. Must not be used directly.
*/
internal class CodeInsightTestExtension :
BeforeAllCallback,
AfterAllCallback,
BeforeEachCallback,
InvocationInterceptor,
ParameterResolver {
private val namespace = ExtensionContext.Namespace.create(CodeInsightTestExtension::class.java)
private val testFixtureKey = "TESTFIXTURE"
private val testPathKey = "TESTPATH"

override fun beforeAll(context: ExtensionContext) {
ApplicationManager.setApplication(null)

val projectFixture =
IdeaTestFixtureFactory
.getFixtureFactory()
.createLightFixtureBuilder(context.requiredTestClass.simpleName)
.fixture

val tempDirTestFixtureImpl = TempDirTestFixtureImpl()
val testFixture =
IdeaTestFixtureFactory
.getFixtureFactory()
.createCodeInsightFixture(
projectFixture,
tempDirTestFixtureImpl,
).apply {
testDataPath = tempDirTestFixtureImpl.tempDirPath
}

context.getStore(namespace).put(testFixtureKey, testFixture)
testFixture.setUp()

ApplicationManager.getApplication().invokeAndWait {
if (!JavaLibraryUtil.hasLibraryJar(testFixture.module, "org.mongodb:mongodb-driver-sync:5.1.1")) {
runCatching {
PsiTestUtil.addProjectLibrary(
testFixture.module,
"mongodb-driver-sync",
listOf(
Path(
"src/test/resources/mongodb-driver-sync-5.1.1.jar",
).toAbsolutePath().toString(),
),
)
}
}
}

PsiTestUtil.addSourceRoot(testFixture.module, testFixture.project.guessProjectDir()!!)
val tmpRootDir = testFixture.tempDirFixture.getFile(".")!!
PsiTestUtil.addSourceRoot(testFixture.module, tmpRootDir)
context.getStore(namespace).put(testPathKey, tmpRootDir.path)
}

override fun beforeEach(context: ExtensionContext) {
val fixture = context.getStore(namespace).get(testFixtureKey) as CodeInsightTestFixture
val modulePath = context.getStore(namespace).get(testPathKey).toString()

ApplicationManager.getApplication().invokeAndWait {
val parsingTest = context.requiredTestMethod.getAnnotation(ParsingTest::class.java)
val fileName = Path(modulePath, "src", "main", "java", parsingTest.fileName).absolutePathString()

fixture.configureByText(
fileName,
parsingTest.value,
)
}
}

override fun interceptTestMethod(
invocation: InvocationInterceptor.Invocation<Void>,
invocationContext: ReflectiveInvocationContext<Method>,
extensionContext: ExtensionContext,
) {
val throwable: AtomicReference<Throwable?> = AtomicReference(null)
val finished = AtomicBoolean(false)

val fixture = extensionContext.getStore(namespace).get(testFixtureKey) as CodeInsightTestFixture
val dumbService = fixture.project.getService(DumbService::class.java)
dumbService.runWhenSmart {
val result =
runCatching {
invocation.proceed()
}
result.onSuccess {
finished.set(true)
}

result.onFailure {
finished.set(true)
throwable.set(it)
}
}

while (!finished.get()) {
Thread.sleep(1)
}

throwable.get()?.let {
System.err.println(it.message)
it.printStackTrace(System.err)
throw it
}
}

override fun afterAll(context: ExtensionContext) {
val testFixture = context.getStore(namespace).get(testFixtureKey) as CodeInsightTestFixture

ApplicationManager.getApplication().invokeAndWait {
testFixture.tearDown()
}
}

override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext,
): Boolean =
parameterContext.parameter.type == Project::class.java ||
parameterContext.parameter.type == CodeInsightTestFixture::class.java ||
parameterContext.parameter.type == PsiFile::class.java ||
parameterContext.parameter.type == JavaPsiFacade::class.java

override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext,
): Any {
val fixture = extensionContext.getStore(namespace).get(testFixtureKey) as CodeInsightTestFixture

return when (parameterContext.parameter.type) {
Project::class.java -> fixture.project
CodeInsightTestFixture::class.java -> fixture
PsiFile::class.java -> fixture.file
JavaPsiFacade::class.java -> JavaPsiFacade.getInstance(fixture.project)
else -> TODO("Parameter of type ${parameterContext.parameter.type.canonicalName} is not supported.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.mongodb.jbplugin.inlays

import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.mongodb.jbplugin.fixtures.CodeInsightTest
import com.mongodb.jbplugin.fixtures.ParsingTest
import org.junit.jupiter.api.Assertions.*

@CodeInsightTest
class JavaDriverQueryNamespaceInlayTest {
@ParsingTest(
fileName = "Repository.java",
value = """
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import org.bson.Document;
import org.bson.types.ObjectId;
import static com.mongodb.client.model.Filters.*;

public class Repository {
private final MongoClient client;

public Repository(MongoClient client) {
this.client = client;
}

public FindIterable<Document> exampleFind() {
return client.getDatabase(<hint text="s:"/>"myDatabase")
.getCollection(<hint text="s:"/>"myCollection")
.find();
}
}
""",
)
fun `shows a inlay hint when the namespace is resolved`(
psiFile: PsiFile,
fixture: CodeInsightTestFixture,
) {
fixture.testInlays()
}
}
kmruiz marked this conversation as resolved.
Show resolved Hide resolved
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package alt.mongodb.javadriver;

import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import org.bson.Document;
import org.bson.types.ObjectId;
import static com.mongodb.client.model.Filters.*;

public class JavaDriverRepository {
private final MongoClient client;

public JavaDriverRepository(MongoClient client) {
this.client = client;
}

public FindIterable<Document> exampleFind() {
return client.getDatabase("myDatabase")
.getCollection("myCollection")
.find();
}
}
Loading
Loading