Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: quick fix for MethodHandles#constant with zero value #128

Merged
merged 1 commit into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
package de.sirywell.handlehints.mhtype

import com.intellij.codeInspection.dataFlow.types.DfConstantType
import com.intellij.psi.*
import com.intellij.psi.util.parentOfType
import de.sirywell.handlehints.*
import de.sirywell.handlehints.MethodHandleBundle.message
import de.sirywell.handlehints.dfa.SsaAnalyzer
import de.sirywell.handlehints.dfa.SsaConstruction
import de.sirywell.handlehints.inspection.ProblemEmitter
import de.sirywell.handlehints.type.*
import de.sirywell.intellij.ReplaceMethodCallFix

private const val VAR_HANDLE_FQN = "java.lang.invoke.VarHandle"
private val ZERO_VALUES = setOf(
null,
0.toByte(),
0.toShort(),
0.toChar(),
0,
0L,
false
)


/**
* Contains methods from [java.lang.invoke.MethodHandles] that create
* new [java.lang.invoke.MethodHandle]s.
*/
class MethodHandlesInitializer(private val ssaAnalyzer: SsaAnalyzer) : ProblemEmitter(ssaAnalyzer.typeData) {

private val topType = TopMethodHandleType

fun arrayConstructor(arrayClass: PsiExpression): MethodHandleType {
Expand Down Expand Up @@ -57,13 +69,23 @@ class MethodHandlesInitializer(private val ssaAnalyzer: SsaAnalyzer) : ProblemEm

fun constant(typeExpr: PsiExpression, valueExpr: PsiExpression): MethodHandleType {
val type = typeExpr.asType()
val valueType = valueExpr.type?.let { ExactType(it) } ?: BotType
val valueType = valueExpr.type?.let { ExactType(it) } ?: TopType
if (type == ExactType.voidType) {
return emitProblem(typeExpr, message("problem.creation.arguments.invalid.type", ExactType.voidType))
}
if (!typesAreCompatible(type, valueType, valueExpr)) {
return emitProblem(valueExpr, message("problem.general.parameters.expected.type", type, valueType))
}
if (type is ExactType) {
val dfType = valueExpr.getDfType()
if (dfType is DfConstantType<*> && ZERO_VALUES.contains(dfType.value)) {
emitRedundant(
typeExpr.parentOfType<PsiMethodCallExpression>()!!,
message("problem.creation.constant.zero"),
ReplaceMethodCallFix("zero") { it.take(1) }
)
}
}
return complete(type, listOf())
}

Expand Down Expand Up @@ -136,8 +158,8 @@ class MethodHandlesInitializer(private val ssaAnalyzer: SsaAnalyzer) : ProblemEm
}
val varHandleType = PsiType.getTypeByName(VAR_HANDLE_FQN, methodTypeExpr.project, methodTypeExpr.resolveScope)
return type.withParameterTypes(
type.parameterTypes.addAllAt(0, CompleteTypeList(listOf(ExactType(varHandleType))))
)
type.parameterTypes.addAllAt(0, CompleteTypeList(listOf(ExactType(varHandleType))))
)
}

private fun checkAccessModeType(
Expand Down
8 changes: 6 additions & 2 deletions src/main/kotlin/de/sirywell/handlehints/psiSupport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.intellij.codeInspection.dataFlow.CommonDataflow
import com.intellij.codeInspection.dataFlow.CommonDataflow.DataflowResult
import com.intellij.codeInspection.dataFlow.types.DfType
import com.intellij.psi.*
import com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil
import com.intellij.psi.search.GlobalSearchScope
Expand All @@ -14,7 +15,7 @@
val PsiMethodCallExpression.methodName
get() = this.methodExpression.referenceName

fun isJavaLangInvoke(element: PsiMethodCallExpression) =

Check warning on line 18 in src/main/kotlin/de/sirywell/handlehints/psiSupport.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Function "isJavaLangInvoke" is never used
(element.resolveMethod()?.containingClass?.containingFile as? PsiJavaFile)?.packageName == "java.lang.invoke"

fun receiverIsInvokeClass(element: PsiMethodCallExpression, className: String) =
Expand Down Expand Up @@ -122,11 +123,14 @@
}
}

@Suppress("UnstableApiUsage")
inline fun <reified T> PsiExpression.getConstantOfType(): T? {
return getDfType()?.getConstantOfType(T::class.java)
}

@Suppress("UnstableApiUsage")
fun PsiExpression.getDfType(): DfType? {
return (getDataflowResult(this) as DataflowResult?)
?.getDfType(this)
?.getConstantOfType(T::class.java)
}

fun PsiExpression.getConstantLong(): Long? {
Expand Down
36 changes: 24 additions & 12 deletions src/main/kotlin/de/sirywell/intellij/ReplaceMethodCallFix.kt
Original file line number Diff line number Diff line change
Expand Up @@ -182,32 +182,44 @@
// Modifications:
// - move to own package
// - Suppress warnings
// - adaption to work with PsiMethodCallExpression elements
// - adaption to allow adjusting argument list

// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:Suppress("UnstableApiUsage", "MemberVisibilityCanBePrivate")

package de.sirywell.intellij

import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview
import com.intellij.codeInspection.CommonQuickFixBundle
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiMethodCallExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.generate.getUastElementFactory
import org.jetbrains.uast.generate.replace
import org.jetbrains.uast.getQualifiedParentOrThis
import org.jetbrains.uast.getUastParentOfType

class ReplaceMethodCallFix(val methodName: String) : LocalQuickFix {
override fun getFamilyName(): String = CommonQuickFixBundle.message("fix.replace.with.x", "$methodName()")

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val uCall = descriptor.psiElement.getUastParentOfType<UCallExpression>() ?: return
val uFactory = uCall.getUastElementFactory(project) ?: return
val newCall = uFactory.createCallExpression(
uCall.receiver, methodName, uCall.valueArguments, null, uCall.kind, null
) ?: return
val oldCall = uCall.getQualifiedParentOrThis()
oldCall.replace(newCall)
}
class ReplaceMethodCallFix(
val methodName: String,
@SafeFieldForPreview private val argsFilter: (List<UExpression>) -> List<UExpression> = { it }
) : LocalQuickFix {
override fun getFamilyName(): String = CommonQuickFixBundle.message("fix.replace.with.x", "$methodName()")

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val uCall = if (descriptor.psiElement is PsiMethodCallExpression) {
descriptor.psiElement.firstChild.getUastParentOfType<UCallExpression>() ?: return
} else {
descriptor.psiElement.getUastParentOfType<UCallExpression>() ?: return
}
val uFactory = uCall.getUastElementFactory(project) ?: return
val newCall = uFactory.createCallExpression(
uCall.receiver, methodName, argsFilter(uCall.valueArguments), null, uCall.kind, null
) ?: return
val oldCall = uCall.getQualifiedParentOrThis()
oldCall.replace(newCall)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ problem.invocation.returnType.fix.introduce.variable=Insert variable
problem.invocation.returnType.wrong.cast=Method call requires an explicit cast to {0} but got a cast to {1}.
problem.invocation.arguments.wrong.types=There are arguments with wrong static types. Expected {0} but got {1}.
problem.creation.arguments.invalid.type=Parameter must not be of type {0}.
problem.creation.constant.zero=Usage of 'constant' with zero value can be replaced with 'zero'.
problem.general.parameters.expected.type=Expected parameter of type {0} but got {1}.
problem.general.parameters.noParameter=MethodHandle type does not have a parameter but requires at least one.
problem.general.position.invalidIndexKnownBoundsExcl=Position argument value {0} is out of bounds [0, {1}).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class MethodHandleInspectionsTest : TypeAnalysisTestBase() {

fun testMethodHandlesCollectArguments() = doTypeCheckingTest(true)

fun testMethodHandlesConstantWithZero() = doInspectionTest()

fun testMethodHandlesDropArguments() = doTypeCheckingTest()

fun testMethodHandlesDropReturn() = doTypeCheckingTest()
Expand Down
18 changes: 18 additions & 0 deletions src/test/testData/MethodHandlesConstantWithZero.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

class MethodHandlesConstantWithZero {
void m() {
MethodHandle mh00 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(int.class, 0)</warning>;
MethodHandle mh01 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(int.class, '\0')</warning>;
MethodHandle mh02 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(String.class, null)</warning>;
MethodHandle mh03 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(Integer.class, null)</warning>;
MethodHandle mh04 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(boolean.class, false)</warning>;
MethodHandle mh05 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(char.class, (char) ('a' - 'a'))</warning>;
MethodHandle mh06 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(long.class, 0)</warning>;
MethodHandle mh07 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(long.class, 0L)</warning>;
MethodHandle mh08 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(byte.class, (byte) 0)</warning>;
MethodHandle mh09 = <warning descr="Usage of 'constant' with zero value can be replaced with 'zero'.">MethodHandles.constant(short.class, (short) 0)</warning>;
}
}
4 changes: 2 additions & 2 deletions src/test/testData/WrongArgumentTypeInConstant.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ class WrongArgumentTypeInConstant {
private static final MethodHandle a = MethodHandles.constant(byte.class, <warning descr="Expected parameter of type byte but got int.">0</warning>);
private static final MethodHandle b = MethodHandles.constant(int.class, <warning descr="Expected parameter of type int but got String.">"Hello World"</warning>); // CCE
private static final MethodHandle c = MethodHandles.constant(byte.class, <warning descr="Expected parameter of type byte but got int.">0</warning>); // CCE
private static final MethodHandle d = MethodHandles.constant(byte.class, (byte) 0); // fine
private static final MethodHandle e = MethodHandles.constant(int.class, (byte) 0); // fine
private static final MethodHandle d = MethodHandles.constant(byte.class, (byte) 1); // fine
private static final MethodHandle e = MethodHandles.constant(int.class, (byte) 1); // fine
private static final MethodHandle f = MethodHandles.constant(CharSequence.class, "Hello"); // fine
private static final MethodHandle g = MethodHandles.constant(String.class, (CharSequence) "a"); // fine
private static final MethodHandle h = MethodHandles.constant(String.class, <warning descr="Expected parameter of type String but got Integer.">Integer.valueOf(1)</warning>); // CCE
Expand Down
Loading