User: joe Date: 05 Nov 25 00:48 Revision: 039e8835d95ec2a3e21b865fdd171db50d13bfdb Summary: Add inspection for when two @Share annotations have different local types TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10209&personal=false Index: src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt (revision 039e8835d95ec2a3e21b865fdd171db50d13bfdb) +++ src/main/kotlin/platform/mixin/handlers/mixinextras/ShareUtil.kt (revision 039e8835d95ec2a3e21b865fdd171db50d13bfdb) @@ -0,0 +1,128 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.handlers.mixinextras + +import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.findContainingClass +import com.demonwav.mcdev.util.findContainingMethod +import com.demonwav.mcdev.util.internalName +import com.demonwav.mcdev.util.mapFirstNotNull +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.impl.java.stubs.index.JavaAnnotationIndex +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.LocalSearchScope +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.PsiModificationTracker +import com.intellij.psi.util.PsiTreeUtil + +@Service(Service.Level.PROJECT) +class ShareUtil(private val project: Project) { + fun getShares(shareAnnotation: PsiAnnotation): List { + val shareClass = shareAnnotation.resolveAnnotationType() ?: return listOf(shareAnnotation) + val allShares = getAllShares(shareClass) + return shareAnnotation.getShareKeys(project).flatMap { allShares[it] ?: listOf(shareAnnotation) }.distinct() + } + + private fun getAllShares(shareClass: PsiClass): Map> { + return CachedValuesManager.getManager(project).getCachedValue(shareClass) { + CachedValueProvider.Result.create(computeAllShares(shareClass), PsiModificationTracker.MODIFICATION_COUNT) + } + } + + private fun computeAllShares(shareClass: PsiClass): Map> { + val result = hashMapOf>() + for (annotation in getUsages(shareClass)) { + for (key in annotation.getShareKeys(project)) { + result.getOrPut(key) { mutableListOf() } += annotation + } + } + return result + } + + private fun getUsages(shareClass: PsiClass): List { + return when (val useScope = shareClass.useScope) { + is GlobalSearchScope -> JavaAnnotationIndex.getInstance().getAnnotations("Share", project, useScope).filter { annotation -> + annotation.nameReferenceElement?.isReferenceTo(shareClass) == true + } + is LocalSearchScope -> { + useScope.scope.flatMap { element -> + PsiTreeUtil.collectElementsOfType(element, PsiAnnotation::class.java).filter { annotation -> + annotation.nameReferenceElement?.isReferenceTo(shareClass) == true + } + } + } + else -> throw IllegalStateException("Unknown scope class ${useScope.javaClass.name}") + } + } + + private data class ShareKey( + private val namespace: String, + private val id: String, + private val targetOwner: String, + private val targetName: String, + private val targetDesc: String, + ) + + companion object { + fun getInstance(project: Project): ShareUtil = project.getService(ShareUtil::class.java) + + private val PsiAnnotation.namespace: String? + get() = findDeclaredAttributeValue("namespace")?.constantStringValue + ?: findContainingClass()?.internalName + private val PsiAnnotation.value: String? + get() = findDeclaredAttributeValue("value")?.constantStringValue + + private fun PsiAnnotation.getShareKeys(project: Project): List { + val method = findContainingMethod() ?: return emptyList() + val (injectorAnnotation, injector) = method.annotations.mapFirstNotNull { annotation -> + (MixinAnnotationHandler.forMixinAnnotation(annotation, project) as? InjectorAnnotationHandler)?.let { annotation to it } + } ?: return emptyList() + + val mixinTargets = method.findContainingClass()?.mixinTargets ?: return emptyList() + val namespace = this.namespace ?: return emptyList() + val id = this.value ?: return emptyList() + + return mixinTargets.flatMap { targetClass -> + injector.resolveTarget(injectorAnnotation, targetClass).mapNotNull { target -> + if (target !is MethodTargetMember) { + return@mapNotNull null + } + + ShareKey( + namespace, + id, + target.classAndMethod.clazz.name, + target.classAndMethod.method.name, + target.classAndMethod.method.desc, + ) + } + } + } + } +} Index: src/main/kotlin/platform/mixin/inspection/mixinextras/ConflictingShareTypeInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/mixinextras/ConflictingShareTypeInspection.kt (revision 039e8835d95ec2a3e21b865fdd171db50d13bfdb) +++ src/main/kotlin/platform/mixin/inspection/mixinextras/ConflictingShareTypeInspection.kt (revision 039e8835d95ec2a3e21b865fdd171db50d13bfdb) @@ -0,0 +1,92 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.mixinextras + +import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.ShareUtil +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.MixinExtras.unwrapLocalRef +import com.demonwav.mcdev.util.equivalentTo +import com.demonwav.mcdev.util.findContainingClass +import com.demonwav.mcdev.util.findContainingMethod +import com.demonwav.mcdev.util.fullQualifiedName +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiType +import com.intellij.psi.util.PsiTypesUtil + +class ConflictingShareTypeInspection : MixinInspection() { + override fun getStaticDescription() = "Reports when two @Shares of the same variable have different types" + + override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.MixinExtras.SHARE)) { + return + } + + val mixinClass = annotation.findContainingClass() ?: return + val expectedType = getShareType(annotation) ?: return + val differentTypes = mutableSetOf() + val differentTypeLocations = mutableSetOf() + + val allShares = ShareUtil.getInstance(holder.project).getShares(annotation) + for (otherShare in allShares) { + if (otherShare equivalentTo annotation) { + continue + } + + val otherType = getShareType(otherShare) ?: continue + if (otherType != expectedType) { + differentTypes += otherType + + val containingMethod = otherShare.findContainingMethod() + val containingClass = otherShare.findContainingClass() + if (containingClass != null && containingClass != mixinClass) { + differentTypeLocations += "class '${containingClass.fullQualifiedName}'" + } else if (containingMethod != null) { + differentTypeLocations += "method '${containingMethod.name}'" + } + } + } + + if (differentTypes.isNotEmpty()) { + val differentTypesStr = differentTypes.joinToString(limit = 3) { it.presentableText } + val differentLocationsStr = differentTypeLocations.joinToString(limit = 3) + holder.registerProblem( + (annotation.parent?.parent as? PsiParameter)?.typeElement ?: annotation, + "@Share type ${expectedType.presentableText} is not compatible with other @Share types $differentTypesStr, found in $differentLocationsStr" + ) + } + } + } + + private fun getShareType(share: PsiAnnotation): PsiType? { + val param = share.parent?.parent as? PsiParameter ?: return null + val paramType = param.type + val paramClass = PsiTypesUtil.getPsiClass(paramType) ?: return null + if (paramClass.qualifiedName?.startsWith(MixinConstants.MixinExtras.LOCAL_REF_PACKAGE) != true) { + return null + } + return paramType.unwrapLocalRef() + } +} Index: src/main/kotlin/platform/mixin/util/MixinConstants.kt =================================================================== --- src/main/kotlin/platform/mixin/util/MixinConstants.kt (revision 8050bcbf8aab3abaee87054c63468db868a9dda2) +++ src/main/kotlin/platform/mixin/util/MixinConstants.kt (revision 039e8835d95ec2a3e21b865fdd171db50d13bfdb) @@ -92,6 +92,7 @@ const val WRAP_METHOD = "com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod" const val LOCAL = "com.llamalad7.mixinextras.sugar.Local" const val LOCAL_REF_PACKAGE = "com.llamalad7.mixinextras.sugar.ref." + const val SHARE = "com.llamalad7.mixinextras.sugar.Share" const val EXPRESSION = "com.llamalad7.mixinextras.expression.Expression" const val DEFINITION = "com.llamalad7.mixinextras.expression.Definition" const val MIXIN_EXTRAS_CONFIG = "com.llamalad7.mixinextras.config.MixinExtrasConfig" Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 8050bcbf8aab3abaee87054c63468db868a9dda2) +++ src/main/resources/META-INF/plugin.xml (revision 039e8835d95ec2a3e21b865fdd171db50d13bfdb) @@ -1458,6 +1458,14 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.LocalArgsOnlyInspection"/> +