User: joe Date: 23 Jul 24 22:13 Revision: e0844832bc81a61274f4959ffaeaf7de72c7e96c Summary: Inspections for when @Inject local capture is unused and for when they can be replaced with @Local TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=9504&personal=false Index: src/main/kotlin/platform/mixin/inspection/injector/UnusedLocalCaptureInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/injector/UnusedLocalCaptureInspection.kt (revision e0844832bc81a61274f4959ffaeaf7de72c7e96c) +++ src/main/kotlin/platform/mixin/inspection/injector/UnusedLocalCaptureInspection.kt (revision e0844832bc81a61274f4959ffaeaf7de72c7e96c) @@ -0,0 +1,125 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.findContainingMethod +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.search.searches.OverridingMethodsSearch +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil + +class UnusedLocalCaptureInspection : MixinInspection() { + companion object { + fun findCallbackInfoParam(parameters: Array): Int { + return parameters.indexOfFirst { param -> + val classType = param.type as? PsiClassType ?: return@indexOfFirst false + val className = classType.className + if (className != "CallbackInfo" && className != "CallbackInfoReturnable") { + return@indexOfFirst false + } + val qualifiedName = classType.resolve()?.qualifiedName ?: return@indexOfFirst false + qualifiedName == MixinConstants.Classes.CALLBACK_INFO || + qualifiedName == MixinConstants.Classes.CALLBACK_INFO_RETURNABLE + } + } + } + + override fun getStaticDescription() = + "Reports when an @Inject local capture is unused" + + override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.INJECT)) { + return + } + + // check that we are capturing locals + val localsValue = + PsiUtil.skipParenthesizedExprDown( + annotation.findDeclaredAttributeValue("locals") as? PsiExpression + ) as? PsiReferenceExpression ?: return + if (localsValue.referenceName == "NO_CAPTURE") { + return + } + val enumName = (localsValue.resolve() as? PsiEnumConstant)?.containingClass?.qualifiedName + if (enumName != MixinConstants.Classes.LOCAL_CAPTURE) { + return + } + + val method = annotation.findContainingMethod() ?: return + + if (OverridingMethodsSearch.search(method).any()) { + return + } + + // find the start of the locals in the parameter list + val parameters = method.parameterList.parameters + val callbackInfoIndex = findCallbackInfoParam(parameters) + if (callbackInfoIndex == -1) { + return + } + + val hasAnyUsedLocals = parameters.asSequence().drop(callbackInfoIndex + 1).any { param -> + ReferencesSearch.search(param).anyMatch { + !it.isSoft && !PsiTreeUtil.isAncestor(param, it.element, false) + } + } + if (!hasAnyUsedLocals) { + holder.registerProblem( + localsValue, + "Unused @Inject local capture", + RemoveLocalCaptureFix(annotation, callbackInfoIndex) + ) + } + } + } + + private class RemoveLocalCaptureFix( + injectAnnotation: PsiAnnotation, + private val callbackInfoIndex: Int + ) : LocalQuickFixOnPsiElement(injectAnnotation) { + override fun getFamilyName() = "Remove @Inject local capture" + + override fun getText() = "Remove @Inject local capture" + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val injectAnnotation = startElement as? PsiAnnotation ?: return + val method = injectAnnotation.findContainingMethod() ?: return + method.parameterList.parameters.asSequence().drop(callbackInfoIndex + 1).forEach(PsiElement::delete) + AnnotationAttributeFix(injectAnnotation, "locals" to null).applyFix() + } + } +} Index: src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt (revision e0844832bc81a61274f4959ffaeaf7de72c7e96c) +++ src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt (revision e0844832bc81a61274f4959ffaeaf7de72c7e96c) @@ -0,0 +1,205 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.mixinextras + +import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.inspection.injector.UnusedLocalCaptureInspection +import com.demonwav.mcdev.platform.mixin.util.LocalVariables +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.hasAccess +import com.demonwav.mcdev.util.addAnnotation +import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.findContainingMethod +import com.demonwav.mcdev.util.findModule +import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.searches.OverridingMethodsSearch +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type + +class InjectLocalCaptureReplaceWithLocalInspection : MixinInspection() { + override fun getStaticDescription() = + "Reports when @Inject local capture can be replaced with @Local, which is less brittle" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor { + val localClass = JavaPsiFacade.getInstance(holder.project) + .findClass(MixinConstants.MixinExtras.LOCAL, GlobalSearchScope.allScope(holder.project)) + if (localClass == null) { + return PsiElementVisitor.EMPTY_VISITOR + } + + return object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.INJECT)) { + return + } + + // check that we are capturing locals + val localsValue = + PsiUtil.skipParenthesizedExprDown( + annotation.findDeclaredAttributeValue("locals") as? PsiExpression + ) as? PsiReferenceExpression ?: return + if (localsValue.referenceName != "CAPTURE_FAILHARD") { + return + } + val enumName = (localsValue.resolve() as? PsiEnumConstant)?.containingClass?.qualifiedName + if (enumName != MixinConstants.Classes.LOCAL_CAPTURE) { + return + } + + val method = annotation.findContainingMethod() ?: return + + if (OverridingMethodsSearch.search(method).any()) { + return + } + + // find the start of the locals in the parameter list + val parameters = method.parameterList.parameters + val callbackInfoIndex = UnusedLocalCaptureInspection.findCallbackInfoParam(parameters) + if (callbackInfoIndex == -1) { + return + } + + // resolve the local variables at the targets + val handler = MixinAnnotationHandler.forMixinAnnotation(MixinConstants.Annotations.INJECT) + as InjectorAnnotationHandler + val module = annotation.findModule() ?: return + val localsAndParamCountsAtTargets = handler.resolveInstructions(annotation).map { result -> + val locals = LocalVariables.getLocals( + module, + result.method.clazz, + result.method.method, + result.result.insn + ) ?: return + var paramCount = Type.getMethodType(result.method.method.desc).argumentTypes.size + if (!result.method.method.hasAccess(Opcodes.ACC_STATIC)) { + paramCount++ + } + locals to paramCount + } + + // based on the resolved local variables, figure out what @Local specifiers to use + val localSpecifiers = parameters.drop(callbackInfoIndex + 1).withIndex().map { (index, param) -> + val isLocalUsed = ReferencesSearch.search(param).anyMatch { + !it.isSoft && !PsiTreeUtil.isAncestor(param, it.element, false) + } + if (!isLocalUsed) { + return@map UnusedSpecifier + } + + val localType = param.type.descriptor + val canBeImplicit = localsAndParamCountsAtTargets.all { (localsAtTarget, _) -> + localsAtTarget.singleOrNull { it?.desc == localType } != null + } + if (canBeImplicit) { + return@map ImplicitSpecifier + } + + val ordinals = localsAndParamCountsAtTargets.map { (localsAtTarget, paramCount) -> + localsAtTarget.filterNotNull().take(index + paramCount).count { it.desc == localType } + } + if (ordinals.isEmpty()) { + return + } + if (ordinals.all { it == ordinals.first() }) { + return@map OrdinalSpecifier(ordinals.first()) + } + + val indexes = localsAndParamCountsAtTargets.map { (localsAtTarget, paramCount) -> + localsAtTarget.filterNotNull().getOrNull(index + paramCount)?.index ?: return + } + if (indexes.isEmpty()) { + return + } + if (indexes.all { it == indexes.first() }) { + return@map IndexSpecifier(indexes.first()) + } + + return + } + + if (localSpecifiers.isEmpty() || localSpecifiers.all { it is UnusedSpecifier }) { + // this is reported by the redundant local capture inspection + return + } + + holder.registerProblem( + localsValue, + "@Inject local capture can be replaced by @Local", + ReplaceWithLocalFix(annotation, callbackInfoIndex, localSpecifiers) + ) + } + } + } + + private sealed interface LocalSpecifier + private data class OrdinalSpecifier(val ordinal: Int) : LocalSpecifier + private data class IndexSpecifier(val index: Int) : LocalSpecifier + private data object ImplicitSpecifier : LocalSpecifier + private data object UnusedSpecifier : LocalSpecifier + + private class ReplaceWithLocalFix( + injectAnnotation: PsiAnnotation, + private val callbackInfoIndex: Int, + @SafeFieldForPreview private val localSpecifiers: List + ) : LocalQuickFixOnPsiElement(injectAnnotation) { + override fun getFamilyName() = "Replace @Inject local capture with @Local" + + override fun getText() = "Replace @Inject local capture with @Local" + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val injectAnnotation = startElement as? PsiAnnotation ?: return + val method = injectAnnotation.findContainingMethod() ?: return + val paramsForLocals = method.parameterList.parameters.asSequence().drop(callbackInfoIndex + 1) + for ((param, specifier) in paramsForLocals.zip(localSpecifiers.asSequence())) { + val localAnnotationText = when (specifier) { + ImplicitSpecifier -> "@${MixinConstants.MixinExtras.LOCAL}" + is IndexSpecifier -> "@${MixinConstants.MixinExtras.LOCAL}(index = ${specifier.index})" + is OrdinalSpecifier -> "@${MixinConstants.MixinExtras.LOCAL}(ordinal = ${specifier.ordinal})" + is UnusedSpecifier -> { + param.delete() + continue + } + } + param.addAnnotation(localAnnotationText) + } + AnnotationAttributeFix(injectAnnotation, "locals" to null).applyFix() + } + } +} Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 3f58cd903d19a29b4a7100f9fd294f5f00fdd6b8) +++ src/main/resources/META-INF/plugin.xml (revision e0844832bc81a61274f4959ffaeaf7de72c7e96c) @@ -1042,6 +1042,22 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.CaptureFailExceptionInspection"/> + +