diff --git a/.github/workflows/co.yml b/.github/workflows/co.yml index a8a3944cd..e2a63ebf6 100644 --- a/.github/workflows/co.yml +++ b/.github/workflows/co.yml @@ -33,7 +33,7 @@ jobs: ./gradlew check --stacktrace ./gradlew codeCoverageReport - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} file: build/reports/jacoco/report.xml diff --git a/idea-plugin/build.gradle.kts b/idea-plugin/build.gradle.kts index 21bc1d88b..82681b980 100644 --- a/idea-plugin/build.gradle.kts +++ b/idea-plugin/build.gradle.kts @@ -108,7 +108,7 @@ intellij { type.set("IC") pluginName.set("easy-yapi") sandboxDir.set("idea-sandbox") - plugins.set(listOf("java")) + plugins.set(listOf("java", "maven", "gradle")) } tasks { diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/rule/ScriptRuleParser.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/rule/ScriptRuleParser.kt index a5386f31a..ade07190b 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/rule/ScriptRuleParser.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/rule/ScriptRuleParser.kt @@ -12,6 +12,8 @@ import com.itangcent.common.utils.mapToTypedArray import com.itangcent.http.RequestUtils import com.itangcent.idea.plugin.api.MethodInferHelper import com.itangcent.idea.plugin.format.Json5Formatter +import com.itangcent.idea.utils.MavenHelper +import com.itangcent.idea.utils.MavenIdData import com.itangcent.intellij.config.rule.* import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.extend.notReentrant @@ -470,6 +472,10 @@ abstract class ScriptRuleParser : AbstractRuleParser() { } } + override fun mavenId(): MavenIdData? { + return actionContext.callInReadUI { MavenHelper.getMavenId(psiClass) } + } + override fun toString(): String { return name() } @@ -1069,6 +1075,7 @@ abstract class ScriptRuleParser : AbstractRuleParser() { */ abstract fun implements(): Array? + abstract fun mavenId(): MavenIdData? } /** @@ -1231,6 +1238,11 @@ abstract class ScriptRuleParser : AbstractRuleParser() { } } + override fun mavenId(): MavenIdData? { + val psiClass = getResource() as? PsiClass ?: return null + return actionContext.callInReadUI { MavenHelper.getMavenId(psiClass) } + } + override fun toString(): String { return name() } @@ -1439,6 +1451,11 @@ abstract class ScriptRuleParser : AbstractRuleParser() { } } + override fun mavenId(): MavenIdData? { + val psiClass = getResource() as? PsiClass ?: return null + return actionContext.callInReadUI { MavenHelper.getMavenId(psiClass) } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ScriptClassContext) return false diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/MavenHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/MavenHelper.kt new file mode 100644 index 000000000..07e715e7c --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/MavenHelper.kt @@ -0,0 +1,163 @@ +package com.itangcent.idea.utils + +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.psi.PsiClass +import com.itangcent.annotation.script.ScriptTypeName +import com.itangcent.common.utils.safe +import org.jetbrains.idea.maven.project.MavenProjectsManager +import org.jetbrains.plugins.gradle.service.project.data.ExternalProjectDataCache + + +/** + * Utility object for obtaining Maven identifiers of a given PsiClass through Maven or Gradle. + * + * @author tangcent + * @date 2024/08/11 + */ +object MavenHelper { + + /** + * Attempts to retrieve the Maven ID of a PsiClass using Maven or Gradle. + * + * @param psiClass the PsiClass for which the Maven ID is to be retrieved. + * @return MavenIdData if found, null otherwise. + */ + fun getMavenId(psiClass: PsiClass): MavenIdData? { + return safe { + getMavenIdByMaven(psiClass) + } ?: safe { + getMavenIdByGradle(psiClass) + } + } + + /** + * Retrieves the Maven ID of a PsiClass using Maven. + * + * @param psiClass the PsiClass for which the Maven ID is to be retrieved. + * @return MavenIdData if found, null otherwise. + */ + private fun getMavenIdByMaven(psiClass: PsiClass): MavenIdData? { + val project = psiClass.project + val module = ModuleUtilCore.findModuleForPsiElement(psiClass) + ?: return null + + val mavenProjectsManager = MavenProjectsManager.getInstance(project) + val mavenProject = mavenProjectsManager.findProject(module) ?: return null + val mavenId = mavenProject.mavenId + return MavenIdData( + groupId = mavenId.groupId!!, + artifactId = mavenId.artifactId!!, + version = mavenId.version!! + ) + } + + /** + * Retrieves the Maven ID of a PsiClass using Gradle. + * + * @param psiClass the PsiClass for which the Maven ID is to be retrieved. + * @return MavenIdData if found, null otherwise. + */ + private fun getMavenIdByGradle(psiClass: PsiClass): MavenIdData? { + val project = psiClass.project + val projectPath = project.basePath ?: return null + val externalProject = ExternalProjectDataCache.getInstance(project).getRootExternalProject(projectPath) + ?: return null + + return MavenIdData( + groupId = externalProject.group, + artifactId = externalProject.name, + version = externalProject.version + ) + } +} + +/** + * Data class representing Maven ID information. + */ +@ScriptTypeName("MavenId") +class MavenIdData( + val groupId: String, + val artifactId: String, + val version: String +) { + + /** + * Generates a Maven dependency snippet + */ + fun maven(): String { + return """ + + $groupId + $artifactId + $version + + """.trimIndent() + } + + /** + * Generates a Gradle implementation dependency snippet + */ + fun gradle(): String { + return """ + implementation group: '$groupId', name: '$artifactId', version: '$version' + """.trimIndent() + } + + /** + * Generates a Gradle implementation dependency snippet in short form. + */ + fun gradleShort(): String { + return """ + implementation '$groupId:$artifactId:$version' + """.trimIndent() + } + + /** + * Generates a Gradle implementation dependency snippet in Kotlin DSL. + */ + fun gradleKotlin(): String { + return """ + implementation("$groupId:$artifactId:$version") + """.trimIndent() + } + + /** + * Generates an SBT dependency snippet. + */ + fun sbt(): String { + return """ + libraryDependencies += "$groupId" % "$artifactId" % "$version" + """.trimIndent() + } + + /** + * Generates an Ivy dependency snippet. + */ + fun ivy(): String { + return """ + + """.trimIndent() + } + + override fun toString(): String { + return "$groupId:$artifactId:$version" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MavenIdData) return false + + if (groupId != other.groupId) return false + if (artifactId != other.artifactId) return false + if (version != other.version) return false + + return true + } + + override fun hashCode(): Int { + var result = groupId.hashCode() + result = 31 * result + artifactId.hashCode() + result = 31 * result + version.hashCode() + return result + } +} \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/rule/ScriptClassContextBaseTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/rule/ScriptClassContextBaseTest.kt index 108e3c498..bc7d6d1d4 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/rule/ScriptClassContextBaseTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/rule/ScriptClassContextBaseTest.kt @@ -1186,6 +1186,10 @@ abstract class ScriptClassContextBaseTest : PluginContextLightCodeInsightFixture assertTrue(modelPsiClass.asClassContext().implements()!!.isEmpty()) } + fun testMavenId() { + assertNull(objectPsiClass.asClassContext().mavenId()) + } + //endregion //region tests of ScriptFieldContext diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/ETHUtils.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/ETHUtils.kt index f40a24f26..d4c9db2c6 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/ETHUtils.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/ETHUtils.kt @@ -2,6 +2,7 @@ package com.itangcent.idea.plugin.settings import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty +import kotlin.reflect.KProperty import kotlin.reflect.full.memberProperties import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -13,9 +14,10 @@ import kotlin.test.assertNotNull */ object ETHUtils { - fun testCETH(original: T, copy: T.() -> T) { + fun testCETH(original: T, copy: T.() -> T) { + assertEquals(original, original) assertNotNull(original, "original should not be null") - original!! + assertNotEquals(original, "original should not be equals to a string") // Create a copy using the data class generated `copy` method val copied = original.copy() assertEquals(original, copied) @@ -23,8 +25,8 @@ object ETHUtils { assertEquals(original.toString(), copied.toString()) // Use reflection to fetch all properties - Settings::class.memberProperties - .filterIsInstance>() + val memberProperties = original::class.memberProperties + memberProperties.filterIsInstance>() .forEach { property -> val newCopied = original.copy() @@ -33,27 +35,61 @@ object ETHUtils { val updateValue = backup?.backup() ?: propClass?.fake() property.setter.call(newCopied, updateValue) + // Check that the modified object does not equal the copied object - assertNotEquals( - original, - newCopied, - "[${original!!::class}] Change Property: ${property.name} from $backup to $updateValue}" - ) - assertNotEquals( - original.hashCode(), newCopied.hashCode(), - "[${original!!::class}] Change Property: ${property.name} from $backup to $updateValue}" - ) - assertNotEquals( - original.toString(), newCopied.toString(), - "[${original!!::class}] Change Property: ${property.name} from $backup to $updateValue}" - ) + checkEquals(original, newCopied, property, backup, updateValue) // Restore original property to continue clean tests property.setter.call(original, backup) } + + val originalProperties = memberProperties.associate { it.name to it.getter.call(original) } + val constructor = original::class.constructors.maxByOrNull { it.parameters.size } ?: return + memberProperties.filter { it !is KMutableProperty<*> } + .forEach { property -> + val propertyName = property.name + val backup = property.getter.call(original) + val propClass = property.returnType.classifier as? KClass<*> + val updateValue = backup?.backup() ?: propClass?.fake() + + val newCopied = constructor.callBy( + constructor.parameters.associateWith { + if (it.name == propertyName) updateValue else originalProperties[it.name] + } + ) + + // Check that the modified object does not equal the copied object + checkEquals(original, newCopied, property, backup, updateValue) + } + } + + /** + * Check that the modified object does not equal the copied object + */ + private fun checkEquals( + original: Any, + newCopied: Any, + property: KProperty<*>, + backup: Any?, + updateValue: Any? + ) { + assertNotEquals( + original, + newCopied, + "[${original::class}] Change Property: ${property.name} from $backup to $updateValue}" + ) + assertNotEquals( + original.hashCode(), newCopied.hashCode(), + "[${original::class}] Change Property: ${property.name} from $backup to $updateValue}" + ) + assertNotEquals( + original.toString(), newCopied.toString(), + "[${original::class}] Change Property: ${property.name} from $backup to $updateValue}" + ) } } +@Suppress("UNCHECKED_CAST") fun Any.backup(): Any { return when (this) { is Array<*> -> { diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/utils/MavenHelperTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/utils/MavenHelperTest.kt new file mode 100644 index 000000000..f07cb18fc --- /dev/null +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/utils/MavenHelperTest.kt @@ -0,0 +1,132 @@ +package com.itangcent.idea.utils + +import com.intellij.openapi.module.Module +import com.intellij.psi.PsiClass +import com.itangcent.idea.plugin.settings.ETHUtils +import com.itangcent.testFramework.PluginContextLightCodeInsightFixtureTestCase +import org.jetbrains.idea.maven.model.MavenId +import org.jetbrains.idea.maven.project.MavenProject +import org.jetbrains.idea.maven.project.MavenProjectsManager +import org.jetbrains.plugins.gradle.model.ExternalProject +import org.jetbrains.plugins.gradle.service.project.data.ExternalProjectDataCache +import org.mockito.Mockito.mockStatic +import org.mockito.kotlin.any +import org.mockito.kotlin.mock + +/** + * Test class for [MavenHelper]. + * + * @author tangcent + * @date 2024/08/11 + */ +class MavenHelperTest : PluginContextLightCodeInsightFixtureTestCase() { + + private lateinit var userCtrlPsiClass: PsiClass + + override fun beforeBind() { + super.beforeBind() + userCtrlPsiClass = loadClass("api/UserCtrl.java")!! + } + + fun testGetMavenIdByMaven() { + mockStatic(MavenProjectsManager::class.java).use { mavenProjectsManagerMock -> + val mavenProject = mock { + on(it.mavenId) + .thenReturn(MavenId("com.itangcent", "intellij-idea-test", "1.0.0")) + } + val mavenProjectsManager = mock { + on(it.findProject(any())) + .thenReturn(mavenProject) + } + + mavenProjectsManagerMock.`when` { + MavenProjectsManager.getInstance(project) + }.thenReturn(mavenProjectsManager) + + val mavenId = MavenHelper.getMavenId(userCtrlPsiClass) + + assertNotNull(mavenId) + assertEquals("com.itangcent", mavenId!!.groupId) + assertEquals("intellij-idea-test", mavenId.artifactId) + assertEquals("1.0.0", mavenId.version) + } + } + + fun testGetMavenIdByGradle() { + mockStatic(ExternalProjectDataCache::class.java).use { externalProjectDataCacheMock -> + val externalProject = mock { + on(it.name) + .thenReturn("intellij-idea-test") + on(it.group) + .thenReturn("com.itangcent") + on(it.version) + .thenReturn("1.0.0") + } + val externalProjectDataCache = mock { + on(it.getRootExternalProject(any())) + .thenReturn(externalProject) + } + + externalProjectDataCacheMock.`when` { + ExternalProjectDataCache.getInstance(project) + }.thenReturn(externalProjectDataCache) + + val mavenIdData = MavenHelper.getMavenId(userCtrlPsiClass) + + assertNotNull(mavenIdData) + assertEquals("com.itangcent", mavenIdData!!.groupId) + assertEquals("intellij-idea-test", mavenIdData.artifactId) + assertEquals("1.0.0", mavenIdData.version) + } + } + + fun testMavenIdData() { + val mavenIdData = MavenIdData("com.itangcent", "intellij-idea-test", "1.0.0") + assertEquals("com.itangcent", mavenIdData.groupId) + assertEquals("intellij-idea-test", mavenIdData.artifactId) + assertEquals("1.0.0", mavenIdData.version) + + assertEquals( + """ + + com.itangcent + intellij-idea-test + 1.0.0 + + """.trimIndent(), mavenIdData.maven() + ) + + assertEquals( + """ + implementation group: 'com.itangcent', name: 'intellij-idea-test', version: '1.0.0' + """.trimIndent(), mavenIdData.gradle() + ) + + assertEquals( + """ + implementation 'com.itangcent:intellij-idea-test:1.0.0' + """.trimIndent(), mavenIdData.gradleShort() + ) + + assertEquals( + """ + implementation("com.itangcent:intellij-idea-test:1.0.0") + """.trimIndent(), mavenIdData.gradleKotlin() + ) + + assertEquals( + """ + + """.trimIndent(), mavenIdData.ivy() + ) + + assertEquals( + """ + libraryDependencies += "com.itangcent" % "intellij-idea-test" % "1.0.0" + """.trimIndent(), mavenIdData.sbt() + ) + + assertEquals("com.itangcent:intellij-idea-test:1.0.0", mavenIdData.toString()) + ETHUtils.testCETH(mavenIdData) { MavenIdData("com.itangcent", "intellij-idea-test", "1.0.0") } + } +} \ No newline at end of file