←
→
edited src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt
⁠
llamalad7: Feature/method signature intersection (#2647 )
* Refactor: Rework expected MethodSignatures.
- ParameterGroups are gone, with every signature now effectively containing a required leading group and an optional vararg trailing group. The prior flexibility made working them easy to get wrong, for example in the matching logic which would have been incorrect for leading vararg groups.
- Inject previously contained 2 optional groups (the target's params and the captured locals) but this was in fact incorrect, since the captured params are required if we want to capture any locals, so it is now (better) represented as 2 separate signature options.
- The distinction between `WARN_IF_ABSENT` and `ERROR_IF_ABSENT` is entirely removed. It was effectively unused since it only applied to captured Inject locals, which are varargs and therefore never "absent" (a separate inspection handles unused LocalCapture).
- ModifyArgs is in fact all-or-nothing wrt capturing the target parameters, and this is now reflected.
* Fix: Disallow non-int int-like types in @ModifyConstant.
Not sure why these are there, Mixin doesn't allow them.
* Fix: Disallow `nullValue` in `@ModifyConstant`.
It does not work and cannot ever work.
* stash
* stash
* finish general intersection
* cleanup
* stash
* stash
* fix general intersection
* stash
* fix tests
* return type only suggestions
* prioritise existing return type in modifier suggestions
* restore BeforeConstant's behaviour of ensuring only 1 discriminator is specified, and supporting wildcard matches
* Fix: Support `.class` constants in `@ModifyConstant`.
* Switch `@ModifyConstant` to a general signature type.
* int coercion kinda fixed ish
* fix int coercion properly
* cleanup
* proper int-like logic
* cleanup
* some docs and fixes
* fix return type solving
* Cache walked supertypes and improve handling of Object as a supertype.
* Cleanup
* Cleanup
* Small fixes
* Small fixes
* Fix: Suggest "ci" for coerced callba...
* Refactor: Rework expected MethodSignatures.
- ParameterGroups are gone, with every signature now effectively containing a required leading group and an optional vararg trailing group. The prior flexibility made working them easy to get wrong, for example in the matching logic which would have been incorrect for leading vararg groups.
- Inject previously contained 2 optional groups (the target's params and the captured locals) but this was in fact incorrect, since the captured params are required if we want to capture any locals, so it is now (better) represented as 2 separate signature options.
- The distinction between `WARN_IF_ABSENT` and `ERROR_IF_ABSENT` is entirely removed. It was effectively unused since it only applied to captured Inject locals, which are varargs and therefore never "absent" (a separate inspection handles unused LocalCapture).
- ModifyArgs is in fact all-or-nothing wrt capturing the target parameters, and this is now reflected.
* Fix: Disallow non-int int-like types in @ModifyConstant.
Not sure why these are there, Mixin doesn't allow them.
* Fix: Disallow `nullValue` in `@ModifyConstant`.
It does not work and cannot ever work.
* stash
* stash
* finish general intersection
* cleanup
* stash
* stash
* fix general intersection
* stash
* fix tests
* return type only suggestions
* prioritise existing return type in modifier suggestions
* restore BeforeConstant's behaviour of ensuring only 1 discriminator is specified, and supporting wildcard matches
* Fix: Support `.class` constants in `@ModifyConstant`.
* Switch `@ModifyConstant` to a general signature type.
* int coercion kinda fixed ish
* fix int coercion properly
* cleanup
* proper int-like logic
* cleanup
* some docs and fixes
* fix return type solving
* Cache walked supertypes and improve handling of Object as a supertype.
* Cleanup
* Cleanup
* Small fixes
* Small fixes
* Fix: Suggest "ci" for coerced callba...
- /*
- * Minecraft Development for IntelliJ
- *
- * https://mcdev.io/
- *
- * Copyright (C) 2025 minecraft-dev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, version 3.0 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
- 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.handlers.mixinextras.WrapOperationHandler
- import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection
- import com.demonwav.mcdev.platform.mixin.util.MixinConstants
- import com.demonwav.mcdev.platform.mixin.util.isLocalRef
- import com.demonwav.mcdev.platform.mixin.util.unwrapLocalRef
- 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.PsiClass
- import com.intellij.psi.PsiElement
- import com.intellij.psi.PsiElementFactory
- import com.intellij.psi.PsiExpressionList
- import com.intellij.psi.PsiFile
- import com.intellij.psi.PsiMethod
- import com.intellij.psi.PsiMethodCallExpression
- 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.PsiTypesUtil
- import com.intellij.psi.util.PsiUtil
- import com.intellij.psi.util.parentOfType
- import com.intellij.util.Processor
- import com.siyeh.ig.psiutils.MethodCallUtils
-
- class UnnecessaryMutableLocalInspection : MixinInspection() {
- override fun getStaticDescription() = "Unnecessary mutable reference to captured local"
-
- override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() {
- override fun visitMethod(method: PsiMethod) {
- val project = method.project
- val hasValidMixinAnnotation = method.annotations.any { ann ->
- MixinAnnotationHandler.forMixinAnnotation(ann, project)
- // Mutable Local references do have different semantics inside a WrapOperation.
- ?.let { it is InjectorAnnotationHandler && it !is WrapOperationHandler } == true
- }
- if (!hasValidMixinAnnotation) {
- return
- }
-
- // ignore if method has any references
- val hasReferences = ReferencesSearch.search(method)
- .mapNotNull { PsiUtil.skipParenthesizedExprUp(it.element).parent as? PsiMethodCallExpression }
- .any { !MethodCallUtils.hasSuperQualifier(it) }
- if (hasReferences) {
- return
- }
-
- for ((i, param) in method.parameterList.parameters.withIndex()) {
- if (!param.hasAnnotation(MixinConstants.MixinExtras.LOCAL)) {
- continue
- }
- val paramType = param.type
- if (!param.type.isLocalRef) {
- continue
- }
-
- val paramClass = PsiTypesUtil.getPsiClass(paramType) ?: continue
- checkParameter(holder, method, param, i, paramClass)
- }
- }
- }
-
- private fun checkParameter(
- holder: ProblemsHolder,
- originalMethod: PsiMethod,
- originalParam: PsiParameter,
- paramIndex: Int,
- paramType: PsiClass
- ) {
- var hasAnyGets = false
- for (method in OverridingMethodsSearch.search(originalMethod).findAll() + listOf(originalMethod)) {
- val param = method.parameterList.getParameter(paramIndex) ?: return
- val getMethod = paramType.findMethodsByName("get", false).firstOrNull() ?: return
-
- var exitEarly = false
- ReferencesSearch.search(param).forEach(Processor { ref ->
- if (isDelegationToSuper(ref.element, paramIndex)) {
- return@Processor true
- }
- val parent = PsiUtil.skipParenthesizedExprUp(ref.element.parent) as? PsiReferenceExpression ?: run {
- exitEarly = true
- return@Processor false
- }
- if (parent.references.any { it.isReferenceTo(getMethod) }) {
- hasAnyGets = true
- } else {
- exitEarly = true
- return@Processor false
- }
-
- return@Processor true
- })
-
- if (exitEarly) {
- return
- }
- }
- if (!hasAnyGets) {
- // Don't annoy them if they've just made the parameter
- return
- }
- holder.registerProblem(
- originalParam.typeElement ?: originalParam,
- "@Local could be captured immutably",
- SwitchToImmutableCaptureFix(originalParam)
- )
- }
-
- // Ignore super delegations in subclasses. super.foo(myLocalRef) has no effect on whether the local can be converted
- private fun isDelegationToSuper(ref: PsiElement, paramIndex: Int): Boolean {
- val method = ref.findContainingMethod() ?: return false
- val superMethod = method.findSuperMethods().firstOrNull { it.containingClass?.isInterface == false }
- ?: return false
-
- // For some reason ref is sometimes the identifier rather than the reference expression. Get the reference expr
- val actualRef = ref as? PsiReferenceExpression
- ?: PsiUtil.skipParenthesizedExprUp(ref.parent) as? PsiReferenceExpression
- ?: return false
- val param = PsiUtil.skipParenthesizedExprUp(actualRef)
- val paramList = param.parent as? PsiExpressionList ?: return false
- val methodCall = paramList.parent as? PsiMethodCallExpression ?: return false
-
- // Check that the method call is a super call
- if (!MethodCallUtils.hasSuperQualifier(methodCall)) {
- return false
- }
-
- // Check that our reference is in the correct parameter index
- if (paramList.expressions.getOrNull(paramIndex) != param) {
- return false
- }
-
- // Check that the super call is referencing the correct super method.
- return methodCall.resolveMethod() == superMethod
- }
-
- private class SwitchToImmutableCaptureFix(param: PsiParameter) : LocalQuickFixOnPsiElement(param) {
- override fun getFamilyName() = "Switch to immutable capture"
- override fun getText() = "Switch to immutable capture"
-
- override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
- val param = startElement as? PsiParameter ?: return
- val method = param.parentOfType<PsiMethod>() ?: return
- val paramIndex = method.parameterList.getParameterIndex(param)
- val methods = mutableListOf(method)
- if (file.isPhysical) {
- methods.addAll(OverridingMethodsSearch.search(method))
- }
- for (impl in methods) {
- fixMethod(impl, paramIndex)
- }
- }
-
- private fun fixMethod(method: PsiMethod, paramIndex: Int) {
- val param = method.parameterList.getParameter(paramIndex) ?: return
- val innerType = param.type.unwrapLocalRef()
- val factory = PsiElementFactory.getInstance(method.project)
- param.typeElement?.replace(factory.createTypeElement(innerType))
- ReferencesSearch.search(param).forEach(Processor { ref ->
- val refExpression = PsiUtil.skipParenthesizedExprUp(ref.element.parent) as? PsiReferenceExpression
- ?: return@Processor true
- val call = refExpression.parent as? PsiMethodCallExpression ?: return@Processor true
- call.replace(ref.element)
-
- return@Processor true
- })
- }
- }
- }
- /*
- * Minecraft Development for IntelliJ
- *
- * https://mcdev.io/
- *
- * Copyright (C) 2025 minecraft-dev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, version 3.0 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
- package com.demonwav.mcdev.platform.mixin.inspection.mixinextras
-
- import com.demonwav.mcdev.platform.mixin.handlers.InsnInjectorAnnotationHandler
- import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
- import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.WrapOperationHandler
- import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection
- import com.demonwav.mcdev.platform.mixin.util.MixinConstants
- import com.demonwav.mcdev.platform.mixin.util.isLocalRef
- import com.demonwav.mcdev.platform.mixin.util.unwrapLocalRef
- 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.PsiClass
- import com.intellij.psi.PsiElement
- import com.intellij.psi.PsiElementFactory
- import com.intellij.psi.PsiExpressionList
- import com.intellij.psi.PsiFile
- import com.intellij.psi.PsiMethod
- import com.intellij.psi.PsiMethodCallExpression
- 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.PsiTypesUtil
- import com.intellij.psi.util.PsiUtil
- import com.intellij.psi.util.parentOfType
- import com.intellij.util.Processor
- import com.siyeh.ig.psiutils.MethodCallUtils
-
- class UnnecessaryMutableLocalInspection : MixinInspection() {
- override fun getStaticDescription() = "Unnecessary mutable reference to captured local"
-
- override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() {
- override fun visitMethod(method: PsiMethod) {
- val project = method.project
- val hasValidMixinAnnotation = method.annotations.any { ann ->
- MixinAnnotationHandler.forMixinAnnotation(ann, project)
- // Mutable Local references do have different semantics inside a WrapOperation.
- ?.let { it is InsnInjectorAnnotationHandler && it !is WrapOperationHandler } == true
- }
- if (!hasValidMixinAnnotation) {
- return
- }
-
- // ignore if method has any references
- val hasReferences = ReferencesSearch.search(method)
- .mapNotNull { PsiUtil.skipParenthesizedExprUp(it.element).parent as? PsiMethodCallExpression }
- .any { !MethodCallUtils.hasSuperQualifier(it) }
- if (hasReferences) {
- return
- }
-
- for ((i, param) in method.parameterList.parameters.withIndex()) {
- if (!param.hasAnnotation(MixinConstants.MixinExtras.LOCAL)) {
- continue
- }
- val paramType = param.type
- if (!param.type.isLocalRef) {
- continue
- }
-
- val paramClass = PsiTypesUtil.getPsiClass(paramType) ?: continue
- checkParameter(holder, method, param, i, paramClass)
- }
- }
- }
-
- private fun checkParameter(
- holder: ProblemsHolder,
- originalMethod: PsiMethod,
- originalParam: PsiParameter,
- paramIndex: Int,
- paramType: PsiClass
- ) {
- var hasAnyGets = false
- for (method in OverridingMethodsSearch.search(originalMethod).findAll() + listOf(originalMethod)) {
- val param = method.parameterList.getParameter(paramIndex) ?: return
- val getMethod = paramType.findMethodsByName("get", false).firstOrNull() ?: return
-
- var exitEarly = false
- ReferencesSearch.search(param).forEach(Processor { ref ->
- if (isDelegationToSuper(ref.element, paramIndex)) {
- return@Processor true
- }
- val parent = PsiUtil.skipParenthesizedExprUp(ref.element.parent) as? PsiReferenceExpression ?: run {
- exitEarly = true
- return@Processor false
- }
- if (parent.references.any { it.isReferenceTo(getMethod) }) {
- hasAnyGets = true
- } else {
- exitEarly = true
- return@Processor false
- }
-
- return@Processor true
- })
-
- if (exitEarly) {
- return
- }
- }
- if (!hasAnyGets) {
- // Don't annoy them if they've just made the parameter
- return
- }
- holder.registerProblem(
- originalParam.typeElement ?: originalParam,
- "@Local could be captured immutably",
- SwitchToImmutableCaptureFix(originalParam)
- )
- }
-
- // Ignore super delegations in subclasses. super.foo(myLocalRef) has no effect on whether the local can be converted
- private fun isDelegationToSuper(ref: PsiElement, paramIndex: Int): Boolean {
- val method = ref.findContainingMethod() ?: return false
- val superMethod = method.findSuperMethods().firstOrNull { it.containingClass?.isInterface == false }
- ?: return false
-
- // For some reason ref is sometimes the identifier rather than the reference expression. Get the reference expr
- val actualRef = ref as? PsiReferenceExpression
- ?: PsiUtil.skipParenthesizedExprUp(ref.parent) as? PsiReferenceExpression
- ?: return false
- val param = PsiUtil.skipParenthesizedExprUp(actualRef)
- val paramList = param.parent as? PsiExpressionList ?: return false
- val methodCall = paramList.parent as? PsiMethodCallExpression ?: return false
-
- // Check that the method call is a super call
- if (!MethodCallUtils.hasSuperQualifier(methodCall)) {
- return false
- }
-
- // Check that our reference is in the correct parameter index
- if (paramList.expressions.getOrNull(paramIndex) != param) {
- return false
- }
-
- // Check that the super call is referencing the correct super method.
- return methodCall.resolveMethod() == superMethod
- }
-
- private class SwitchToImmutableCaptureFix(param: PsiParameter) : LocalQuickFixOnPsiElement(param) {
- override fun getFamilyName() = "Switch to immutable capture"
- override fun getText() = "Switch to immutable capture"
-
- override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
- val param = startElement as? PsiParameter ?: return
- val method = param.parentOfType<PsiMethod>() ?: return
- val paramIndex = method.parameterList.getParameterIndex(param)
- val methods = mutableListOf(method)
- if (file.isPhysical) {
- methods.addAll(OverridingMethodsSearch.search(method))
- }
- for (impl in methods) {
- fixMethod(impl, paramIndex)
- }
- }
-
- private fun fixMethod(method: PsiMethod, paramIndex: Int) {
- val param = method.parameterList.getParameter(paramIndex) ?: return
- val innerType = param.type.unwrapLocalRef()
- val factory = PsiElementFactory.getInstance(method.project)
- param.typeElement?.replace(factory.createTypeElement(innerType))
- ReferencesSearch.search(param).forEach(Processor { ref ->
- val refExpression = PsiUtil.skipParenthesizedExprUp(ref.element.parent) as? PsiReferenceExpression
- ?: return@Processor true
- val call = refExpression.parent as? PsiMethodCallExpression ?: return@Processor true
- call.replace(ref.element)
-
- return@Processor true
- })
- }
- }
- }