From 8ff2325fabc6421a5ac63d4ac2045ca948f096c0 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 22 Aug 2020 10:59:30 +0100 Subject: [PATCH 1/5] Rewrite SideOnly inspection --- .../platform/forge/util/ForgeConstants.kt | 3 + src/main/kotlin/sideonly/Side.kt | 15 + src/main/kotlin/sideonly/SideHardness.kt | 19 + src/main/kotlin/sideonly/SideInstance.kt | 37 ++ .../SideOnlyInferredAnnotationProvider.kt | 42 ++ .../kotlin/sideonly/SideOnlyInspection.kt | 187 +++++++++ src/main/kotlin/sideonly/SideOnlyUtil.kt | 389 ++++++++++++++++++ .../SoftSideOnlyInstructionVisitor.kt | 185 +++++++++ src/main/resources/META-INF/plugin.xml | 10 + 9 files changed, 887 insertions(+) create mode 100644 src/main/kotlin/sideonly/Side.kt create mode 100644 src/main/kotlin/sideonly/SideHardness.kt create mode 100644 src/main/kotlin/sideonly/SideInstance.kt create mode 100644 src/main/kotlin/sideonly/SideOnlyInferredAnnotationProvider.kt create mode 100644 src/main/kotlin/sideonly/SideOnlyInspection.kt create mode 100644 src/main/kotlin/sideonly/SideOnlyUtil.kt create mode 100644 src/main/kotlin/sideonly/SoftSideOnlyInstructionVisitor.kt diff --git a/src/main/kotlin/platform/forge/util/ForgeConstants.kt b/src/main/kotlin/platform/forge/util/ForgeConstants.kt index 34bc7178e..e75be9a41 100644 --- a/src/main/kotlin/platform/forge/util/ForgeConstants.kt +++ b/src/main/kotlin/platform/forge/util/ForgeConstants.kt @@ -14,7 +14,10 @@ object ForgeConstants { const val SIDE_ONLY_ANNOTATION = "net.minecraftforge.fml.relauncher.SideOnly" const val SIDE_ANNOTATION = "net.minecraftforge.fml.relauncher.Side" + const val ONLY_IN_ANNOTATION = "net.minecraftforge.api.distmarker.OnlyIn" + const val DIST_ANNOTATION = "net.minecraftforge.api.distmarker.Dist" const val SIDED_PROXY_ANNOTATION = "net.minecraftforge.fml.common.SidedProxy" + const val DIST_EXECUTOR = "net.minecraftforge.fml.DistExecutor" const val MOD_ANNOTATION = "net.minecraftforge.fml.common.Mod" const val CORE_MOD_INTERFACE = "net.minecraftforge.fml.relauncher.IFMLLoadingPlugin" const val EVENT_HANDLER_ANNOTATION = "net.minecraftforge.fml.common.Mod.EventHandler" diff --git a/src/main/kotlin/sideonly/Side.kt b/src/main/kotlin/sideonly/Side.kt new file mode 100644 index 000000000..af7eff40f --- /dev/null +++ b/src/main/kotlin/sideonly/Side.kt @@ -0,0 +1,15 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +enum class Side(val forgeName: String) { + CLIENT("CLIENT"), SERVER("DEDICATED_SERVER"), BOTH("BOTH") +} diff --git a/src/main/kotlin/sideonly/SideHardness.kt b/src/main/kotlin/sideonly/SideHardness.kt new file mode 100644 index 000000000..07b213a47 --- /dev/null +++ b/src/main/kotlin/sideonly/SideHardness.kt @@ -0,0 +1,19 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +enum class SideHardness { + /** Stripped at runtime or compile-time */ + HARD, + /** Not stripped but should only be loaded on the correct side */ + SOFT, + EITHER +} diff --git a/src/main/kotlin/sideonly/SideInstance.kt b/src/main/kotlin/sideonly/SideInstance.kt new file mode 100644 index 000000000..8d4eaddaa --- /dev/null +++ b/src/main/kotlin/sideonly/SideInstance.kt @@ -0,0 +1,37 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +import com.intellij.psi.PsiElement + +class SideInstance private constructor(val side: Side, val element: PsiElement, val reason: String) { + companion object { + fun createSideOnly(side: Side, element: PsiElement): SideInstance { + return SideInstance(side, element, "annotated with @SideOnly(Side.$side)") + } + fun createEnvironment(side: Side, element: PsiElement): SideInstance { + return SideInstance(side, element, "annotated with @Environment(EnvType.$side)") + } + fun createOnlyIn(side: Side, element: PsiElement): SideInstance { + return SideInstance(side, element, "annotated with @OnlyIn(Dist.${side.forgeName})") + } + fun createMcDev(side: Side, element: PsiElement): SideInstance { + return SideInstance(side, element, "annotated with @CheckEnv(Env.$side)") + } + fun createImplicitMcDev(side: Side, element: PsiElement): SideInstance { + return SideInstance(side, element, "implicitly annotated with @CheckEnv(Env.$side)") + } + + fun createDistExecutor(side: Side, element: PsiElement): SideInstance { + return SideInstance(side, element, "inside DistExecutor ${side.forgeName}") + } + } +} diff --git a/src/main/kotlin/sideonly/SideOnlyInferredAnnotationProvider.kt b/src/main/kotlin/sideonly/SideOnlyInferredAnnotationProvider.kt new file mode 100644 index 000000000..2e525e451 --- /dev/null +++ b/src/main/kotlin/sideonly/SideOnlyInferredAnnotationProvider.kt @@ -0,0 +1,42 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +import com.intellij.codeInsight.InferredAnnotationProvider +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiModifierListOwner + +class SideOnlyInferredAnnotationProvider : InferredAnnotationProvider { + + override fun findInferredAnnotation(listOwner: PsiModifierListOwner, annotationFQN: String): PsiAnnotation? { + if (annotationFQN != SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION) { + return null + } + if (SideOnlyUtil.getExplicitAnnotation(listOwner, SideHardness.EITHER) != null) { + return null + } + val inferredAnnotation = SideOnlyUtil.getExplicitOrInferredAnnotation(listOwner, SideHardness.EITHER) + ?: return null + val annotationText = + "@${SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION}(${SideOnlyUtil.MCDEV_SIDE}.${inferredAnnotation.side})" + return JavaPsiFacade.getElementFactory(listOwner.project).createAnnotationFromText(annotationText, listOwner) + } + + override fun findInferredAnnotations(listOwner: PsiModifierListOwner): MutableList { + val annotation = findInferredAnnotation(listOwner, SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION) + return if (annotation == null) { + mutableListOf() + } else { + mutableListOf(annotation) + } + } +} diff --git a/src/main/kotlin/sideonly/SideOnlyInspection.kt b/src/main/kotlin/sideonly/SideOnlyInspection.kt new file mode 100644 index 000000000..4efe51108 --- /dev/null +++ b/src/main/kotlin/sideonly/SideOnlyInspection.kt @@ -0,0 +1,187 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassObjectAccessExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiField +import com.intellij.psi.PsiInstanceOfExpression +import com.intellij.psi.PsiLambdaExpression +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiMethodReferenceExpression +import com.intellij.psi.PsiNewExpression +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeCastExpression +import com.intellij.psi.util.parentOfType +import com.intellij.psi.util.parents + +class SideOnlyInspection : AbstractBaseJavaLocalInspectionTool() { + override fun getStaticDescription() = "SideOnly problems" + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return Visitor(holder) + } + + private class Visitor(private val problems: ProblemsHolder) : JavaElementVisitor() { + + // CHECK REFERENCES TO HARD AND SOFT SIDEONLY MEMBERS + + override fun visitClass(clazz: PsiClass) { + val classSide = SideOnlyUtil.getContextSide(clazz, SideHardness.EITHER) + + val superClass = clazz.superClass + if (superClass != null) { + val targetSide = SideOnlyUtil.getContextSide(superClass, SideHardness.EITHER) + val problemElement = clazz.extendsList?.referenceElements?.firstOrNull() + if (problemElement != null && targetSide != null && targetSide.side != classSide?.side) { + problems.registerProblem( + problemElement, + SideOnlyUtil.createInspectionMessage(classSide, targetSide) + ) + } + } + + val interfaceList = if (clazz.isInterface) { + clazz.extendsList + } else { + clazz.implementsList + }?.let { it.referencedTypes zip it.referenceElements } ?: emptyList() + val sidedInterfaces = SideOnlyUtil.getSidedInterfaces(clazz) + for ((itf, problemElement) in interfaceList) { + val itfClass = itf.resolve() ?: continue + val targetSide = SideOnlyUtil.getContextSide(itfClass, SideHardness.EITHER) + val sidedInterface = sidedInterfaces[itfClass.qualifiedName] + if (targetSide != null && targetSide.side != classSide?.side && targetSide.side != sidedInterface) { + problems.registerProblem( + problemElement, + SideOnlyUtil.createInspectionMessage(classSide, targetSide) + ) + } + } + } + + override fun visitField(field: PsiField) { + checkEitherAccess(field.type, field.typeElement) + } + + override fun visitMethod(method: PsiMethod) { + checkEitherAccess(method.returnType, method.returnTypeElement) + for (parameter in method.parameterList.parameters) { + checkEitherAccess(parameter.type, parameter.typeElement) + } + for ((type, element) in method.throwsList.referencedTypes zip method.throwsList.referenceElements) { + checkEitherAccess(type, element) + } + + val body = method.body ?: return + SideOnlyUtil.analyzeBodyForSoftSideProblems(body, problems) + } + + override fun visitLambdaExpression(expression: PsiLambdaExpression) { + // TODO: lambda parameter types? + + val body = expression.body ?: return + SideOnlyUtil.analyzeBodyForSoftSideProblems(body, problems) + } + + private fun checkEitherAccess(targetType: PsiType?, element: PsiElement?) { + targetType ?: return + element ?: return + + val targetClass = SideOnlyUtil.getClassInType(targetType) ?: return + val contextSide = SideOnlyUtil.getContextSide(element, SideHardness.EITHER) + val targetSide = SideOnlyUtil.getContextSide(targetClass, SideHardness.EITHER) + if (targetSide != null && targetSide.side != contextSide?.side) { + problems.registerProblem(element, SideOnlyUtil.createInspectionMessage(contextSide, targetSide)) + } + } + + // CHECK REFERENCES TO HARD SIDEONLY MEMBERS + + override fun visitClassObjectAccessExpression(expression: PsiClassObjectAccessExpression) { + // class references in annotations are always legal + if (expression.parentOfType(PsiAnnotation::class, PsiMember::class, PsiClass::class) is PsiAnnotation) { + return + } + + checkHardAccess(expression.operand.type, expression.operand) + } + + override fun visitInstanceOfExpression(expression: PsiInstanceOfExpression) { + checkHardAccess(expression.checkType?.type, expression.checkType) + } + + override fun visitMethodCallExpression(expression: PsiMethodCallExpression) { + checkHardAccess(expression.resolveMethod(), expression.methodExpression.referenceNameElement) + } + + override fun visitNewExpression(expression: PsiNewExpression) { + checkHardAccess( + expression.classOrAnonymousClassReference?.resolve(), + expression.classOrAnonymousClassReference + ) + } + + override fun visitReferenceExpression(expression: PsiReferenceExpression) { + val field = expression.resolve() as? PsiField ?: return + checkHardAccess(field, expression.referenceNameElement) + } + + override fun visitMethodReferenceExpression(expression: PsiMethodReferenceExpression) { + checkHardAccess(expression.potentiallyApplicableMember, expression.referenceNameElement) + } + + override fun visitTypeCastExpression(expression: PsiTypeCastExpression) { + checkHardAccess(expression.castType?.type, expression.castType) + } + + private fun checkHardAccess(target: PsiElement?, reference: PsiElement?) { + target ?: return + reference ?: return + + val targetSide = SideOnlyUtil.getContextSide(target, SideHardness.HARD) ?: return + val contextSide = getContextSideForHardAccess(reference) + if (targetSide.side != contextSide?.side) { + val contextSideForMsg = SideOnlyUtil.getContextSide(reference, SideHardness.EITHER) + problems.registerProblem(reference, SideOnlyUtil.createInspectionMessage(contextSideForMsg, targetSide)) + } + } + + private fun checkHardAccess(targetType: PsiType?, element: PsiElement?) { + targetType ?: return + checkHardAccess(SideOnlyUtil.getClassInType(targetType), element) + } + + private fun getContextSideForHardAccess(element: PsiElement): SideInstance? { + // Same as SideOnlyUtil.getContextSide(element, SideHardness.EITHER), with the exception that soft-sidedness + // of methods are ignored, as the mere presence of these methods can trip the verifier. + val softSide = SideOnlyUtil.getContextSide(element, SideHardness.SOFT) + val hardSide = SideOnlyUtil.getContextSide(element, SideHardness.HARD) + return if (softSide != null && + softSide.element !is PsiMethod && + softSide.element.parents().contains(hardSide?.element) + ) { + softSide + } else { + hardSide + } + } + } +} diff --git a/src/main/kotlin/sideonly/SideOnlyUtil.kt b/src/main/kotlin/sideonly/SideOnlyUtil.kt new file mode 100644 index 000000000..e78b3759d --- /dev/null +++ b/src/main/kotlin/sideonly/SideOnlyUtil.kt @@ -0,0 +1,389 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.fabric.FabricModuleType +import com.demonwav.mcdev.platform.fabric.util.FabricConstants +import com.demonwav.mcdev.platform.forge.util.ForgeConstants +import com.demonwav.mcdev.platform.mixin.util.isMixin +import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.util.findAnnotation +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.packageName +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.codeInspection.dataFlow.StandardDataFlowRunner +import com.intellij.json.psi.JsonFile +import com.intellij.json.psi.JsonObject +import com.intellij.json.psi.JsonStringLiteral +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassObjectAccessExpression +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiField +import com.intellij.psi.PsiLambdaExpression +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiModifierListOwner +import com.intellij.psi.PsiPackage +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.PsiResolveHelper +import com.intellij.psi.PsiType +import com.intellij.psi.util.parentOfType + +object SideOnlyUtil { + const val MCDEV_SIDEONLY_ANNOTATION = "com.demonwav.mcdev.annotations.CheckEnv" + const val MCDEV_SIDE = "com.demonwav.mcdev.annotations.Env" + + internal fun getExplicitAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { + if (hardness != SideHardness.HARD) { + val mcDevAnnotation = element.findAnnotation(MCDEV_SIDEONLY_ANNOTATION) + if (mcDevAnnotation != null) { + val side = mcDevAnnotation.findAttributeValue("value") as? PsiReferenceExpression + ?: return null + val sideConstant = side.resolve() as? PsiEnumConstant ?: return null + return when (sideConstant.name) { + "CLIENT" -> SideInstance.createMcDev(Side.CLIENT, element) + "SERVER" -> SideInstance.createMcDev(Side.SERVER, element) + else -> null + } + } + } + + if (hardness != SideHardness.SOFT) { + val sideOnlyAnnotation = element.findAnnotation(ForgeConstants.SIDE_ONLY_ANNOTATION) + if (sideOnlyAnnotation != null) { + val side = sideOnlyAnnotation.findAttributeValue("value") as? PsiReferenceExpression + ?: return null + val sideConstant = side.resolve() as? PsiEnumConstant ?: return null + return when (sideConstant.name) { + "CLIENT" -> SideInstance.createSideOnly(Side.CLIENT, element) + "SERVER" -> SideInstance.createSideOnly(Side.SERVER, element) + else -> null + } + } + + val environmentAnnotation = element.findAnnotation(FabricConstants.ENVIRONMENT_ANNOTATION) + if (environmentAnnotation != null) { + val env = environmentAnnotation.findAttributeValue("value") as? PsiReferenceExpression + ?: return null + val envConstant = env.resolve() as? PsiEnumConstant ?: return null + return when (envConstant.name) { + "CLIENT" -> SideInstance.createEnvironment(Side.CLIENT, element) + "SERVER" -> SideInstance.createEnvironment(Side.SERVER, element) + else -> null + } + } + + val onlyInAnnotation = element.annotations.firstOrNull { + it.hasQualifiedName(ForgeConstants.ONLY_IN_ANNOTATION) && + it.findAttributeValue("_interface") == null + } + if (onlyInAnnotation != null) { + val dist = onlyInAnnotation.findAttributeValue("value") as? PsiReferenceExpression + ?: return null + val distConstant = dist.resolve() as? PsiEnumConstant ?: return null + return when (distConstant.name) { + "CLIENT" -> SideInstance.createOnlyIn(Side.CLIENT, element) + "DEDICATED_SERVER" -> SideInstance.createOnlyIn(Side.SERVER, element) + else -> null + } + } + } + + return null + } + + internal fun getExplicitOrInferredAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { + val explicitAnnotation = getExplicitAnnotation(element, hardness) + if (explicitAnnotation != null) { + return explicitAnnotation + } + + return when (element) { + is PsiClass -> getInferredClassAnnotation(element, hardness) + is PsiMethod -> getInferredMethodAnnotation(element, hardness) + else -> null + } + } + + private fun getInferredClassAnnotation(cls: PsiClass, hardness: SideHardness): SideInstance? { + // If mixing into a client class, the mixin is also clientside + if (cls.isMixin) { + val side = PsiResolveHelper.ourGraphGuard.doPreventingRecursion(cls, true) preventRecursion@{ + var ret = Side.BOTH + for (target in cls.mixinTargets) { + val newSide = getExplicitOrInferredAnnotation(target, hardness)?.side ?: Side.BOTH + if (newSide != Side.BOTH) { + if (ret != Side.BOTH && newSide != ret) { + return@preventRecursion Side.BOTH + } + ret = newSide + } + } + return@preventRecursion ret + } ?: Side.BOTH + return if (side == Side.BOTH) { + null + } else { + SideInstance.createImplicitMcDev(side, cls) + } + } + + // Inherit side from superclass + val superSide = PsiResolveHelper.ourGraphGuard.doPreventingRecursion(cls, true) { + cls.superClass?.let { getExplicitOrInferredAnnotation(it, hardness) } + }?.side ?: Side.BOTH + if (superSide != Side.BOTH) { + return SideInstance.createImplicitMcDev(superSide, cls) + } + + // Inner classes inherit their side from outer classes + val parentSide = PsiResolveHelper.ourGraphGuard.doPreventingRecursion(cls, true) { + cls.parentOfType()?.let { getExplicitOrInferredAnnotation(it, hardness) } + }?.side ?: Side.BOTH + if (parentSide != Side.BOTH) { + return SideInstance.createImplicitMcDev(parentSide, cls) + } + + if (hardness != SideHardness.HARD) { + // Inherit from the side of the whole mod + val fabricModule = cls.findModule()?.let { module -> + MinecraftFacet.getInstance(module)?.getModuleOfType(FabricModuleType) + } + if (fabricModule != null) { + val fabricModJson = fabricModule.fabricJson?.let { + PsiManager.getInstance(cls.project).findFile(it) + } as? JsonFile + val jsonObj = fabricModJson?.topLevelValue as? JsonObject + val side = (jsonObj?.findProperty("environment")?.value as? JsonStringLiteral)?.value ?: "*" + if (side == "client") { + return SideInstance.createImplicitMcDev(Side.CLIENT, cls) + } else if (side == "server") { + return SideInstance.createImplicitMcDev(Side.SERVER, cls) + } + } + } + + return null + } + + private fun getInferredMethodAnnotation(method: PsiMethod, hardness: SideHardness): SideInstance? { + if (hardness == SideHardness.HARD) { + return null + } + + // Inherit side from super method + val superSide = PsiResolveHelper.ourGraphGuard.doPreventingRecursion(method, true) preventRecursion@{ + var side = Side.BOTH + for (superMethod in method.findSuperMethods()) { + val newSide = getExplicitOrInferredAnnotation(superMethod, SideHardness.EITHER)?.side ?: Side.BOTH + if (newSide != Side.BOTH) { + if (side != Side.BOTH && newSide != side) { + return@preventRecursion Side.BOTH + } + side = newSide + } + } + return@preventRecursion side + } ?: Side.BOTH + if (superSide != Side.BOTH) { + return SideInstance.createImplicitMcDev(superSide, method) + } + + return null + } + + private fun getDist(expression: PsiExpression): Side { + if (expression.type?.equalsToText(ForgeConstants.DIST_ANNOTATION) != true) { + return Side.BOTH + } + val field = (expression as? PsiReferenceExpression)?.resolve() as? PsiField ?: return Side.BOTH + if (field.containingClass?.qualifiedName == ForgeConstants.DIST_ANNOTATION) { + return when (field.name) { + "CLIENT" -> Side.CLIENT + "DEDICATED_SERVER" -> Side.SERVER + else -> Side.BOTH + } + } + return Side.BOTH + } + + private fun createDistExecutorSide(side: Side, lambda: PsiLambdaExpression): SideInstance? { + return when (side) { + Side.BOTH -> null + else -> SideInstance.createDistExecutor(side, lambda) + } + } + + private fun getDistExecutorSide( + methodCall: PsiMethodCallExpression, + method: PsiMethod, + lambda: PsiLambdaExpression + ): SideInstance? { + val args = methodCall.argumentList.expressions + val lambdaIndex = args.indexOf(lambda) + if (lambdaIndex == -1) { + return null + } + return when (method.name) { + "callWhenOn", + "unsafeCallWhenOn", + "safeCallWhenOn", + "runWhenOn", + "unsafeRunWhenOn", + "safeRunWhenOn" -> createDistExecutorSide(getDist(args[0]), lambda) + "runForDist", + "unsafeRunForDist", + "safeRunForDist" -> { + val side = if (lambdaIndex == 0) { + Side.CLIENT + } else { + Side.SERVER + } + createDistExecutorSide(side, lambda) + } + else -> null + } + } + + private fun getPackageSide(cls: PsiClass, hardness: SideHardness): SideInstance? { + // hard side annotations can't be put on packages + if (hardness == SideHardness.HARD) { + return null + } + + val packageName = cls.packageName ?: return null + var pkg = JavaPsiFacade.getInstance(cls.project).findPackage(packageName) + while (pkg != null) { + val side = getExplicitAnnotation(pkg, SideHardness.SOFT) + if (side != null) { + return side + } + pkg = pkg.parentPackage + } + + return null + } + + private fun getLambdaContextSide(element: PsiLambdaExpression, hardness: SideHardness): SideInstance? { + val methodCall = element.parentOfType() + val method = methodCall?.resolveMethod() + if (method?.hasModifierProperty(PsiModifier.STATIC) == true && + method.containingClass?.qualifiedName == ForgeConstants.DIST_EXECUTOR + ) { + val distExecutorSide = getDistExecutorSide(methodCall, method, element) + if (distExecutorSide != null) { + return distExecutorSide + } + } + if (hardness != SideHardness.HARD) { + val surroundingMethod = element.parentOfType(PsiMethod::class, PsiClass::class) + if (surroundingMethod is PsiMethod) { + val side = getContextSide(surroundingMethod, SideHardness.EITHER) + if (side != null) { + return SideInstance.createImplicitMcDev(side.side, element) + } + } + } + return getContextSide(element.parentOfType(), hardness) + } + + fun getContextSide(element: PsiElement?, hardness: SideHardness): SideInstance? { + element ?: return null + return when (element) { + is PsiLambdaExpression -> getLambdaContextSide(element, hardness) + is PsiMethod -> + getExplicitOrInferredAnnotation(element, hardness) ?: getContextSide(element.parent, hardness) + is PsiClass -> getExplicitOrInferredAnnotation(element, hardness) ?: getPackageSide(element, hardness) + else -> getContextSide(element.parent, hardness) + } + } + + fun getSidedInterfaces(clazz: PsiClass): Map { + val interfaces = mutableMapOf() + for (annotation in clazz.annotations) { + if (annotation.hasQualifiedName(FabricConstants.ENVIRONMENT_INTERFACE_ANNOTATION)) { + val value = annotation.findAttributeValue("value") as? PsiReferenceExpression + ?: continue + val itf = annotation.findAttributeValue("itf") as? PsiClassObjectAccessExpression + ?: continue + val itfFqn = (itf.operand.type as? PsiClassType)?.resolve()?.qualifiedName ?: continue + val envConstant = value.resolve() as? PsiEnumConstant ?: continue + when (envConstant.name) { + "CLIENT" -> interfaces[itfFqn] = Side.CLIENT + "SERVER" -> interfaces[itfFqn] = Side.SERVER + } + } else if (annotation.hasQualifiedName(ForgeConstants.ONLY_IN_ANNOTATION)) { + val value = annotation.findAttributeValue("value") as? PsiReferenceExpression + ?: continue + val itf = annotation.findAttributeValue("_interface") as? PsiClassObjectAccessExpression + ?: continue + val itfFqn = (itf.operand.type as? PsiClassType)?.resolve()?.qualifiedName ?: continue + val sideConstant = value.resolve() as? PsiEnumConstant ?: continue + when (sideConstant.name) { + "CLIENT" -> interfaces[itfFqn] = Side.CLIENT + "DEDICATED_SERVER" -> interfaces[itfFqn] = Side.SERVER + } + } + } + return interfaces + } + + fun analyzeBodyForSoftSideProblems(body: PsiElement, problems: ProblemsHolder) { + val runner = StandardDataFlowRunner() + val factory = runner.factory + val visitor = SoftSideOnlyInstructionVisitor(body, factory, problems) + runner.analyzeMethod(body, visitor) + } + + fun createInspectionMessage(from: SideInstance?, to: SideInstance): String { + val toType = getElementTypeName(to.element) + if (from == null) { + return "Cannot access $toType ${to.reason} from common code" + } + val fromType = getElementTypeName(from.element) + return "Cannot access $toType ${to.reason} from $fromType ${from.reason}" + } + + private fun getElementTypeName(element: PsiElement): String { + return when (element) { + is PsiPackage -> "package" + is PsiClass -> when { + element.isInterface -> "interface" + element.isEnum -> "enum" + element.isAnnotationType -> "@interface" + else -> "class" + } + is PsiField -> "field" + is PsiMethod -> when { + element.isConstructor -> "constructor" + else -> "method" + } + is PsiLambdaExpression -> "lambda" + else -> "element" + } + } + + fun getClassInType(type: PsiType): PsiClass? { + return when (type) { + is PsiClassType -> type.resolve() + is PsiArrayType -> getClassInType(type.deepComponentType) + else -> null + } + } +} diff --git a/src/main/kotlin/sideonly/SoftSideOnlyInstructionVisitor.kt b/src/main/kotlin/sideonly/SoftSideOnlyInstructionVisitor.kt new file mode 100644 index 000000000..11bd51d51 --- /dev/null +++ b/src/main/kotlin/sideonly/SoftSideOnlyInstructionVisitor.kt @@ -0,0 +1,185 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +import com.intellij.codeInsight.daemon.impl.quickfix.SimplifyBooleanExpressionFix +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.codeInspection.dataFlow.DataFlowRunner +import com.intellij.codeInspection.dataFlow.DfaInstructionState +import com.intellij.codeInspection.dataFlow.DfaMemoryState +import com.intellij.codeInspection.dataFlow.StandardInstructionVisitor +import com.intellij.codeInspection.dataFlow.instructions.InstanceofInstruction +import com.intellij.codeInspection.dataFlow.instructions.MethodCallInstruction +import com.intellij.codeInspection.dataFlow.instructions.MethodReferenceInstruction +import com.intellij.codeInspection.dataFlow.instructions.PushInstruction +import com.intellij.codeInspection.dataFlow.instructions.TypeCastInstruction +import com.intellij.codeInspection.dataFlow.value.DfaValueFactory +import com.intellij.codeInspection.dataFlow.value.DfaVariableValue +import com.intellij.psi.PsiAssignmentExpression +import com.intellij.psi.PsiClassObjectAccessExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiField +import com.intellij.psi.PsiInstanceOfExpression +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiNewExpression +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.util.PsiTreeUtil + +class SoftSideOnlyInstructionVisitor( + private val body: PsiElement, + private val factory: DfaValueFactory, + private val problems: ProblemsHolder +) : StandardInstructionVisitor() { + private val contextSide = SideOnlyUtil.getContextSide(body, SideHardness.EITHER) + private val isLogicalClientVars = mutableListOf() + + override fun visitPush( + instruction: PushInstruction, + runner: DataFlowRunner, + memState: DfaMemoryState + ): Array { + val dfaVar = instruction.value as? DfaVariableValue + ?: return super.visitPush(instruction, runner, memState) + + val expression = instruction.expression + + if (expression is PsiClassObjectAccessExpression) { + val targetClass = SideOnlyUtil.getClassInType(expression.operand.type) + ?: return super.visitPush(instruction, runner, memState) + val problemElement = expression.operand + checkAccess(memState, problemElement, targetClass) + return super.visitPush(instruction, runner, memState) + } + + val field = dfaVar.psiVariable as? PsiField ?: return super.visitPush(instruction, runner, memState) + val problemElement = (expression as? PsiReferenceExpression)?.referenceNameElement + + if (field.containingClass?.qualifiedName == "net.minecraft.world.World" && + (field.name == "isRemote" || field.name == "isClient") + ) { + if (isPhysicalServer(memState)) { + memState.setVarValue(dfaVar, factory.constFactory.`false`) + if (problemElement != null) { + val fix = createSimplifyBooleanFix(expression, false) + problems.registerProblem( + problemElement, + "Expression is always false", + *listOfNotNull(fix).toTypedArray() + ) + } + } + isLogicalClientVars.add(dfaVar) + } else { + if (problemElement != null) { + checkAccess(memState, problemElement, field) + } + } + + return super.visitPush(instruction, runner, memState) + } + + override fun visitMethodCall( + instruction: MethodCallInstruction, + runner: DataFlowRunner, + memState: DfaMemoryState + ): Array { + val result = super.visitMethodCall(instruction, runner, memState) + + val targetMethod = instruction.targetMethod ?: return result + val problemElement = when (val callExpression = instruction.callExpression) { + is PsiMethodCallExpression -> callExpression.methodExpression.referenceNameElement + is PsiNewExpression -> callExpression.classOrAnonymousClassReference + else -> return result + } ?: return result + checkAccess(memState, problemElement, targetMethod) + + return result + } + + override fun visitTypeCast( + instruction: TypeCastInstruction, + runner: DataFlowRunner, + memState: DfaMemoryState + ): Array { + val result = super.visitTypeCast(instruction, runner, memState) + + val targetClass = SideOnlyUtil.getClassInType(instruction.castTo) ?: return result + val problemElement = instruction.expression.castType ?: return result + checkAccess(memState, problemElement, targetClass) + + return result + } + + override fun visitInstanceof( + instruction: InstanceofInstruction, + runner: DataFlowRunner, + memState: DfaMemoryState + ): Array { + val result = super.visitInstanceof(instruction, runner, memState) + + val targetType = instruction.castType ?: return result + val targetClass = SideOnlyUtil.getClassInType(targetType) ?: return result + val problemElement = (instruction.expression as? PsiInstanceOfExpression)?.checkType ?: return result + checkAccess(memState, problemElement, targetClass) + + return result + } + + override fun visitMethodReference( + instruction: MethodReferenceInstruction, + runner: DataFlowRunner, + memState: DfaMemoryState + ): Array { + val result = super.visitMethodReference(instruction, runner, memState) + + val targetMember = instruction.expression.potentiallyApplicableMember ?: return result + val problemElement = instruction.expression.referenceNameElement ?: return result + checkAccess(memState, problemElement, targetMember) + + return result + } + + private fun checkAccess(memState: DfaMemoryState, problemElement: PsiElement, targetElement: PsiElement) { + val targetSide = SideOnlyUtil.getContextSide(targetElement, SideHardness.SOFT) + if (targetSide?.side == Side.CLIENT && !isPhysicalClient(memState)) { + problems.registerProblem(problemElement, SideOnlyUtil.createInspectionMessage(contextSide, targetSide)) + } else if (targetSide?.side == Side.SERVER && !isPhysicalServer(memState)) { + problems.registerProblem(problemElement, SideOnlyUtil.createInspectionMessage(contextSide, targetSide)) + } + } + + private fun isPhysicalClient(memState: DfaMemoryState): Boolean { + return contextSide?.side == Side.CLIENT || + isLogicalClientVars.any { memState.getConstantValue(it) == factory.constFactory.`true` } + } + + private fun isPhysicalServer(memState: DfaMemoryState): Boolean { + return contextSide?.side == Side.SERVER + } + + private fun createSimplifyBooleanFix(element: PsiElement, value: Boolean): LocalQuickFixOnPsiElement? { + val expression = element as? PsiExpression ?: return null + if (PsiTreeUtil.findChildOfType(expression, PsiAssignmentExpression::class.java) != null) return null + var wholeExpression = expression + var parent = expression.parent + while (parent is PsiExpression) { + wholeExpression = parent + parent = wholeExpression.parent + } + val fix = SimplifyBooleanExpressionFix(expression, value) + // simplify intention already active + if (!fix.isAvailable || SimplifyBooleanExpressionFix.canBeSimplified(wholeExpression)) return null + + return fix + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 182070d3a..325806414 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -83,6 +83,9 @@ parentId="Settings.Minecraft" instance="com.demonwav.mcdev.translations.sorting.TranslationTemplateConfigurable"/> + + + @@ -322,6 +325,13 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.translations.inspections.SuperfluousFormatInspection"/> + From a3fbaac57aee12aa18f58f714810dcd5631cb5a2 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 22 Aug 2020 18:16:00 +0100 Subject: [PATCH 2/5] Remove old SideOnly code --- .../fabric/creator/FabricProjectConfig.kt | 4 +- .../creator/FabricProjectSettingsWizard.kt | 4 +- .../platform/fabric/creator/FabricTemplate.kt | 2 +- src/main/kotlin/platform/forge/ForgeModule.kt | 24 -- .../FieldDeclarationSideOnlyInspection.kt | 126 ---------- ...alVariableDeclarationSideOnlyInspection.kt | 136 ----------- .../sideonly/MethodCallSideOnlyInspection.kt | 215 ------------------ .../sideonly/MethodSideOnlyInspection.kt | 133 ----------- .../sideonly/NestedClassSideOnlyInspection.kt | 85 ------- .../NewExpressionSideOnlyInspection.kt | 110 --------- .../RemoveAnnotationInspectionGadgetsFix.kt | 41 ---- .../forge/inspections/sideonly/Side.kt | 25 -- .../inspections/sideonly/SideOnlyUtil.kt | 173 -------------- .../sideonly/SidedProxyAnnotator.kt | 90 -------- .../sideonly/VariableUseSideOnlyInspection.kt | 182 --------------- .../mixin/inspection/UnusedMixinInspection.kt | 11 +- src/main/resources/META-INF/plugin.xml | 52 ----- 17 files changed, 13 insertions(+), 1400 deletions(-) delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/FieldDeclarationSideOnlyInspection.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/LocalVariableDeclarationSideOnlyInspection.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/MethodCallSideOnlyInspection.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/MethodSideOnlyInspection.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/NestedClassSideOnlyInspection.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/NewExpressionSideOnlyInspection.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/RemoveAnnotationInspectionGadgetsFix.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/Side.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/SideOnlyUtil.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/SidedProxyAnnotator.kt delete mode 100644 src/main/kotlin/platform/forge/inspections/sideonly/VariableUseSideOnlyInspection.kt diff --git a/src/main/kotlin/platform/fabric/creator/FabricProjectConfig.kt b/src/main/kotlin/platform/fabric/creator/FabricProjectConfig.kt index 87939f06b..b04f34e2c 100644 --- a/src/main/kotlin/platform/fabric/creator/FabricProjectConfig.kt +++ b/src/main/kotlin/platform/fabric/creator/FabricProjectConfig.kt @@ -17,7 +17,7 @@ import com.demonwav.mcdev.creator.buildsystem.gradle.GradleBuildSystem import com.demonwav.mcdev.creator.buildsystem.gradle.GradleCreator import com.demonwav.mcdev.platform.PlatformType import com.demonwav.mcdev.platform.fabric.EntryPoint -import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side +import com.demonwav.mcdev.sideonly.Side import com.demonwav.mcdev.util.License import com.demonwav.mcdev.util.SemanticVersion import com.intellij.openapi.module.Module @@ -35,7 +35,7 @@ class FabricProjectConfig : ProjectConfig(), GradleCreator { var apiMavenLocation: String? = null var loomVersion = SemanticVersion.release() var gradleVersion = SemanticVersion.release() - var environment = Side.NONE + var environment = Side.BOTH var entryPoints: List = arrayListOf() var modRepo: String? = null var mixins = false diff --git a/src/main/kotlin/platform/fabric/creator/FabricProjectSettingsWizard.kt b/src/main/kotlin/platform/fabric/creator/FabricProjectSettingsWizard.kt index ed8c8df77..751be51ea 100644 --- a/src/main/kotlin/platform/fabric/creator/FabricProjectSettingsWizard.kt +++ b/src/main/kotlin/platform/fabric/creator/FabricProjectSettingsWizard.kt @@ -19,7 +19,7 @@ import com.demonwav.mcdev.creator.ValidatedFieldType.NON_BLANK import com.demonwav.mcdev.platform.PlatformType import com.demonwav.mcdev.platform.fabric.EntryPoint import com.demonwav.mcdev.platform.fabric.util.FabricConstants -import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side +import com.demonwav.mcdev.sideonly.Side import com.demonwav.mcdev.util.License import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.modUpdateStep @@ -255,7 +255,7 @@ class FabricProjectSettingsWizard(private val creator: MinecraftProjectCreator) conf.environment = when ((environmentBox.selectedItem as? String)?.toLowerCase(Locale.ROOT)) { "client" -> Side.CLIENT "server" -> Side.SERVER - else -> Side.NONE + else -> Side.BOTH } conf.license = licenseBox.selectedItem as? License conf.entryPoints = entryPoints.filter { it.valid } diff --git a/src/main/kotlin/platform/fabric/creator/FabricTemplate.kt b/src/main/kotlin/platform/fabric/creator/FabricTemplate.kt index e27ffc347..1732de3ce 100644 --- a/src/main/kotlin/platform/fabric/creator/FabricTemplate.kt +++ b/src/main/kotlin/platform/fabric/creator/FabricTemplate.kt @@ -12,7 +12,7 @@ package com.demonwav.mcdev.platform.fabric.creator import com.demonwav.mcdev.creator.buildsystem.BuildSystem import com.demonwav.mcdev.platform.BaseTemplate -import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side +import com.demonwav.mcdev.sideonly.Side import com.demonwav.mcdev.util.License import com.demonwav.mcdev.util.MinecraftTemplates.Companion.FABRIC_BUILD_GRADLE_TEMPLATE import com.demonwav.mcdev.util.MinecraftTemplates.Companion.FABRIC_GRADLE_PROPERTIES_TEMPLATE diff --git a/src/main/kotlin/platform/forge/ForgeModule.kt b/src/main/kotlin/platform/forge/ForgeModule.kt index a3e703659..5674c3b27 100644 --- a/src/main/kotlin/platform/forge/ForgeModule.kt +++ b/src/main/kotlin/platform/forge/ForgeModule.kt @@ -16,7 +16,6 @@ import com.demonwav.mcdev.insight.generation.GenerationData import com.demonwav.mcdev.inspection.IsCancelled import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType -import com.demonwav.mcdev.platform.forge.inspections.sideonly.SidedProxyAnnotator import com.demonwav.mcdev.platform.forge.util.ForgeConstants import com.demonwav.mcdev.util.SourceType import com.demonwav.mcdev.util.extendsOrImplements @@ -26,7 +25,6 @@ import com.demonwav.mcdev.util.waitForAllSmart import com.intellij.json.JsonFileType import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileTypes.FileTypeManager -import com.intellij.openapi.project.DumbService import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType @@ -36,7 +34,6 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiType import com.intellij.psi.search.GlobalSearchScope -import com.intellij.psi.search.searches.AnnotatedElementsSearch class ForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(facet) { @@ -54,27 +51,6 @@ class ForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(f runWriteTaskLater { FileTypeManager.getInstance().associatePattern(JsonFileType.INSTANCE, ForgeConstants.MCMOD_INFO) } - - // Index @SideOnly - val service = DumbService.getInstance(project) - service.runReadActionInSmartMode runSmart@{ - if (service.isDumb || project.isDisposed) { - return@runSmart - } - - val scope = GlobalSearchScope.projectScope(project) - val sidedProxy = JavaPsiFacade.getInstance(project) - .findClass(ForgeConstants.SIDED_PROXY_ANNOTATION, scope) ?: return@runSmart - val annotatedFields = AnnotatedElementsSearch.searchPsiFields(sidedProxy, scope).findAll() - - for (field in annotatedFields) { - if (service.isDumb || project.isDisposed) { - return@runSmart - } - - SidedProxyAnnotator.check(field) - } - } } } diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/FieldDeclarationSideOnlyInspection.kt b/src/main/kotlin/platform/forge/inspections/sideonly/FieldDeclarationSideOnlyInspection.kt deleted file mode 100644 index 1d8461bda..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/FieldDeclarationSideOnlyInspection.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.intellij.psi.PsiClassType -import com.intellij.psi.PsiField -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor -import com.siyeh.ig.InspectionGadgetsFix -import org.jetbrains.annotations.Nls - -class FieldDeclarationSideOnlyInspection : BaseInspection() { - - @Nls - override fun getDisplayName() = "Invalid usage of @SideOnly in field declaration" - - override fun buildErrorString(vararg infos: Any): String { - val error = infos[0] as Error - return error.getErrorString(*SideOnlyUtil.getSubArray(infos)) - } - - override fun getStaticDescription(): String? { - return "A field in a class annotated for one side cannot be declared as being in the other side. " + - "For example, a class which is annotated as @SideOnly(Side.SERVER) cannot contain a field which is " + - "annotated as @SideOnly(Side.CLIENT). Since a class that is annotated with @SideOnly brings " + - "everything with it, @SideOnly annotated fields are usually useless" - } - - override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { - val field = infos[3] as PsiField - - return if (field.isWritable) { - RemoveAnnotationInspectionGadgetsFix(field, "Remove @SideOnly annotation from field") - } else { - null - } - } - - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitField(field: PsiField) { - val psiClass = field.containingClass ?: return - - if (!SideOnlyUtil.beginningCheck(field)) { - return - } - - val fieldSide = SideOnlyUtil.checkField(field) - if (fieldSide === Side.INVALID) { - return - } - - val classSide = SideOnlyUtil.getSideForClass(psiClass) - - if (fieldSide !== Side.NONE && fieldSide !== classSide) { - if (classSide !== Side.NONE && classSide !== Side.INVALID) { - registerFieldError( - field, - Error.CLASS_CROSS_ANNOTATED, - fieldSide.annotation, - classSide.annotation, - field - ) - } else if (classSide !== Side.NONE) { - registerFieldError(field, Error.CLASS_UNANNOTATED, fieldSide.annotation, null, field) - } - } - - if (fieldSide === Side.NONE) { - return - } - - if (field.type !is PsiClassType) { - return - } - - val type = field.type as PsiClassType - val fieldClass = type.resolve() ?: return - - val fieldClassSide = SideOnlyUtil.getSideForClass(fieldClass) - - if (fieldClassSide === Side.NONE || fieldClassSide === Side.INVALID) { - return - } - - if (fieldClassSide !== fieldSide) { - registerFieldError( - field, - Error.FIELD_CROSS_ANNOTATED, - fieldClassSide.annotation, - fieldSide.annotation, - field - ) - } - } - } - } - - enum class Error { - CLASS_UNANNOTATED { - override fun getErrorString(vararg infos: Any): String { - return "Field with type annotation ${infos[1]} cannot be declared in an un-annotated class" - } - }, - CLASS_CROSS_ANNOTATED { - override fun getErrorString(vararg infos: Any): String { - return "Field annotated with ${infos[0]} cannot be declared inside a class annotated with ${infos[1]}." - } - }, - FIELD_CROSS_ANNOTATED { - override fun getErrorString(vararg infos: Any): String { - return "Field with type annotation ${infos[0]} cannot be declared as ${infos[1]}." - } - }; - - abstract fun getErrorString(vararg infos: Any): String - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/LocalVariableDeclarationSideOnlyInspection.kt b/src/main/kotlin/platform/forge/inspections/sideonly/LocalVariableDeclarationSideOnlyInspection.kt deleted file mode 100644 index 1985e5b05..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/LocalVariableDeclarationSideOnlyInspection.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.demonwav.mcdev.util.findContainingClass -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiClassType -import com.intellij.psi.PsiLocalVariable -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor -import com.siyeh.ig.InspectionGadgetsFix -import org.jetbrains.annotations.Nls - -class LocalVariableDeclarationSideOnlyInspection : BaseInspection() { - - @Nls - override fun getDisplayName() = "Invalid usage of local variable declaration annotated with @SideOnly" - - override fun buildErrorString(vararg infos: Any): String { - val error = infos[0] as Error - return error.getErrorString(*SideOnlyUtil.getSubArray(infos)) - } - - override fun getStaticDescription() = - "A variable whose class declaration is annotated with @SideOnly for one side cannot be declared in a class" + - " or method that does not match the same side." - - override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { - val variableClass = infos[3] as PsiClass - - return if (variableClass.isWritable) { - RemoveAnnotationInspectionGadgetsFix( - variableClass, - "Remove @SideOnly annotation from variable class declaration" - ) - } else { - null - } - } - - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitLocalVariable(variable: PsiLocalVariable) { - val psiClass = variable.findContainingClass() ?: return - - if (!SideOnlyUtil.beginningCheck(variable)) { - return - } - - val type = variable.type as? PsiClassType ?: return - - val variableClass = type.resolve() ?: return - - val variableSide = SideOnlyUtil.getSideForClass(variableClass) - if (variableSide === Side.NONE || variableSide === Side.INVALID) { - return - } - - val containingClassSide = SideOnlyUtil.getSideForClass(psiClass) - val methodSide = SideOnlyUtil.checkElementInMethod(variable) - - var classAnnotated = false - - if (containingClassSide !== Side.NONE && containingClassSide !== Side.INVALID) { - if (variableSide !== containingClassSide) { - registerVariableError( - variable, - Error.VAR_CROSS_ANNOTATED_CLASS, - variableSide.annotation, - containingClassSide.annotation, - variableClass - ) - } - classAnnotated = true - } - - if (methodSide === Side.INVALID) { - return - } - - if (variableSide !== methodSide) { - if (methodSide === Side.NONE) { - if (!classAnnotated) { - registerVariableError( - variable, - Error.VAR_UNANNOTATED_METHOD, - variableSide.annotation, - methodSide.annotation, - variableClass - ) - } - } else { - registerVariableError( - variable, - Error.VAR_CROSS_ANNOTATED_METHOD, - variableSide.annotation, - methodSide.annotation, - variableClass - ) - } - } - } - } - } - - enum class Error { - VAR_CROSS_ANNOTATED_CLASS { - override fun getErrorString(vararg infos: Any): String { - return "A local variable whose class is annotated with ${infos[0]} " + - "cannot be used in a class annotated with ${infos[1]}" - } - }, - VAR_CROSS_ANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "A local variable whose class is annotated with ${infos[0]} " + - "cannot be used in a method annotated with ${infos[1]}" - } - }, - VAR_UNANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "A local variable whose class is annotated with ${infos[0]} " + - "cannot be used in an un-annotated method." - } - }; - - abstract fun getErrorString(vararg infos: Any): String - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/MethodCallSideOnlyInspection.kt b/src/main/kotlin/platform/forge/inspections/sideonly/MethodCallSideOnlyInspection.kt deleted file mode 100644 index e3aef554c..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/MethodCallSideOnlyInspection.kt +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.demonwav.mcdev.platform.forge.util.ForgeConstants -import com.demonwav.mcdev.util.findContainingClass -import com.intellij.psi.PsiField -import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiMethodCallExpression -import com.intellij.psi.PsiReferenceExpression -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor -import com.siyeh.ig.InspectionGadgetsFix -import org.jetbrains.annotations.Nls - -class MethodCallSideOnlyInspection : BaseInspection() { - - @Nls - override fun getDisplayName() = - "Invalid usage of a @SideOnly method call" - - override fun buildErrorString(vararg infos: Any): String { - val error = infos[0] as Error - return error.getErrorString(*SideOnlyUtil.getSubArray(infos)) - } - - override fun getStaticDescription() = - "Methods which are declared with a @SideOnly annotation can only be " + - "used in matching @SideOnly classes and methods." - - override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { - val method = infos[3] as PsiMethod - - return if (method.isWritable) { - RemoveAnnotationInspectionGadgetsFix(method, "Remove @SideOnly annotation from method declaration") - } else { - null - } - } - - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitMethodCallExpression(expression: PsiMethodCallExpression) { - if (!SideOnlyUtil.beginningCheck(expression)) { - return - } - - val referenceExpression = expression.methodExpression - val qualifierExpression = referenceExpression.qualifierExpression - - // If this field is a @SidedProxy field, don't check. This is because people often are naughty and use the server impl as - // the base class for their @SidedProxy class, and client extends it. this messes up our checks, so we will just assume the - // right class is loaded for @SidedProxy's - run skip@{ - if (qualifierExpression is PsiReferenceExpression) { - val resolve = qualifierExpression.resolve() as? PsiField ?: return@skip - - val resolveFieldModifierList = resolve.modifierList ?: return@skip - - if (resolveFieldModifierList.findAnnotation(ForgeConstants.SIDED_PROXY_ANNOTATION) == null) { - return@skip - } - - return - } - } - - val declaration = referenceExpression.resolve() as? PsiMethod ?: return - - var elementSide = SideOnlyUtil.checkMethod(declaration) - - // Check the class(es) the element is declared in - val declarationContainingClass = declaration.containingClass ?: return - - val declarationClassHierarchySides = SideOnlyUtil.checkClassHierarchy(declarationContainingClass) - - val declarationClassSide = SideOnlyUtil.getFirstSide(declarationClassHierarchySides) - - // The element inherits the @SideOnly from it's parent class if it doesn't explicitly set it itself - var inherited = false - if (declarationClassSide !== Side.NONE && (elementSide === Side.INVALID || elementSide === Side.NONE)) { - inherited = true - elementSide = declarationClassSide - } - - if (elementSide === Side.INVALID || elementSide === Side.NONE) { - return - } - - // Check the class(es) the element is in - val containingClass = expression.findContainingClass() ?: return - - val classSide = SideOnlyUtil.getSideForClass(containingClass) - - var classAnnotated = false - - if (classSide !== Side.NONE && classSide !== Side.INVALID) { - if (classSide !== elementSide) { - if (inherited) { - registerError( - referenceExpression.element, - Error.ANNOTATED_CLASS_METHOD_IN_CROSS_ANNOTATED_CLASS_METHOD, - elementSide.annotation, - classSide.annotation, - declaration - ) - } else { - registerError( - referenceExpression.element, - Error.ANNOTATED_METHOD_IN_CROSS_ANNOTATED_CLASS_METHOD, - elementSide.annotation, - classSide.annotation, - declaration - ) - } - } - classAnnotated = true - } - - // Check the method the element is in - val methodSide = SideOnlyUtil.checkElementInMethod(expression) - - // Put error on for method - if (elementSide !== methodSide && methodSide !== Side.INVALID) { - if (methodSide === Side.NONE) { - // If the class is properly annotated the method doesn't need to also be annotated - if (!classAnnotated) { - if (inherited) { - registerError( - referenceExpression.element, - Error.ANNOTATED_CLASS_METHOD_IN_UNANNOTATED_METHOD, - elementSide.annotation, null, - declaration - ) - } else { - registerError( - referenceExpression.element, - Error.ANNOTATED_METHOD_IN_UNANNOTATED_METHOD, - elementSide.annotation, null, - declaration - ) - } - } - } else { - if (inherited) { - registerError( - referenceExpression.element, - Error.ANNOTATED_CLASS_METHOD_IN_CROSS_ANNOTATED_METHOD, - elementSide.annotation, - methodSide.annotation, - declaration - ) - } else { - registerError( - referenceExpression.element, - Error.ANNOTATED_METHOD_IN_CROSS_ANNOTATED_METHOD, - elementSide.annotation, - methodSide.annotation, - declaration - ) - } - } - } - } - } - } - - enum class Error { - ANNOTATED_METHOD_IN_UNANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Method annotated with " + infos[0] + " cannot be referenced in an un-annotated method." - } - }, - ANNOTATED_CLASS_METHOD_IN_UNANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Method declared in a class annotated with " + infos[0] + - " cannot be referenced in an un-annotated method." - } - }, - ANNOTATED_METHOD_IN_CROSS_ANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Method annotated with " + infos[0] + - " cannot be referenced in a method annotated with " + infos[1] + "." - } - }, - ANNOTATED_CLASS_METHOD_IN_CROSS_ANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Method declared in a class annotated with " + infos[0] + - " cannot be referenced in a method annotated with " + infos[1] + "." - } - }, - ANNOTATED_METHOD_IN_CROSS_ANNOTATED_CLASS_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Method annotated with " + infos[0] + - " cannot be referenced in a class annotated with " + infos[1] + "." - } - }, - ANNOTATED_CLASS_METHOD_IN_CROSS_ANNOTATED_CLASS_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Method declared in a class annotated with " + infos[0] + - " cannot be referenced in a class annotated with " + infos[1] + "." - } - }; - - abstract fun getErrorString(vararg infos: Any): String - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/MethodSideOnlyInspection.kt b/src/main/kotlin/platform/forge/inspections/sideonly/MethodSideOnlyInspection.kt deleted file mode 100644 index 6c855016a..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/MethodSideOnlyInspection.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.intellij.psi.PsiClassType -import com.intellij.psi.PsiMethod -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor -import com.siyeh.ig.InspectionGadgetsFix -import org.jetbrains.annotations.Nls - -class MethodSideOnlyInspection : BaseInspection() { - - @Nls - override fun getDisplayName() = "Invalid usage of @SideOnly in method declaration" - - override fun buildErrorString(vararg infos: Any): String { - val error = infos[0] as Error - return error.getErrorString(*SideOnlyUtil.getSubArray(infos)) - } - - override fun getStaticDescription(): String? { - return "A method in a class annotated for one side cannot be declared as being in the other side. " + - "For example, a class which is annotated as @SideOnly(Side.SERVER) cannot contain a method which " + - "is annotated as @SideOnly(Side.CLIENT). Since a class that is annotated with @SideOnly brings " + - "everything with it, @SideOnly annotated methods are usually useless" - } - - override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { - val error = infos[0] as Error - val method = infos[3] as PsiMethod - - return if (method.isWritable && error === Error.METHOD_IN_WRONG_CLASS) { - RemoveAnnotationInspectionGadgetsFix(method, "Remove @SideOnly annotation from method") - } else { - null - } - } - - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitMethod(method: PsiMethod) { - val psiClass = method.containingClass ?: return - - if (!SideOnlyUtil.beginningCheck(method)) { - return - } - - val methodSide = SideOnlyUtil.checkMethod(method) - - val returnType = method.returnType as? PsiClassType ?: return - - val resolve = returnType.resolve() ?: return - - val returnSide = SideOnlyUtil.getSideForClass(resolve) - if (returnSide !== Side.NONE && returnSide !== Side.INVALID && returnSide !== methodSide && - methodSide !== Side.NONE && methodSide !== Side.INVALID - ) { - registerMethodError( - method, - Error.RETURN_TYPE_ON_WRONG_METHOD, - methodSide.annotation, - returnSide.annotation, - method - ) - } - - val classHierarchySides = SideOnlyUtil.checkClassHierarchy(psiClass) - - for (classHierarchySide in classHierarchySides) { - if (classHierarchySide.first !== Side.NONE && classHierarchySide.first !== Side.INVALID) { - if ( - methodSide !== classHierarchySide.first && - methodSide !== Side.NONE && - methodSide !== Side.INVALID - ) { - registerMethodError( - method, - Error.METHOD_IN_WRONG_CLASS, - methodSide.annotation, - classHierarchySide.first.annotation, - method - ) - } - if (returnSide !== Side.NONE && returnSide !== Side.INVALID) { - if (returnSide !== classHierarchySide.first) { - registerMethodError( - method, - Error.RETURN_TYPE_IN_WRONG_CLASS, - classHierarchySide.first.annotation, - returnSide.annotation, - method - ) - } - } - return - } - } - } - } - } - - enum class Error { - METHOD_IN_WRONG_CLASS { - override fun getErrorString(vararg infos: Any): String { - return "Method annotated with " + infos[0] + - " cannot be declared inside a class annotated with " + infos[1] + "." - } - }, - RETURN_TYPE_ON_WRONG_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Method annotated with " + infos[0] + - " cannot return a type annotated with " + infos[1] + "." - } - }, - RETURN_TYPE_IN_WRONG_CLASS { - override fun getErrorString(vararg infos: Any): String { - return "Method in a class annotated with " + infos[0] + - " cannot return a type annotated with " + infos[1] + "." - } - }; - - abstract fun getErrorString(vararg infos: Any): String - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/NestedClassSideOnlyInspection.kt b/src/main/kotlin/platform/forge/inspections/sideonly/NestedClassSideOnlyInspection.kt deleted file mode 100644 index 524c137eb..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/NestedClassSideOnlyInspection.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.intellij.psi.PsiClass -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor -import com.siyeh.ig.InspectionGadgetsFix -import org.jetbrains.annotations.Nls - -class NestedClassSideOnlyInspection : BaseInspection() { - - @Nls - override fun getDisplayName() = "Invalid usage of @SideOnly in nested class declaration" - - override fun buildErrorString(vararg infos: Any) = - "A nested class cannot declare a side that is different from the parent class." + - "\nEither remove the nested class's @SideOnly annotation, or change it to match it's parent's side." - - override fun getStaticDescription(): String? { - return "Classes which are annotated with @SideOnly cannot contain any nested classes which are " + - "annotated with a different @SideOnly annotation. Since a class that is annotated with @SideOnly " + - "brings everything with it, @SideOnly annotated nested classes are usually useless." - } - - override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { - val psiClass = infos[0] as PsiClass - - return if (psiClass.isWritable) { - RemoveAnnotationInspectionGadgetsFix(psiClass, "Remove @SideOnly annotation from nested class") - } else { - null - } - } - - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitClass(aClass: PsiClass) { - if (aClass.parent == null) { - return - } - - aClass.nameIdentifier ?: return - - if (!SideOnlyUtil.beginningCheck(aClass)) { - return - } - - val classHierarchyList = SideOnlyUtil.checkClassHierarchy(aClass) - - // The class lists are ordered from lowest to highest in the hierarchy - that is the first element in the list - // is the most nested class, and the last element in the list is the top level class - // - // In this case, the higher-level classes take precedence, so if a class is annotated as @SideOnly.CLIENT and a nested class is - // annotated as @SideOnly.SERVER, the nested class is the class that is in error, not the top level class - var currentSide = Side.NONE - for (pair in classHierarchyList) { - if (currentSide === Side.NONE) { - // If currentSide is NONE, then a class hasn't declared yet what it is - if (pair.first !== Side.NONE && pair.first !== Side.INVALID) { - currentSide = pair.first - } else { - // We are only worried about this class - return - } - } else if (pair.first !== Side.NONE && pair.first !== Side.INVALID) { - if (pair.first !== currentSide) { - registerClassError(aClass, aClass) - } else { - return - } - } - } - } - } - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/NewExpressionSideOnlyInspection.kt b/src/main/kotlin/platform/forge/inspections/sideonly/NewExpressionSideOnlyInspection.kt deleted file mode 100644 index ef12635a5..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/NewExpressionSideOnlyInspection.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.demonwav.mcdev.util.findContainingClass -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiNewExpression -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor -import com.siyeh.ig.InspectionGadgetsFix -import org.jetbrains.annotations.Nls - -class NewExpressionSideOnlyInspection : BaseInspection() { - - @Nls - override fun getDisplayName() = "Invalid usage of class annotated with @SideOnly" - - override fun buildErrorString(vararg infos: Any) = - "A class annotated with @SideOnly can only be used in other matching annotated classes and methods" - - override fun getStaticDescription(): String? { - return "A class that is annotated as @SideOnly(Side.CLIENT) or @SideOnly(Side.SERVER) cannot be " + - "used in classes or methods which are annotated differently, or not at all. Since the " + - "irrelevant code is removed when operating as a server or a client, common code cannot " + - "use @SideOnly annotated classes either." - } - - override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { - val psiClass = infos[0] as PsiClass - - return if (psiClass.isWritable) { - RemoveAnnotationInspectionGadgetsFix(psiClass, "Remove @SideOnly annotation from class declaration") - } else { - null - } - } - - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitNewExpression(expression: PsiNewExpression) { - if (!SideOnlyUtil.beginningCheck(expression)) { - return - } - - val element = expression.classReference ?: return - - val psiElement = (element.resolve() ?: return) as? PsiClass ?: return - - val list = SideOnlyUtil.checkClassHierarchy(psiElement) - - var classSide = Side.NONE - - var offender: PsiClass? = null - for (pair in list) { - if (pair.first !== Side.NONE && pair.first !== Side.INVALID) { - classSide = pair.first - offender = pair.second - break - } - } - - if (classSide === Side.NONE) { - return - } - - // Check the class(es) the element is in - val containingClass = expression.findContainingClass() ?: return - - val containingClassSide = SideOnlyUtil.getSideForClass(containingClass) - // Check the method the element is in - val methodSide = SideOnlyUtil.checkElementInMethod(expression) - - var classAnnotated = false - - if (containingClassSide !== Side.NONE && containingClassSide !== Side.INVALID) { - if (containingClassSide !== classSide) { - registerError(expression, offender) - } - classAnnotated = true - } else { - if (methodSide === Side.INVALID) { - // It's not in a method - registerError(expression, offender) - return - } - } - - // Put error on for method - if (classSide !== methodSide && methodSide !== Side.INVALID) { - if (methodSide === Side.NONE) { - // If the class is properly annotated the method doesn't need to also be annotated - if (!classAnnotated) { - registerError(expression, offender) - } - } else { - registerError(expression, offender) - } - } - } - } - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/RemoveAnnotationInspectionGadgetsFix.kt b/src/main/kotlin/platform/forge/inspections/sideonly/RemoveAnnotationInspectionGadgetsFix.kt deleted file mode 100644 index dd3ce8faf..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/RemoveAnnotationInspectionGadgetsFix.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.demonwav.mcdev.platform.forge.util.ForgeConstants -import com.intellij.codeInspection.ProblemDescriptor -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiModifierListOwner -import com.intellij.structuralsearch.plugin.util.SmartPsiPointer -import com.siyeh.ig.InspectionGadgetsFix -import org.jetbrains.annotations.Nls - -class RemoveAnnotationInspectionGadgetsFix(element: PsiModifierListOwner, private val name: String) : - InspectionGadgetsFix() { - - private val pointer: SmartPsiPointer = SmartPsiPointer(element) - - override fun doFix(project: Project, descriptor: ProblemDescriptor) { - val owner = pointer.element as? PsiModifierListOwner ?: return - val list = owner.modifierList ?: return - val annotation = list.findAnnotation(ForgeConstants.SIDE_ONLY_ANNOTATION) ?: return - - annotation.delete() - } - - @Nls - override fun getName() = name - - @Nls - override fun getFamilyName(): String { - return name - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/Side.kt b/src/main/kotlin/platform/forge/inspections/sideonly/Side.kt deleted file mode 100644 index 61e501126..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/Side.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.intellij.openapi.util.Key - -enum class Side(val annotation: String) { - - CLIENT("SideOnly.CLIENT"), - SERVER("SideOnly.SERVER"), - NONE("NONE"), - INVALID("INVALID"); - - companion object { - val KEY = Key("MC_DEV_SIDE") - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/SideOnlyUtil.kt b/src/main/kotlin/platform/forge/inspections/sideonly/SideOnlyUtil.kt deleted file mode 100644 index 627c853a8..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/SideOnlyUtil.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.platform.forge.ForgeModuleType -import com.demonwav.mcdev.platform.forge.util.ForgeConstants -import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.openapi.util.Pair -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiField -import com.intellij.psi.PsiMethod -import java.util.Arrays -import java.util.LinkedList - -object SideOnlyUtil { - - fun beginningCheck(element: PsiElement): Boolean { - // We need the module to get the MinecraftModule - val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return false - - // Check that the MinecraftModule - // 1. Exists - // 2. Is a ForgeModuleType - val facet = MinecraftFacet.getInstance(module) - return facet != null && facet.isOfType(ForgeModuleType) - } - - private fun normalize(text: String): String { - if (text.startsWith(ForgeConstants.SIDE_ANNOTATION)) { - // We chop off the "net.minecraftforge.fml.relauncher." part here - return text.substring(text.lastIndexOf(".") - 4) - } - return text - } - - fun checkMethod(method: PsiMethod): Side { - val methodAnnotation = - // It's not annotated, which would be invalid if the element was annotated - method.modifierList.findAnnotation(ForgeConstants.SIDE_ONLY_ANNOTATION) - // (which, if we've gotten this far, is true) - ?: return Side.NONE - - // Check the value of the annotation - val methodValue = - // The annotation has no value yet, IntelliJ will give it's own error because a value is required - methodAnnotation.findAttributeValue("value") - ?: return Side.INVALID - - // Return the value of the annotation - return getFromName(methodValue.text) - } - - fun checkElementInMethod(element: PsiElement): Side { - var changingElement = element - // Maybe there is a better way of doing this, I don't know, but crawl up the PsiElement stack in search of the - // method this element is in. If it's not in a method it won't find one and the PsiMethod will be null - var method: PsiMethod? = null - while (method == null && changingElement.parent != null) { - val parent = changingElement.parent - - if (parent is PsiMethod) { - method = parent - } else { - changingElement = parent - } - - if (parent is PsiClass) { - break - } - } - - // No method was found - if (method == null) { - return Side.INVALID - } - - return checkMethod(method) - } - - fun checkClassHierarchy(psiClass: PsiClass): List> { - val classList = LinkedList() - classList.add(psiClass) - - var parent: PsiElement = psiClass - while (parent.parent != null) { - parent = parent.parent - - if (parent is PsiClass) { - classList.add(parent) - } - } - - // We want to use an array list so indexing into the list is not expensive - return classList.map { checkClass(it) } - } - - fun getSideForClass(psiClass: PsiClass): Side { - return getFirstSide(checkClassHierarchy(psiClass)) - } - - private fun checkClass(psiClass: PsiClass): Pair { - val side = psiClass.getUserData(Side.KEY) - if (side != null) { - return Pair(side, psiClass) - } - - val modifierList = psiClass.modifierList ?: return Pair(Side.NONE, psiClass) - - // Check for the annotation, if it's not there then we return none, but this is - // usually irrelevant for classes - val annotation = modifierList.findAnnotation(ForgeConstants.SIDE_ONLY_ANNOTATION) - if (annotation == null) { - if (psiClass.supers.isEmpty()) { - return Pair(Side.NONE, psiClass) - } - - // check the classes this class extends - return psiClass.supers.asSequence() - .filter { - // Prevent stack-overflow on cyclic dependencies - psiClass != it - } - .map { checkClassHierarchy(it) } - .firstOrNull { it.isNotEmpty() }?.let { Pair(it[0].getFirst(), psiClass) } ?: Pair(Side.NONE, psiClass) - } - - // Check the value on the annotation. If it's not there, IntelliJ will throw - // it's own error - val value = annotation.findAttributeValue("value") ?: return Pair(Side.INVALID, psiClass) - - return Pair(getFromName(value.text), psiClass) - } - - fun checkField(field: PsiField): Side { - // We check if this field has the @SideOnly annotation we are looking for - // If it doesn't, we aren't worried about it - val modifierList = field.modifierList ?: return Side.NONE - val annotation = modifierList.findAnnotation(ForgeConstants.SIDE_ONLY_ANNOTATION) ?: return Side.NONE - - // The value may not necessarily be set, but that will give an error by default as "value" is a - // required value for @SideOnly - val value = annotation.findAttributeValue("value") ?: return Side.INVALID - - // Finally, get the value of the SideOnly - return SideOnlyUtil.getFromName(value.text) - } - - private fun getFromName(name: String): Side { - return when (normalize(name)) { - "Side.SERVER" -> Side.SERVER - "Side.CLIENT" -> Side.CLIENT - else -> Side.INVALID - } - } - - fun getFirstSide(list: List>): Side { - return list.firstOrNull { it.first !== Side.NONE }?.first ?: Side.NONE - } - - fun getSubArray(infos: Array): Array { - return Arrays.copyOfRange(infos, 1, infos.size - 1) - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/SidedProxyAnnotator.kt b/src/main/kotlin/platform/forge/inspections/sideonly/SidedProxyAnnotator.kt deleted file mode 100644 index d63ca4d27..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/SidedProxyAnnotator.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.platform.forge.ForgeModuleType -import com.demonwav.mcdev.platform.forge.util.ForgeConstants -import com.google.common.base.Strings -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.lang.annotation.Annotator -import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiAnnotationMemberValue -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiField -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.impl.JavaConstantExpressionEvaluator -import com.intellij.psi.impl.source.tree.java.PsiLiteralExpressionImpl -import com.intellij.psi.search.GlobalSearchScope - -class SidedProxyAnnotator : Annotator { - override fun annotate(element: PsiElement, holder: AnnotationHolder) { - if (element !is PsiField) { - return - } - - val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return - - val instance = MinecraftFacet.getInstance(module) ?: return - - if (!instance.isOfType(ForgeModuleType)) { - return - } - - check(element) - } - - companion object { - fun check(field: PsiField) { - val modifierList = field.modifierList ?: return - - val annotation = modifierList.findAnnotation(ForgeConstants.SIDED_PROXY_ANNOTATION) ?: return - - val clientSide = annotation.findAttributeValue("clientSide") - val serverSide = annotation.findAttributeValue("serverSide") - - if (clientSide != null && !Strings.isNullOrEmpty(clientSide.text)) { - annotateClass(clientSide, Side.CLIENT) - } - - if (serverSide != null && !Strings.isNullOrEmpty(serverSide.text)) { - annotateClass(serverSide, Side.SERVER) - } - } - - private fun annotateClass(value: PsiAnnotationMemberValue, side: Side) { - val text: String? - if (value is PsiLiteralExpressionImpl) { - text = value.innerText - if (text == null) { - return - } - } else if (value is PsiReferenceExpression) { - val resolve = value.resolve() as? PsiField ?: return - - text = JavaConstantExpressionEvaluator.computeConstantExpression( - resolve.initializer, - null, - false - ) as? String ?: return - } else { - return - } - - val psiClass = - JavaPsiFacade.getInstance(value.project).findClass(text, GlobalSearchScope.allScope(value.project)) - ?: return - - psiClass.putUserData(Side.KEY, side) - } - } -} diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/VariableUseSideOnlyInspection.kt b/src/main/kotlin/platform/forge/inspections/sideonly/VariableUseSideOnlyInspection.kt deleted file mode 100644 index f25301f41..000000000 --- a/src/main/kotlin/platform/forge/inspections/sideonly/VariableUseSideOnlyInspection.kt +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.platform.forge.inspections.sideonly - -import com.demonwav.mcdev.util.findContainingClass -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.impl.source.PsiFieldImpl -import com.siyeh.ig.BaseInspection -import com.siyeh.ig.BaseInspectionVisitor -import org.jetbrains.annotations.Nls - -class VariableUseSideOnlyInspection : BaseInspection() { - - @Nls - override fun getDisplayName() = "Invalid usage of variable annotated with @SideOnly" - - override fun buildErrorString(vararg infos: Any): String { - val error = infos[0] as Error - return error.getErrorString(*SideOnlyUtil.getSubArray(infos)) - } - - override fun getStaticDescription() = - "Variables which are declared with a @SideOnly annotation can only be used " + - "in matching @SideOnly classes and methods." - - override fun buildVisitor(): BaseInspectionVisitor { - return object : BaseInspectionVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression?) { - if (!SideOnlyUtil.beginningCheck(expression!!)) { - return - } - - val declaration = expression.resolve() as? PsiFieldImpl ?: return - - var elementSide = SideOnlyUtil.checkField(declaration) - - // Check the class(es) the element is declared in - val declarationContainingClass = declaration.containingClass ?: return - - val declarationClassHierarchySides = SideOnlyUtil.checkClassHierarchy(declarationContainingClass) - - val declarationClassSide = SideOnlyUtil.getFirstSide(declarationClassHierarchySides) - - // The element inherits the @SideOnly from it's parent class if it doesn't explicitly set it itself - var inherited = false - if (declarationClassSide !== Side.NONE && (elementSide === Side.INVALID || elementSide === Side.NONE)) { - inherited = true - elementSide = declarationClassSide - } - - if (elementSide === Side.INVALID || elementSide === Side.NONE) { - return - } - - // Check the class(es) the element is in - val containingClass = expression.findContainingClass() ?: return - - val classSide = SideOnlyUtil.getSideForClass(containingClass) - - var classAnnotated = false - - if (classSide !== Side.NONE && classSide !== Side.INVALID) { - if (classSide !== elementSide) { - if (inherited) { - registerError( - expression.element, - Error.ANNOTATED_CLASS_VAR_IN_CROSS_ANNOTATED_CLASS_METHOD, - elementSide.annotation, - classSide.annotation, - declaration - ) - } else { - registerError( - expression.element, - Error.ANNOTATED_VAR_IN_CROSS_ANNOTATED_CLASS_METHOD, - elementSide.annotation, - classSide.annotation, - declaration - ) - } - } - classAnnotated = true - } - - // Check the method the element is in - val methodSide = SideOnlyUtil.checkElementInMethod(expression) - - // Put error on for method - if (elementSide !== methodSide && methodSide !== Side.INVALID) { - if (methodSide === Side.NONE) { - // If the class is properly annotated the method doesn't need to also be annotated - if (!classAnnotated) { - if (inherited) { - registerError( - expression.element, - Error.ANNOTATED_CLASS_VAR_IN_UNANNOTATED_METHOD, - elementSide.annotation, - null, - declaration - ) - } else { - registerError( - expression.element, - Error.ANNOTATED_VAR_IN_UNANNOTATED_METHOD, - elementSide.annotation, - null, - declaration - ) - } - } - } else { - if (inherited) { - registerError( - expression.element, - Error.ANNOTATED_CLASS_VAR_IN_CROSS_ANNOTATED_METHOD, - elementSide.annotation, - methodSide.annotation, - declaration - ) - } else { - registerError( - expression.element, - Error.ANNOTATED_VAR_IN_CROSS_ANNOTATED_METHOD, - elementSide.annotation, - methodSide.annotation, - declaration - ) - } - } - } - } - } - } - - enum class Error { - ANNOTATED_VAR_IN_UNANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Variable annotated with ${infos[0]} cannot be referenced in an un-annotated method." - } - }, - ANNOTATED_CLASS_VAR_IN_UNANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Variable declared in a class annotated with ${infos[0]} " + - "cannot be referenced in an un-annotated method." - } - }, - ANNOTATED_VAR_IN_CROSS_ANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Variable annotated with ${infos[0]} " + - "cannot be referenced in a method annotated with ${infos[1]}." - } - }, - ANNOTATED_CLASS_VAR_IN_CROSS_ANNOTATED_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Variable declared in a class annotated with ${infos[0]} " + - "cannot be referenced in a method annotated with ${infos[1]}." - } - }, - ANNOTATED_VAR_IN_CROSS_ANNOTATED_CLASS_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Variable annotated with ${infos[0]} " + - "cannot be referenced in a class annotated with ${infos[1]}." - } - }, - ANNOTATED_CLASS_VAR_IN_CROSS_ANNOTATED_CLASS_METHOD { - override fun getErrorString(vararg infos: Any): String { - return "Variable declared in a class annotated with ${infos[0]}" + - " cannot be referenced in a class annotated with ${infos[1]}." - } - }; - - abstract fun getErrorString(vararg infos: Any): String - } -} diff --git a/src/main/kotlin/platform/mixin/inspection/UnusedMixinInspection.kt b/src/main/kotlin/platform/mixin/inspection/UnusedMixinInspection.kt index 1f44eb4a6..3756c6453 100644 --- a/src/main/kotlin/platform/mixin/inspection/UnusedMixinInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/UnusedMixinInspection.kt @@ -10,11 +10,12 @@ package com.demonwav.mcdev.platform.mixin.inspection -import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side -import com.demonwav.mcdev.platform.forge.inspections.sideonly.SideOnlyUtil import com.demonwav.mcdev.platform.mixin.MixinModule import com.demonwav.mcdev.platform.mixin.config.MixinConfig import com.demonwav.mcdev.platform.mixin.util.isMixin +import com.demonwav.mcdev.sideonly.Side +import com.demonwav.mcdev.sideonly.SideHardness +import com.demonwav.mcdev.sideonly.SideOnlyUtil import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.fullQualifiedName import com.intellij.codeInspection.LocalQuickFix @@ -61,7 +62,11 @@ class UnusedMixinInspection : MixinInspection() { val bestQuickFixFile = bestQuickFixConfig?.file val qualifiedName = clazz.fullQualifiedName if (bestQuickFixFile != null && qualifiedName != null) { - val quickFix = QuickFix(bestQuickFixFile, qualifiedName, SideOnlyUtil.getSideForClass(clazz)) + val quickFix = QuickFix( + bestQuickFixFile, + qualifiedName, + SideOnlyUtil.getContextSide(clazz, SideHardness.EITHER)?.side ?: Side.BOTH + ) holder.registerProblem(problematicElement, "Mixin not found in any mixin config", quickFix) } else { holder.registerProblem(problematicElement, "Mixin not found in any mixin config") diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 325806414..3d2ed8cd2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -188,8 +188,6 @@ - - @@ -385,56 +383,6 @@ - - - - - - - - Date: Thu, 27 Aug 2020 17:42:30 +0100 Subject: [PATCH 3/5] Add inspection for hard SideOnly usage --- .../sideonly/HardSideOnlyUsageInspection.kt | 75 +++++++++++++++++ src/main/kotlin/sideonly/SideOnlyUtil.kt | 82 ++++++++----------- src/main/resources/META-INF/plugin.xml | 7 ++ 3 files changed, 118 insertions(+), 46 deletions(-) create mode 100644 src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt diff --git a/src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt b/src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt new file mode 100644 index 000000000..b97a7def8 --- /dev/null +++ b/src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt @@ -0,0 +1,75 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.SmartPointerManager +import com.intellij.psi.SmartPsiElementPointer +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.siyeh.ig.BaseInspection +import com.siyeh.ig.BaseInspectionVisitor +import com.siyeh.ig.InspectionGadgetsFix + +class HardSideOnlyUsageInspection : BaseInspection() { + override fun getDisplayName(): String { + return "Usage of hard-SideOnly annotations" + } + + override fun getStaticDescription(): String { + return "Usage of hard-SideOnly annotations" + } + + override fun buildErrorString(vararg infos: Any?): String { + return "Usage of @${infos[0] as String}" + } + + override fun buildVisitor(): BaseInspectionVisitor { + return Visitor() + } + + override fun buildFix(vararg infos: Any?): InspectionGadgetsFix { + return Fix(SmartPointerManager.createPointer(infos[1] as PsiAnnotation)) + } + + private class Visitor : BaseInspectionVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (SideOnlyUtil.getAnnotationSide(annotation, SideHardness.HARD) != Side.BOTH) { + registerError( + annotation.navigationElement, + annotation.nameReferenceElement?.text ?: annotation.qualifiedName, + annotation + ) + } + } + } + + private class Fix(private val annotation: SmartPsiElementPointer) : InspectionGadgetsFix() { + override fun doFix(project: Project, descriptor: ProblemDescriptor) { + val annotation = this.annotation.element ?: return + val oldSide = SideOnlyUtil.getAnnotationSide(annotation, SideHardness.HARD) + val newAnnotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText( + "@${SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION}(${SideOnlyUtil.MCDEV_SIDE}.$oldSide)", + annotation + ) + val createdAnnotation = annotation.replace(newAnnotation) + val codeStyleManager = JavaCodeStyleManager.getInstance(project) + codeStyleManager.shortenClassReferences(createdAnnotation) + createdAnnotation.containingFile?.let { codeStyleManager.optimizeImports(it) } + } + + override fun getName() = "Replace with @CheckEnv" + + override fun getFamilyName() = name + } +} diff --git a/src/main/kotlin/sideonly/SideOnlyUtil.kt b/src/main/kotlin/sideonly/SideOnlyUtil.kt index e78b3759d..97fe59628 100644 --- a/src/main/kotlin/sideonly/SideOnlyUtil.kt +++ b/src/main/kotlin/sideonly/SideOnlyUtil.kt @@ -16,7 +16,6 @@ import com.demonwav.mcdev.platform.fabric.util.FabricConstants import com.demonwav.mcdev.platform.forge.util.ForgeConstants import com.demonwav.mcdev.platform.mixin.util.isMixin import com.demonwav.mcdev.platform.mixin.util.mixinTargets -import com.demonwav.mcdev.util.findAnnotation import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.packageName import com.intellij.codeInspection.ProblemsHolder @@ -25,6 +24,7 @@ import com.intellij.json.psi.JsonFile import com.intellij.json.psi.JsonObject import com.intellij.json.psi.JsonStringLiteral import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiArrayType import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassObjectAccessExpression @@ -49,63 +49,53 @@ object SideOnlyUtil { const val MCDEV_SIDEONLY_ANNOTATION = "com.demonwav.mcdev.annotations.CheckEnv" const val MCDEV_SIDE = "com.demonwav.mcdev.annotations.Env" - internal fun getExplicitAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { + internal fun getAnnotationSide(annotation: PsiAnnotation, hardness: SideHardness): Side { + var isSideAnnotation = false + if (hardness != SideHardness.HARD) { - val mcDevAnnotation = element.findAnnotation(MCDEV_SIDEONLY_ANNOTATION) - if (mcDevAnnotation != null) { - val side = mcDevAnnotation.findAttributeValue("value") as? PsiReferenceExpression - ?: return null - val sideConstant = side.resolve() as? PsiEnumConstant ?: return null - return when (sideConstant.name) { - "CLIENT" -> SideInstance.createMcDev(Side.CLIENT, element) - "SERVER" -> SideInstance.createMcDev(Side.SERVER, element) - else -> null - } + if (annotation.hasQualifiedName(MCDEV_SIDEONLY_ANNOTATION)) { + isSideAnnotation = true } } if (hardness != SideHardness.SOFT) { - val sideOnlyAnnotation = element.findAnnotation(ForgeConstants.SIDE_ONLY_ANNOTATION) - if (sideOnlyAnnotation != null) { - val side = sideOnlyAnnotation.findAttributeValue("value") as? PsiReferenceExpression - ?: return null - val sideConstant = side.resolve() as? PsiEnumConstant ?: return null - return when (sideConstant.name) { - "CLIENT" -> SideInstance.createSideOnly(Side.CLIENT, element) - "SERVER" -> SideInstance.createSideOnly(Side.SERVER, element) - else -> null + if (annotation.hasQualifiedName(ForgeConstants.SIDE_ONLY_ANNOTATION) || + annotation.hasQualifiedName(FabricConstants.ENVIRONMENT_ANNOTATION) + ) { + isSideAnnotation = true + } else if (annotation.hasQualifiedName(ForgeConstants.ONLY_IN_ANNOTATION)) { + if (annotation.findAttributeValue("_interface") != null) { + isSideAnnotation = true } } + } - val environmentAnnotation = element.findAnnotation(FabricConstants.ENVIRONMENT_ANNOTATION) - if (environmentAnnotation != null) { - val env = environmentAnnotation.findAttributeValue("value") as? PsiReferenceExpression - ?: return null - val envConstant = env.resolve() as? PsiEnumConstant ?: return null - return when (envConstant.name) { - "CLIENT" -> SideInstance.createEnvironment(Side.CLIENT, element) - "SERVER" -> SideInstance.createEnvironment(Side.SERVER, element) - else -> null - } - } + if (!isSideAnnotation) { + return Side.BOTH + } - val onlyInAnnotation = element.annotations.firstOrNull { - it.hasQualifiedName(ForgeConstants.ONLY_IN_ANNOTATION) && - it.findAttributeValue("_interface") == null - } - if (onlyInAnnotation != null) { - val dist = onlyInAnnotation.findAttributeValue("value") as? PsiReferenceExpression - ?: return null - val distConstant = dist.resolve() as? PsiEnumConstant ?: return null - return when (distConstant.name) { - "CLIENT" -> SideInstance.createOnlyIn(Side.CLIENT, element) - "DEDICATED_SERVER" -> SideInstance.createOnlyIn(Side.SERVER, element) + val side = annotation.findAttributeValue("value") as? PsiReferenceExpression ?: return Side.BOTH + val sideConstant = side.resolve() as? PsiEnumConstant ?: return Side.BOTH + return when (sideConstant.name) { + "CLIENT" -> Side.CLIENT + "SERVER", "DEDICATED_SERVER" -> Side.SERVER + else -> Side.BOTH + } + } + + internal fun getExplicitAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { + return element.annotations.asSequence() + .map { it to getAnnotationSide(it, hardness) } + .firstOrNull { it.second != Side.BOTH } + ?.let { + when (it.first.qualifiedName) { + MCDEV_SIDEONLY_ANNOTATION -> SideInstance.createMcDev(it.second, element) + ForgeConstants.SIDE_ONLY_ANNOTATION -> SideInstance.createSideOnly(it.second, element) + ForgeConstants.ONLY_IN_ANNOTATION -> SideInstance.createOnlyIn(it.second, element) + FabricConstants.ENVIRONMENT_ANNOTATION -> SideInstance.createEnvironment(it.second, element) else -> null } } - } - - return null } internal fun getExplicitOrInferredAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 3d2ed8cd2..3584bafb4 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -330,6 +330,13 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.sideonly.SideOnlyInspection"/> + From 95e914a2a3ba251a0541e46b4242cf3260983a3a Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 30 Aug 2020 17:21:51 +0100 Subject: [PATCH 4/5] Add DistExecutor inspection --- .../inspections/DistExecutorInspection.kt | 69 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 7 ++ 2 files changed, 76 insertions(+) create mode 100644 src/main/kotlin/platform/forge/inspections/DistExecutorInspection.kt diff --git a/src/main/kotlin/platform/forge/inspections/DistExecutorInspection.kt b/src/main/kotlin/platform/forge/inspections/DistExecutorInspection.kt new file mode 100644 index 000000000..0ef7e91ab --- /dev/null +++ b/src/main/kotlin/platform/forge/inspections/DistExecutorInspection.kt @@ -0,0 +1,69 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.platform.forge.inspections + +import com.demonwav.mcdev.platform.forge.util.ForgeConstants +import com.intellij.psi.PsiErrorElement +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiLambdaExpression +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiMethodReferenceExpression +import com.siyeh.ig.BaseInspection +import com.siyeh.ig.BaseInspectionVisitor + +class DistExecutorInspection : BaseInspection() { + override fun getDisplayName() = "DistExecutor problems" + + override fun getStaticDescription() = "DistExecutor problems" + + override fun buildErrorString(vararg infos: Any?): String { + return infos[0] as String + } + + override fun buildVisitor(): BaseInspectionVisitor { + return Visitor() + } + + private class Visitor : BaseInspectionVisitor() { + override fun visitMethodCallExpression(expression: PsiMethodCallExpression) { + val method = expression.resolveMethod() ?: return + if (method.containingClass?.qualifiedName != ForgeConstants.DIST_EXECUTOR) return + when (method.name) { + "safeCallWhenOn", "safeRunWhenOn" -> { + checkSafeArgument(method.name, expression.argumentList.expressions.getOrNull(1)) + } + "safeRunForDist" -> { + for (arg in expression.argumentList.expressions) { + checkSafeArgument(method.name, arg) + } + } + } + } + + private fun checkSafeArgument(methodName: String, expression: PsiExpression?) { + if (expression == null || expression is PsiErrorElement || expression.textLength == 0) return + + if (expression !is PsiLambdaExpression) { + registerError(expression, "DistExecutor.$methodName must contain lambda argument") + return + } + + val lambdaBody = expression.body + if (lambdaBody != null && + lambdaBody !is PsiMethodReferenceExpression && + lambdaBody !is PsiErrorElement && + lambdaBody.textLength != 0 + ) { + registerError(lambdaBody, "DistExecutor.$methodName must contain a method reference inside a lambda") + } + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 3584bafb4..48a653590 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -397,6 +397,13 @@ level="ERROR" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.forge.inspections.simpleimpl.MissingMessageConstructorInspection"/> + From 2d660a2801f5f60752d9bd50f755e5387a1c714e Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 6 Sep 2020 21:18:41 +0100 Subject: [PATCH 5/5] Add dialog to automatically add mcdev annotations library if not present --- build.gradle.kts | 2 +- src/main/kotlin/MinecraftSettings.kt | 7 ++ .../sideonly/HardSideOnlyUsageInspection.kt | 32 +++++-- .../MakeInferredMcdevAnnotationExplicit.kt | 90 ++++++++++++++++++ .../SideOnlyInferredAnnotationProvider.kt | 42 --------- .../sideonly/SideOnlyLineMarkerProvider.kt | 92 ++++++++++++++++++ src/main/kotlin/sideonly/SideOnlyUtil.kt | 75 +++++++++++++-- src/main/kotlin/util/gradle-util.kt | 94 +++++++++++++++++++ src/main/kotlin/util/utils.kt | 25 +++++ src/main/resources/META-INF/plugin.xml | 12 ++- .../after.java.template | 7 ++ .../before.java.template | 3 + .../description.html | 15 +++ 13 files changed, 433 insertions(+), 63 deletions(-) create mode 100644 src/main/kotlin/sideonly/MakeInferredMcdevAnnotationExplicit.kt delete mode 100644 src/main/kotlin/sideonly/SideOnlyInferredAnnotationProvider.kt create mode 100644 src/main/kotlin/sideonly/SideOnlyLineMarkerProvider.kt create mode 100644 src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/after.java.template create mode 100644 src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/before.java.template create mode 100644 src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/description.html diff --git a/build.gradle.kts b/build.gradle.kts index e2df6cb4a..95485abf2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -109,7 +109,7 @@ intellij { version = ideaVersion // Bundled plugin dependencies setPlugins( - "java", "maven", "gradle", "Groovy", + "java", "maven", "gradle", "Groovy", "Kotlin", // needed dependencies for unit tests "properties", "junit", // useful to have when running for mods.toml diff --git a/src/main/kotlin/MinecraftSettings.kt b/src/main/kotlin/MinecraftSettings.kt index 91d2c0c75..64575cdce 100644 --- a/src/main/kotlin/MinecraftSettings.kt +++ b/src/main/kotlin/MinecraftSettings.kt @@ -24,6 +24,7 @@ class MinecraftSettings : PersistentStateComponent { var isShowEventListenerGutterIcons: Boolean = true, var isShowChatColorGutterIcons: Boolean = true, var isShowChatColorUnderlines: Boolean = false, + var isShowSideOnlyGutterIcons: Boolean = true, var underlineType: MinecraftSettings.UnderlineType = MinecraftSettings.UnderlineType.DOTTED ) @@ -62,6 +63,12 @@ class MinecraftSettings : PersistentStateComponent { state.isShowChatColorUnderlines = showChatColorUnderlines } + var isShowSideOnlyGutterIcons: Boolean + get() = state.isShowSideOnlyGutterIcons + set(showSideOnlyGutterIcons) { + state.isShowSideOnlyGutterIcons = showSideOnlyGutterIcons + } + var underlineType: UnderlineType get() = state.underlineType set(underlineType) { diff --git a/src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt b/src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt index b97a7def8..fc7c8f7e9 100644 --- a/src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt +++ b/src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt @@ -10,7 +10,11 @@ package com.demonwav.mcdev.sideonly +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.runInSmartModeFromReadAction import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.progress.util.ProgressWindow import com.intellij.openapi.project.Project import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation @@ -57,19 +61,29 @@ class HardSideOnlyUsageInspection : BaseInspection() { private class Fix(private val annotation: SmartPsiElementPointer) : InspectionGadgetsFix() { override fun doFix(project: Project, descriptor: ProblemDescriptor) { val annotation = this.annotation.element ?: return - val oldSide = SideOnlyUtil.getAnnotationSide(annotation, SideHardness.HARD) - val newAnnotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText( - "@${SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION}(${SideOnlyUtil.MCDEV_SIDE}.$oldSide)", - annotation - ) - val createdAnnotation = annotation.replace(newAnnotation) - val codeStyleManager = JavaCodeStyleManager.getInstance(project) - codeStyleManager.shortenClassReferences(createdAnnotation) - createdAnnotation.containingFile?.let { codeStyleManager.optimizeImports(it) } + val module = annotation.findModule() ?: return + if (!SideOnlyUtil.ensureMcdevDependencyPresent(project, module, name, annotation.resolveScope)) { + return + } + project.runInSmartModeFromReadAction(ProgressWindow(true, project)) { + val oldSide = SideOnlyUtil.getAnnotationSide(annotation, SideHardness.HARD) + val newAnnotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText( + "@${SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION}(${SideOnlyUtil.MCDEV_SIDE}.$oldSide)", + annotation + ) + WriteCommandAction.runWriteCommandAction(project) { + val createdAnnotation = annotation.replace(newAnnotation) + val codeStyleManager = JavaCodeStyleManager.getInstance(project) + codeStyleManager.shortenClassReferences(createdAnnotation) + createdAnnotation.containingFile?.let { codeStyleManager.optimizeImports(it) } + } + } } override fun getName() = "Replace with @CheckEnv" override fun getFamilyName() = name + + override fun startInWriteAction() = false } } diff --git a/src/main/kotlin/sideonly/MakeInferredMcdevAnnotationExplicit.kt b/src/main/kotlin/sideonly/MakeInferredMcdevAnnotationExplicit.kt new file mode 100644 index 000000000..bfe824335 --- /dev/null +++ b/src/main/kotlin/sideonly/MakeInferredMcdevAnnotationExplicit.kt @@ -0,0 +1,90 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +import com.demonwav.mcdev.util.findModule +import com.intellij.codeInsight.FileModificationService +import com.intellij.codeInsight.intention.impl.BaseIntentionAction +import com.intellij.lang.java.JavaLanguage +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiCompiledElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiModifierListOwner +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.util.PsiUtilCore + +class MakeInferredMcdevAnnotationExplicit : BaseIntentionAction() { + override fun getFamilyName() = "Make Inferred MinecraftDev Annotations Explicit" + + override fun getText() = "Make Inferred MinecraftDev Annotations Explicit" + + override fun isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean { + val leaf = file.findElementAt(editor.caretModel.offset) ?: return false + val owner = leaf.parent as? PsiModifierListOwner + return isAvailable(file, owner) + } + + fun isAvailable(file: PsiFile, owner: PsiModifierListOwner?): Boolean { + if (owner != null && + owner.language.isKindOf(JavaLanguage.INSTANCE) && + isWritable(owner) && + file.findModule() != null + ) { + val annotation = SideOnlyUtil.getInferredAnnotationOnly(owner, SideHardness.HARD) + ?: SideOnlyUtil.getInferredAnnotationOnly(owner, SideHardness.SOFT) + if (annotation != null) { + text = "Insert '@CheckEnv(Env.${annotation.side})'" + return true + } + } + return false + } + + private fun isWritable(owner: PsiModifierListOwner): Boolean { + if (owner is PsiCompiledElement) return false + val vFile = PsiUtilCore.getVirtualFile(owner) + return vFile != null && vFile.isInLocalFileSystem + } + + override fun invoke(project: Project, editor: Editor, file: PsiFile) { + val leaf = file.findElementAt(editor.caretModel.offset) ?: return + val owner = leaf.parent as? PsiModifierListOwner ?: return + makeAnnotationExplicit(project, file, owner) + } + + fun makeAnnotationExplicit(project: Project, file: PsiFile, owner: PsiModifierListOwner) { + val modifierList = owner.modifierList ?: return + val module = file.findModule() ?: return + if (!SideOnlyUtil.ensureMcdevDependencyPresent(project, module, familyName, file.resolveScope)) { + return + } + if (!FileModificationService.getInstance().preparePsiElementForWrite(owner)) return + val facade = JavaPsiFacade.getInstance(project) + val inferredSide = SideOnlyUtil.getInferredAnnotationOnly(owner, SideHardness.HARD) + ?: SideOnlyUtil.getInferredAnnotationOnly(owner, SideHardness.SOFT) ?: return + val inferred = facade.elementFactory.createAnnotationFromText( + "@${SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION}(${SideOnlyUtil.MCDEV_SIDE}.${inferredSide.side})", + owner + ) + WriteCommandAction.runWriteCommandAction(project) { + DumbService.getInstance(project).withAlternativeResolveEnabled { + JavaCodeStyleManager.getInstance(project) + .shortenClassReferences(modifierList.addAfter(inferred, null)) + } + } + } + + override fun startInWriteAction() = false +} diff --git a/src/main/kotlin/sideonly/SideOnlyInferredAnnotationProvider.kt b/src/main/kotlin/sideonly/SideOnlyInferredAnnotationProvider.kt deleted file mode 100644 index 2e525e451..000000000 --- a/src/main/kotlin/sideonly/SideOnlyInferredAnnotationProvider.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Minecraft Dev for IntelliJ - * - * https://minecraftdev.org - * - * Copyright (c) 2020 minecraft-dev - * - * MIT License - */ - -package com.demonwav.mcdev.sideonly - -import com.intellij.codeInsight.InferredAnnotationProvider -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiModifierListOwner - -class SideOnlyInferredAnnotationProvider : InferredAnnotationProvider { - - override fun findInferredAnnotation(listOwner: PsiModifierListOwner, annotationFQN: String): PsiAnnotation? { - if (annotationFQN != SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION) { - return null - } - if (SideOnlyUtil.getExplicitAnnotation(listOwner, SideHardness.EITHER) != null) { - return null - } - val inferredAnnotation = SideOnlyUtil.getExplicitOrInferredAnnotation(listOwner, SideHardness.EITHER) - ?: return null - val annotationText = - "@${SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION}(${SideOnlyUtil.MCDEV_SIDE}.${inferredAnnotation.side})" - return JavaPsiFacade.getElementFactory(listOwner.project).createAnnotationFromText(annotationText, listOwner) - } - - override fun findInferredAnnotations(listOwner: PsiModifierListOwner): MutableList { - val annotation = findInferredAnnotation(listOwner, SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION) - return if (annotation == null) { - mutableListOf() - } else { - mutableListOf(annotation) - } - } -} diff --git a/src/main/kotlin/sideonly/SideOnlyLineMarkerProvider.kt b/src/main/kotlin/sideonly/SideOnlyLineMarkerProvider.kt new file mode 100644 index 000000000..280ad93e9 --- /dev/null +++ b/src/main/kotlin/sideonly/SideOnlyLineMarkerProvider.kt @@ -0,0 +1,92 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2020 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.sideonly + +import com.demonwav.mcdev.MinecraftSettings +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.icons.AllIcons +import com.intellij.ide.actions.ApplyIntentionAction +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.actionSystem.impl.SimpleDataContext +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiIdentifier +import com.intellij.psi.PsiModifierListOwner +import com.intellij.psi.util.PsiUtilCore +import com.intellij.ui.awt.RelativePoint +import java.awt.event.MouseEvent + +class SideOnlyLineMarkerProvider : LineMarkerProvider { + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + if (!MinecraftSettings.instance.isShowSideOnlyGutterIcons) { + return null + } + if (element !is PsiIdentifier) { + return null + } + val listOwner = element.parent as? PsiModifierListOwner ?: return null + val implicitHard = SideOnlyUtil.getInferredAnnotationOnly(listOwner, SideHardness.HARD) + val implicitSoft = SideOnlyUtil.getInferredAnnotationOnly(listOwner, SideHardness.SOFT) + val implicitAnnotation = implicitHard ?: implicitSoft ?: return null + + var message = "Implicit " + message += if (implicitHard == null) { + "soft" + } else { + "hard" + } + message += "-sided annotation available: " + implicitAnnotation.reason + return LineMarkerInfo( + element, + element.textRange, + AllIcons.Gutter.ExtAnnotation, + { message }, + this::navigate, + GutterIconRenderer.Alignment.RIGHT + ) + } + + private fun navigate(event: MouseEvent, element: PsiElement) { + val listOwner = element.parent + val containingFile = listOwner.containingFile + val virtualFile = PsiUtilCore.getVirtualFile(listOwner) + + if (virtualFile != null && containingFile != null) { + val project = listOwner.project + val editor = FileEditorManager.getInstance(project).selectedTextEditor + if (editor != null) { + editor.caretModel.moveToOffset(element.textOffset) + val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) + if (file != null && virtualFile == file.virtualFile) { + val popup = createActionGroupPopup(containingFile, project, editor) + popup?.show(RelativePoint(event)) + } + } + } + } + + private fun createActionGroupPopup(file: PsiFile, project: Project, editor: Editor): JBPopup? { + val intention = MakeInferredMcdevAnnotationExplicit() + val action = ApplyIntentionAction(intention, intention.text, editor, file) + val group = DefaultActionGroup(action) + val context = SimpleDataContext.getProjectContext(null) + return JBPopupFactory.getInstance() + .createActionGroupPopup(null, group, context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true) + } +} diff --git a/src/main/kotlin/sideonly/SideOnlyUtil.kt b/src/main/kotlin/sideonly/SideOnlyUtil.kt index 97fe59628..2453bbe90 100644 --- a/src/main/kotlin/sideonly/SideOnlyUtil.kt +++ b/src/main/kotlin/sideonly/SideOnlyUtil.kt @@ -16,6 +16,8 @@ import com.demonwav.mcdev.platform.fabric.util.FabricConstants import com.demonwav.mcdev.platform.forge.util.ForgeConstants import com.demonwav.mcdev.platform.mixin.util.isMixin import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.addGradleDependency import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.packageName import com.intellij.codeInspection.ProblemsHolder @@ -23,6 +25,9 @@ import com.intellij.codeInspection.dataFlow.StandardDataFlowRunner import com.intellij.json.psi.JsonFile import com.intellij.json.psi.JsonObject import com.intellij.json.psi.JsonStringLiteral +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiArrayType @@ -43,13 +48,63 @@ import com.intellij.psi.PsiPackage import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiResolveHelper import com.intellij.psi.PsiType +import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.parentOfType object SideOnlyUtil { const val MCDEV_SIDEONLY_ANNOTATION = "com.demonwav.mcdev.annotations.CheckEnv" const val MCDEV_SIDE = "com.demonwav.mcdev.annotations.Env" - internal fun getAnnotationSide(annotation: PsiAnnotation, hardness: SideHardness): Side { + fun ensureMcdevDependencyPresent( + project: Project, + module: Module, + title: String, + scope: GlobalSearchScope + ): Boolean { + if (JavaPsiFacade.getInstance(project).findClass(MCDEV_SIDEONLY_ANNOTATION, scope) != null) { + return true + } + + if (addMcdevAnnotationsDependency(project, module, title)) { + return true + } + + return false + } + + private fun addMcdevAnnotationsDependency(project: Project, module: Module, title: String): Boolean { + val message = "MinecraftDev annotations library is missing. " + + "Without the library, MinecraftDev cannot run the analysis.\n" + + "Would you like to add it to the project buildscript automatically?" + if (Messages.showOkCancelDialog( + project, + message, + title, + Messages.OK_BUTTON, + Messages.CANCEL_BUTTON, + Messages.getErrorIcon() + ) == Messages.OK + ) { + // TODO: fetch the latest version + if (addGradleDependency( + project, + module, + "com.demonwav.mcdev", + "annotations", + SemanticVersion.release(1, 0) + ) + ) { + return true + } else { + val errorMessage = "Failed to add MinecraftDev annotations library automatically. " + + "Please add it to your buildscript manually." + Messages.showMessageDialog(project, errorMessage, title, Messages.getErrorIcon()) + } + } + return false + } + + fun getAnnotationSide(annotation: PsiAnnotation, hardness: SideHardness): Side { var isSideAnnotation = false if (hardness != SideHardness.HARD) { @@ -83,7 +138,7 @@ object SideOnlyUtil { } } - internal fun getExplicitAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { + fun getExplicitAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { return element.annotations.asSequence() .map { it to getAnnotationSide(it, hardness) } .firstOrNull { it.second != Side.BOTH } @@ -98,12 +153,7 @@ object SideOnlyUtil { } } - internal fun getExplicitOrInferredAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { - val explicitAnnotation = getExplicitAnnotation(element, hardness) - if (explicitAnnotation != null) { - return explicitAnnotation - } - + private fun getInferredAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { return when (element) { is PsiClass -> getInferredClassAnnotation(element, hardness) is PsiMethod -> getInferredMethodAnnotation(element, hardness) @@ -111,6 +161,15 @@ object SideOnlyUtil { } } + fun getExplicitOrInferredAnnotation(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { + return getExplicitAnnotation(element, hardness) ?: getInferredAnnotation(element, hardness) + } + + fun getInferredAnnotationOnly(element: PsiModifierListOwner, hardness: SideHardness): SideInstance? { + if (getExplicitAnnotation(element, hardness) != null) return null + return getInferredAnnotation(element, hardness) + } + private fun getInferredClassAnnotation(cls: PsiClass, hardness: SideHardness): SideInstance? { // If mixing into a client class, the mixin is also clientside if (cls.isMixin) { diff --git a/src/main/kotlin/util/gradle-util.kt b/src/main/kotlin/util/gradle-util.kt index a91c6e0b1..21faae3c5 100644 --- a/src/main/kotlin/util/gradle-util.kt +++ b/src/main/kotlin/util/gradle-util.kt @@ -11,16 +11,35 @@ package com.demonwav.mcdev.util import com.intellij.execution.executors.DefaultRunExecutor +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode import com.intellij.openapi.externalSystem.task.TaskCallback import com.intellij.openapi.externalSystem.util.ExternalSystemUtil +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project +import com.intellij.openapi.project.rootManager +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.psi.PsiManager +import com.intellij.psi.codeStyle.CodeStyleManager import java.nio.file.Path import java.util.concurrent.locks.Condition import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock +import org.jetbrains.kotlin.psi.KtCallElement +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtLambdaExpression +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtScriptInitializer +import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.plugins.gradle.util.GradleConstants +import org.jetbrains.plugins.groovy.lang.psi.GroovyFile +import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression +import org.jetbrains.plugins.groovy.lang.psi.util.childrenOfType inline fun runGradleTask( project: Project, @@ -51,6 +70,81 @@ inline fun runGradleTask( } } +fun addGradleDependency( + project: Project, + module: Module, + group: String, + artifact: String, + version: SemanticVersion +): Boolean { + val buildGradleFile = module.rootManager.contentRoots.mapFirstNotNull { moduleFile -> + moduleFile.findChild("build.gradle") ?: moduleFile.findChild("build.gradle.kts") + } ?: ProjectRootManager.getInstance(project).contentRoots.mapFirstNotNull { projectFile -> + projectFile.findChild("build.gradle") ?: projectFile.findChild("build.gradle.kts") + } ?: return false + val psiFile = PsiManager.getInstance(project).findFile(buildGradleFile) ?: return false + val success = when (psiFile) { + is GroovyFile -> addGroovyGradleDependency(project, psiFile, group, artifact, version) + is KtFile -> addKotlinGradleDependency(project, psiFile, group, artifact, version) + else -> false + } + if (success) { + FileDocumentManager.getInstance().saveAllDocuments() + ExternalSystemUtil.refreshProjects( + ImportSpecBuilder(project, GradleConstants.SYSTEM_ID).use(ProgressExecutionMode.MODAL_SYNC) + ) + } + return success +} + +private fun addGroovyGradleDependency( + project: Project, + file: GroovyFile, + group: String, + artifact: String, + version: SemanticVersion +): Boolean { + val dependenciesCall = file.childrenOfType().firstOrNull { methodCall -> + (methodCall.invokedExpression as? GrReferenceExpression)?.referenceName == "dependencies" + } ?: return false + val dependenciesClosure = dependenciesCall.closureArguments.firstOrNull() ?: return false + val toInsert = GroovyPsiElementFactory.getInstance(project).createStatementFromText( + "implementation '$group:$artifact:$version'", + dependenciesClosure + ) + WriteCommandAction.runWriteCommandAction(project) { + dependenciesClosure.addStatementBefore(toInsert, null) + CodeStyleManager.getInstance(project).reformat(dependenciesClosure) + } + return true +} + +private fun addKotlinGradleDependency( + project: Project, + file: KtFile, + group: String, + artifact: String, + version: SemanticVersion +): Boolean { + val scriptBlock = file.script?.blockExpression ?: return false + val dependenciesCall = scriptBlock.statements.asSequence() + .mapNotNull { it as? KtScriptInitializer } + .mapNotNull { it.body as? KtCallElement } + .firstOrNull { (it.calleeExpression as? KtSimpleNameExpression)?.getReferencedName() == "dependencies" } + ?: return false + val dependenciesLambda = dependenciesCall.lambdaArguments.firstOrNull()?.getArgumentExpression() + as? KtLambdaExpression ?: return false + val dependenciesBlock = dependenciesLambda.functionLiteral.bodyBlockExpression ?: return false + val factory = KtPsiFactory(project) + val toInsert = factory.createExpression("implementation(\"$group:$artifact:$version\")") + WriteCommandAction.runWriteCommandAction(project) { + dependenciesBlock.addBefore(factory.createNewLine(), dependenciesBlock.rBrace) + dependenciesBlock.addBefore(toInsert, dependenciesBlock.rBrace) + CodeStyleManager.getInstance(project).reformat(dependenciesBlock) + } + return true +} + class GradleCallback(private val lock: ReentrantLock, private val condition: Condition) : TaskCallback { override fun onSuccess() = resume() diff --git a/src/main/kotlin/util/utils.kt b/src/main/kotlin/util/utils.kt index 95a12cb52..9a62117fa 100644 --- a/src/main/kotlin/util/utils.kt +++ b/src/main/kotlin/util/utils.kt @@ -20,6 +20,8 @@ import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager @@ -67,6 +69,29 @@ inline fun Project.runWriteTaskInSmartMode(crossinline func: () -> T) return ref.get() } +fun Project.runInSmartModeFromReadAction( + progressIndicator: ProgressIndicator? = null, + func: () -> Unit +) { + val dumbService = DumbService.getInstance(this) + if (dumbService.isDumb) { + ApplicationManager.getApplication().executeOnPooledThread { + ProgressManager.getInstance().runProcess( + { + ProgressManager.progress("Indexing") + dumbService.runReadActionInSmartMode { + ProgressManager.checkCanceled() + invokeLater(func) + } + }, + progressIndicator + ) + } + } else { + func() + } +} + fun invokeAndWait(func: () -> T): T { val ref = Ref() ApplicationManager.getApplication().invokeAndWait({ ref.set(func()) }, ModalityState.defaultModalityState()) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 48a653590..4a6756ef5 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -14,6 +14,7 @@ org.jetbrains.idea.maven com.intellij.gradle org.intellij.groovy + org.jetbrains.kotlin com.demonwav.minecraft-dev Minecraft Development @@ -57,10 +58,18 @@ + + + com.demonwav.mcdev.sideonly.MakeInferredMcdevAnnotationExplicit + Minecraft + MakeInferredMcdevAnnotationExplicit + + + @@ -83,9 +92,6 @@ parentId="Settings.Minecraft" instance="com.demonwav.mcdev.translations.sorting.TranslationTemplateConfigurable"/> - - - diff --git a/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/after.java.template b/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/after.java.template new file mode 100644 index 000000000..0a063aeba --- /dev/null +++ b/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/after.java.template @@ -0,0 +1,7 @@ +import com.demonwav.mcdev.annotations.CheckEnv; +import com.demonwav.mcdev.annotations.Env; + +@CheckEnv(Env.CLIENT) +class A extends ClientOnlyClass { + // stuff +} diff --git a/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/before.java.template b/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/before.java.template new file mode 100644 index 000000000..df77c4d46 --- /dev/null +++ b/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/before.java.template @@ -0,0 +1,3 @@ +class A extends ClientOnlyClass { + // stuff +} diff --git a/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/description.html b/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/description.html new file mode 100644 index 000000000..e8393688c --- /dev/null +++ b/src/main/resources/intentionDescriptions/MakeInferredMcdevAnnotationExplicit/description.html @@ -0,0 +1,15 @@ + + + + +This intention adds annotations inferred by MinecraftDev (@CheckEnv) to the code explicitly. + +