Skip to content

Commit

Permalink
Add unnecessary mutable reference to captured local inspection.
Browse files Browse the repository at this point in the history
Co-authored-by: LlamaLad7 <[email protected]>
  • Loading branch information
Earthcomputer and LlamaLad7 committed Nov 28, 2023
1 parent 3a6b32f commit e6ddd88
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2023 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.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.PsiClassType
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.PsiType
import com.intellij.psi.search.searches.OverridingMethodsSearch
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.psi.util.PsiUtil
import com.intellij.psi.util.parentOfType
import com.siyeh.ig.psiutils.MethodCallUtils
import org.jetbrains.plugins.groovy.intentions.style.inference.resolve

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 ->
ann.qualifiedName?.let { MixinAnnotationHandler.forMixinAnnotation(it, project) }
// Mutable Local references do have different semantics inside a WrapOperation.
?.let { it is InjectorAnnotationHandler && it !is WrapOperationHandler } == true
}
if (hasValidMixinAnnotation) {

This comment has been minimized.

Copy link
@LlamaLad7

LlamaLad7 Nov 28, 2023

Author Contributor

Missing !?

This comment has been minimized.

Copy link
@Earthcomputer

Earthcomputer Nov 28, 2023

Author Member

I extracted this into a variable to appease ktlint and didn't test it 😆

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.resolve()
if (paramType?.qualifiedName?.startsWith(MixinConstants.MixinExtras.LOCAL_REF_PACKAGE) != true) {
continue
}

checkParameter(holder, method, param, i, paramType)
}
}
}

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
for (ref in ReferencesSearch.search(param)) {
if (isDelegationToSuper(ref.element, paramIndex)) {
continue
}
val parent = PsiUtil.skipParenthesizedExprUp(ref.element.parent) as? PsiReferenceExpression ?: return
if (parent.references.any { it.isReferenceTo(getMethod) }) {
hasAnyGets = true
} else {
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 = if (ref is PsiReferenceExpression) {
ref
} else {
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 paramType = param.type as? PsiClassType ?: return
val innerType = paramType.innerRefType ?: return
val factory = PsiElementFactory.getInstance(method.project)
param.typeElement?.replace(factory.createTypeElement(innerType))
for (ref in ReferencesSearch.search(param)) {
val refExpression = PsiUtil.skipParenthesizedExprUp(ref.element.parent) as? PsiReferenceExpression
?: continue
val call = refExpression.parent as? PsiMethodCallExpression ?: continue
call.replace(ref.element)
}
}

private val PsiClassType.innerRefType: PsiType?
get() =
when (resolve()?.qualifiedName?.substringAfterLast('.')) {
"LocalBooleanRef" -> PsiType.BOOLEAN
"LocalCharRef" -> PsiType.CHAR
"LocalDoubleRef" -> PsiType.DOUBLE
"LocalFloatRef" -> PsiType.FLOAT
"LocalIntRef" -> PsiType.INT
"LocalLongRef" -> PsiType.LONG
"LocalShortRef" -> PsiType.SHORT
"LocalRef" -> parameters.getOrNull(0)
else -> null
}
}
}
2 changes: 2 additions & 0 deletions src/main/kotlin/platform/mixin/util/MixinConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,7 @@ object MixinConstants {
object MixinExtras {
const val OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.Operation"
const val WRAP_OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation"
const val LOCAL = "com.llamalad7.mixinextras.sugar.Local"
const val LOCAL_REF_PACKAGE = "com.llamalad7.mixinextras.sugar.ref."
}
}
8 changes: 8 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,14 @@
level="ERROR"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.WrongOperationParametersInspection"/>
<localInspection displayName="Unnecessary mutable reference to captured local"
shortName="MixinExtrasUnnecessaryMutableLocal"
groupName="Mixin"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.UnnecessaryMutableLocalInspection"/>
<!--endregion-->

<!--region Configuration -->
Expand Down

0 comments on commit e6ddd88

Please sign in to comment.