Skip to content

Commit 039e883

Browse files
committed
Add inspection for when two @share annotations have different local types
1 parent 8050bcb commit 039e883

4 files changed

Lines changed: 229 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
22+
23+
import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
24+
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
25+
import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
26+
import com.demonwav.mcdev.platform.mixin.util.mixinTargets
27+
import com.demonwav.mcdev.util.constantStringValue
28+
import com.demonwav.mcdev.util.findContainingClass
29+
import com.demonwav.mcdev.util.findContainingMethod
30+
import com.demonwav.mcdev.util.internalName
31+
import com.demonwav.mcdev.util.mapFirstNotNull
32+
import com.intellij.openapi.components.Service
33+
import com.intellij.openapi.project.Project
34+
import com.intellij.psi.PsiAnnotation
35+
import com.intellij.psi.PsiClass
36+
import com.intellij.psi.impl.java.stubs.index.JavaAnnotationIndex
37+
import com.intellij.psi.search.GlobalSearchScope
38+
import com.intellij.psi.search.LocalSearchScope
39+
import com.intellij.psi.util.CachedValueProvider
40+
import com.intellij.psi.util.CachedValuesManager
41+
import com.intellij.psi.util.PsiModificationTracker
42+
import com.intellij.psi.util.PsiTreeUtil
43+
44+
@Service(Service.Level.PROJECT)
45+
class ShareUtil(private val project: Project) {
46+
fun getShares(shareAnnotation: PsiAnnotation): List<PsiAnnotation> {
47+
val shareClass = shareAnnotation.resolveAnnotationType() ?: return listOf(shareAnnotation)
48+
val allShares = getAllShares(shareClass)
49+
return shareAnnotation.getShareKeys(project).flatMap { allShares[it] ?: listOf(shareAnnotation) }.distinct()
50+
}
51+
52+
private fun getAllShares(shareClass: PsiClass): Map<ShareKey, List<PsiAnnotation>> {
53+
return CachedValuesManager.getManager(project).getCachedValue(shareClass) {
54+
CachedValueProvider.Result.create(computeAllShares(shareClass), PsiModificationTracker.MODIFICATION_COUNT)
55+
}
56+
}
57+
58+
private fun computeAllShares(shareClass: PsiClass): Map<ShareKey, List<PsiAnnotation>> {
59+
val result = hashMapOf<ShareKey, MutableList<PsiAnnotation>>()
60+
for (annotation in getUsages(shareClass)) {
61+
for (key in annotation.getShareKeys(project)) {
62+
result.getOrPut(key) { mutableListOf() } += annotation
63+
}
64+
}
65+
return result
66+
}
67+
68+
private fun getUsages(shareClass: PsiClass): List<PsiAnnotation> {
69+
return when (val useScope = shareClass.useScope) {
70+
is GlobalSearchScope -> JavaAnnotationIndex.getInstance().getAnnotations("Share", project, useScope).filter { annotation ->
71+
annotation.nameReferenceElement?.isReferenceTo(shareClass) == true
72+
}
73+
is LocalSearchScope -> {
74+
useScope.scope.flatMap { element ->
75+
PsiTreeUtil.collectElementsOfType(element, PsiAnnotation::class.java).filter { annotation ->
76+
annotation.nameReferenceElement?.isReferenceTo(shareClass) == true
77+
}
78+
}
79+
}
80+
else -> throw IllegalStateException("Unknown scope class ${useScope.javaClass.name}")
81+
}
82+
}
83+
84+
private data class ShareKey(
85+
private val namespace: String,
86+
private val id: String,
87+
private val targetOwner: String,
88+
private val targetName: String,
89+
private val targetDesc: String,
90+
)
91+
92+
companion object {
93+
fun getInstance(project: Project): ShareUtil = project.getService(ShareUtil::class.java)
94+
95+
private val PsiAnnotation.namespace: String?
96+
get() = findDeclaredAttributeValue("namespace")?.constantStringValue
97+
?: findContainingClass()?.internalName
98+
private val PsiAnnotation.value: String?
99+
get() = findDeclaredAttributeValue("value")?.constantStringValue
100+
101+
private fun PsiAnnotation.getShareKeys(project: Project): List<ShareKey> {
102+
val method = findContainingMethod() ?: return emptyList()
103+
val (injectorAnnotation, injector) = method.annotations.mapFirstNotNull { annotation ->
104+
(MixinAnnotationHandler.forMixinAnnotation(annotation, project) as? InjectorAnnotationHandler)?.let { annotation to it }
105+
} ?: return emptyList()
106+
107+
val mixinTargets = method.findContainingClass()?.mixinTargets ?: return emptyList()
108+
val namespace = this.namespace ?: return emptyList()
109+
val id = this.value ?: return emptyList()
110+
111+
return mixinTargets.flatMap { targetClass ->
112+
injector.resolveTarget(injectorAnnotation, targetClass).mapNotNull { target ->
113+
if (target !is MethodTargetMember) {
114+
return@mapNotNull null
115+
}
116+
117+
ShareKey(
118+
namespace,
119+
id,
120+
target.classAndMethod.clazz.name,
121+
target.classAndMethod.method.name,
122+
target.classAndMethod.method.desc,
123+
)
124+
}
125+
}
126+
}
127+
}
128+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.mixin.inspection.mixinextras
22+
23+
import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.ShareUtil
24+
import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection
25+
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
26+
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.MixinExtras.unwrapLocalRef
27+
import com.demonwav.mcdev.util.equivalentTo
28+
import com.demonwav.mcdev.util.findContainingClass
29+
import com.demonwav.mcdev.util.findContainingMethod
30+
import com.demonwav.mcdev.util.fullQualifiedName
31+
import com.intellij.codeInspection.ProblemsHolder
32+
import com.intellij.psi.JavaElementVisitor
33+
import com.intellij.psi.PsiAnnotation
34+
import com.intellij.psi.PsiParameter
35+
import com.intellij.psi.PsiType
36+
import com.intellij.psi.util.PsiTypesUtil
37+
38+
class ConflictingShareTypeInspection : MixinInspection() {
39+
override fun getStaticDescription() = "Reports when two @Shares of the same variable have different types"
40+
41+
override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() {
42+
override fun visitAnnotation(annotation: PsiAnnotation) {
43+
if (!annotation.hasQualifiedName(MixinConstants.MixinExtras.SHARE)) {
44+
return
45+
}
46+
47+
val mixinClass = annotation.findContainingClass() ?: return
48+
val expectedType = getShareType(annotation) ?: return
49+
val differentTypes = mutableSetOf<PsiType>()
50+
val differentTypeLocations = mutableSetOf<String>()
51+
52+
val allShares = ShareUtil.getInstance(holder.project).getShares(annotation)
53+
for (otherShare in allShares) {
54+
if (otherShare equivalentTo annotation) {
55+
continue
56+
}
57+
58+
val otherType = getShareType(otherShare) ?: continue
59+
if (otherType != expectedType) {
60+
differentTypes += otherType
61+
62+
val containingMethod = otherShare.findContainingMethod()
63+
val containingClass = otherShare.findContainingClass()
64+
if (containingClass != null && containingClass != mixinClass) {
65+
differentTypeLocations += "class '${containingClass.fullQualifiedName}'"
66+
} else if (containingMethod != null) {
67+
differentTypeLocations += "method '${containingMethod.name}'"
68+
}
69+
}
70+
}
71+
72+
if (differentTypes.isNotEmpty()) {
73+
val differentTypesStr = differentTypes.joinToString(limit = 3) { it.presentableText }
74+
val differentLocationsStr = differentTypeLocations.joinToString(limit = 3)
75+
holder.registerProblem(
76+
(annotation.parent?.parent as? PsiParameter)?.typeElement ?: annotation,
77+
"@Share type ${expectedType.presentableText} is not compatible with other @Share types $differentTypesStr, found in $differentLocationsStr"
78+
)
79+
}
80+
}
81+
}
82+
83+
private fun getShareType(share: PsiAnnotation): PsiType? {
84+
val param = share.parent?.parent as? PsiParameter ?: return null
85+
val paramType = param.type
86+
val paramClass = PsiTypesUtil.getPsiClass(paramType) ?: return null
87+
if (paramClass.qualifiedName?.startsWith(MixinConstants.MixinExtras.LOCAL_REF_PACKAGE) != true) {
88+
return null
89+
}
90+
return paramType.unwrapLocalRef()
91+
}
92+
}

src/main/kotlin/platform/mixin/util/MixinConstants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ object MixinConstants {
9292
const val WRAP_METHOD = "com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod"
9393
const val LOCAL = "com.llamalad7.mixinextras.sugar.Local"
9494
const val LOCAL_REF_PACKAGE = "com.llamalad7.mixinextras.sugar.ref."
95+
const val SHARE = "com.llamalad7.mixinextras.sugar.Share"
9596
const val EXPRESSION = "com.llamalad7.mixinextras.expression.Expression"
9697
const val DEFINITION = "com.llamalad7.mixinextras.expression.Definition"
9798
const val MIXIN_EXTRAS_CONFIG = "com.llamalad7.mixinextras.config.MixinExtrasConfig"

src/main/resources/META-INF/plugin.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,14 @@
14581458
level="WARNING"
14591459
hasStaticDescription="true"
14601460
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.LocalArgsOnlyInspection"/>
1461+
<localInspection displayName="@Share has conflicting variable type"
1462+
shortName="ConflictingShareType"
1463+
groupName="Mixin"
1464+
language="JAVA"
1465+
enabledByDefault="true"
1466+
level="ERROR"
1467+
hasStaticDescription="true"
1468+
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.ConflictingShareTypeInspection"/>
14611469
<!--endregion-->
14621470

14631471
<!--region Configuration -->

0 commit comments

Comments
 (0)