[java-refactoring] TypeMigration: migration rules for optional

GitOrigin-RevId: 5f7c1413476c7aea1b6282e86426e7ccf414c94b
This commit is contained in:
Tagir Valeev
2024-02-01 13:28:41 +01:00
committed by intellij-monorepo-bot
parent 4e8623cb77
commit 86d9d3a43e
10 changed files with 258 additions and 14 deletions

View File

@@ -4,6 +4,7 @@
implementation="com.intellij.refactoring.typeMigration.TypeMigrationVariableTypeFixProvider"/>
<conversion.rule implementation="com.intellij.refactoring.typeMigration.rules.ListArrayConversionRule"/>
<conversion.rule implementation="com.intellij.refactoring.typeMigration.rules.AtomicConversionRule"/>
<conversion.rule implementation="com.intellij.refactoring.typeMigration.rules.OptionalConversionRule"/>
<conversion.rule implementation="com.intellij.refactoring.typeMigration.rules.BoxingTypeConversionRule"/>
<conversion.rule implementation="com.intellij.refactoring.typeMigration.rules.ElementToArrayConversionRule"/>
<conversion.rule implementation="com.intellij.refactoring.typeMigration.rules.ThreadLocalConversionRule"/>

View File

@@ -0,0 +1,127 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.refactoring.typeMigration.rules;
import com.intellij.codeInsight.Nullability;
import com.intellij.codeInspection.dataFlow.NullabilityUtil;
import com.intellij.codeInspection.util.OptionalUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.typeMigration.TypeConversionDescriptor;
import com.intellij.refactoring.typeMigration.TypeConversionDescriptorBase;
import com.intellij.refactoring.typeMigration.TypeMigrationLabeler;
import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.TypeUtils;
import com.siyeh.ig.psiutils.VariableNameGenerator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
public final class OptionalConversionRule extends TypeConversionRule {
private static final Map<String, String> GET_METHODS = Map.of(
CommonClassNames.JAVA_UTIL_OPTIONAL, "orElse(null)",
"java.util.OptionalInt", "getAsInt()",
"java.util.OptionalLong", "getAsLong()",
"java.util.OptionalDouble", "getAsDouble()");
@Override
public @Nullable TypeConversionDescriptorBase findConversion(PsiType from,
PsiType to,
PsiMember member,
PsiExpression context,
TypeMigrationLabeler labeler) {
PsiType optionalElementType = OptionalUtil.getOptionalElementType(to);
if (optionalElementType == null) return null;
if (!from.equals(optionalElementType)) {
return null;
}
PsiClass optionalClass = PsiUtil.resolveClassInClassTypeOnly(to);
if (optionalClass == null) return null;
String qualifiedName = optionalClass.getQualifiedName();
if (qualifiedName == null) return null;
PsiElement parent = context.getParent();
TypeMigrationUsageInfo root = labeler.getCurrentRoot();
PsiElement element = root == null ? null : root.getElement();
if (element != null && context instanceof PsiReferenceExpression ref) {
PsiExpression qualifier = ref.getQualifierExpression();
if (qualifier instanceof PsiReferenceExpression qualifierRef && qualifierRef.isReferenceTo(element)) {
return new TypeConversionDescriptor("$val$", "$val$.get()", qualifier);
}
}
if (element != null && context instanceof PsiReferenceExpression ref && ref.isReferenceTo(element)) {
if (parent instanceof PsiBinaryExpression binOp) {
IElementType tokenType = binOp.getOperationTokenType();
if (tokenType.equals(JavaTokenType.EQEQ) || tokenType.equals(JavaTokenType.NE)) {
PsiExpression lOp = binOp.getLOperand();
PsiExpression rOp = binOp.getROperand();
if (rOp != null) {
if (ExpressionUtils.isNullLiteral(rOp)) {
return new TypeConversionDescriptor("$val$" + binOp.getOperationSign().getText() + rOp.getText(),
tokenType.equals(JavaTokenType.EQEQ) ? "$val$.isEmpty()" : "$val$.isPresent()", binOp);
}
if (ExpressionUtils.isNullLiteral(lOp)) {
return new TypeConversionDescriptor(lOp.getText() + binOp.getOperationSign().getText() + "$val$",
tokenType.equals(JavaTokenType.EQEQ) ? "$val$.isEmpty()" : "$val$.isPresent()", binOp);
}
}
}
}
return new TypeConversionDescriptor("$val$", "$val$." + GET_METHODS.get(qualifiedName), context);
}
else if (parent instanceof PsiAssignmentExpression assignment && assignment.getRExpression() == context
&& element != null &&
assignment.getLExpression() instanceof PsiReferenceExpression ref && ref.isReferenceTo(element) &&
assignment.getOperationTokenType().equals(JavaTokenType.PLUSEQ) && TypeUtils.isJavaLangString(optionalElementType)) {
String varName = new VariableNameGenerator(context, VariableKind.PARAMETER).byName(ref.getText()).byType(optionalElementType).generate(true);
return new TypeConversionDescriptor("$val$+=$operand$", "$val$=$val$.map(" + varName + "->" + varName + "+$operand$)", assignment);
}
else if (context instanceof PsiAssignmentExpression assignment) {
String compoundOp = assignment.getOperationSign().getText();
String op = compoundOp.substring(0, compoundOp.length() - 1);
if (!op.isEmpty()) {
return new TypeConversionDescriptor(
"$val$" + compoundOp + "$operand$",
"$val$=" + qualifiedName + ".of($val$." + GET_METHODS.get(qualifiedName) + op + "$operand$)", context);
}
}
else if (context instanceof PsiUnaryExpression unary &&
(context instanceof PsiPrefixExpression || ExpressionUtils.isVoidContext(unary))) {
IElementType tokenType = unary.getOperationTokenType();
String op = tokenType.equals(JavaTokenType.PLUSPLUS) ? "+" :
tokenType.equals(JavaTokenType.MINUSMINUS) ? "-" : null;
if (op != null) {
String orig = "$val$";
if (context instanceof PsiPrefixExpression) {
orig = op + op + orig;
}
else {
orig = orig + op + op;
}
return new TypeConversionDescriptor(orig,
"$val$=" + qualifiedName + ".of($val$." + GET_METHODS.get(qualifiedName) + op + "1)", context);
}
}
if (ExpressionUtils.isVoidContext(context)) {
return new TypeConversionDescriptor("$val$", "$val$", context);
}
return wrap(context, qualifiedName);
}
private static @NotNull TypeConversionDescriptor wrap(PsiExpression context, String qualifiedName) {
if (ExpressionUtils.isNullLiteral(context)) {
return new TypeConversionDescriptor("$val$", qualifiedName + ".empty()", context);
}
if (NullabilityUtil.getExpressionNullability(context, true) == Nullability.NOT_NULL) {
return new TypeConversionDescriptor("$val$", qualifiedName + ".of($val$)", context);
}
return new TypeConversionDescriptor("$val$", qualifiedName + ".ofNullable($val$)", context);
}
@Override
public boolean shouldConvertNullInitializer(PsiType from, PsiType to, PsiExpression context) {
return from.equals(OptionalUtil.getOptionalElementType(to));
}
}

View File

@@ -867,6 +867,14 @@ public class TypeMigrationTest extends TypeMigrationTestBase {
public void testGetterToBoolean() {
doTestFieldType("fooMigrateName", PsiTypes.booleanType());
}
public void testInt2OptionalInt() {
doTestFirstParamType("test", myFactory.createTypeFromText("java.util.OptionalInt", null));
}
public void testString2OptionalString() {
doTestFirstParamType("testString", myFactory.createTypeFromText("java.util.Optional<java.lang.String>", null));
}
public void testGetterToBoolean2() {
doTestFieldType("fooDontMigrateName", PsiTypes.booleanType());

View File

@@ -0,0 +1,20 @@
Types:
PsiAssignmentExpression:x += 2 : java.util.OptionalInt
PsiParameter:x : java.util.OptionalInt
PsiPostfixExpression:x++ : java.util.OptionalInt
PsiReferenceExpression:x : java.util.OptionalInt
PsiReferenceExpression:x : java.util.OptionalInt
PsiReferenceExpression:x : java.util.OptionalInt
PsiReferenceExpression:x : java.util.OptionalInt
PsiReferenceExpression:x : java.util.OptionalInt
Conversions:
10 -> java.util.OptionalInt.of($val$) $val$ 10
124 -> java.util.OptionalInt.of($val$) $val$ 124
x += 2 -> $val$=java.util.OptionalInt.of($val$.getAsInt()+$operand$) $val$+=$operand$ x += 2
x -> $val$.getAsInt() $val$ x
x -> $val$.getAsInt() $val$ x
x++ -> $val$=java.util.OptionalInt.of($val$.getAsInt()+1) $val$++ x++
New expression type changes:
Fails:

View File

@@ -0,0 +1,15 @@
import java.util.OptionalInt;
public class Test {
public static void main(String[] args) {
test(OptionalInt.of(124));
}
static void test(OptionalInt x) {
x = OptionalInt.of(10);
x = OptionalInt.of(x.getAsInt() + 1);
x = OptionalInt.of(x.getAsInt() + 2);
System.out.println(x.getAsInt());
System.out.println(x.getAsInt() + 1);
}
}

View File

@@ -0,0 +1,13 @@
public class Test {
public static void main(String[] args) {
test(124);
}
static void test(int x) {
x = 10;
x++;
x += 2;
System.out.println(x);
System.out.println(x + 1);
}
}

View File

@@ -0,0 +1,25 @@
Types:
PsiAssignmentExpression:s += null : java.util.Optional<java.lang.String>
PsiMethodCallExpression:s.trim() : java.lang.String
PsiParameter:s : java.util.Optional<java.lang.String>
PsiReferenceExpression:s : java.util.Optional<java.lang.String>
PsiReferenceExpression:s : java.util.Optional<java.lang.String>
PsiReferenceExpression:s : java.util.Optional<java.lang.String>
PsiReferenceExpression:s : java.util.Optional<java.lang.String>
PsiReferenceExpression:s : java.util.Optional<java.lang.String>
PsiReferenceExpression:s : java.util.Optional<java.lang.String>
PsiReferenceExpression:s : java.util.Optional<java.lang.String>
Conversions:
"World " -> java.util.Optional.of($val$) $val$ "World "
"hello" -> $val$=$val$.map(string->string+$operand$) $val$+=$operand$ s += "hello"
null -> java.util.Optional.empty() $val$ null
null -> java.util.Optional.empty() $val$ null
s += null -> $val$=java.util.Optional.of($val$.orElse(null)+$operand$) $val$+=$operand$ s += null
s -> $val$.isEmpty() $val$==null s == null
s -> $val$.isPresent() $val$!=null s != null
s -> $val$.orElse(null) $val$ s
s.trim() -> $val$.get() $val$ s
New expression type changes:
Fails:

View File

@@ -0,0 +1,21 @@
import java.util.Optional;
public class Test {
public static void main(String[] args) {
testString(Optional.of("World "));
testString(Optional.empty());
}
static void testString(Optional<String> s) {
if (s.isEmpty()) {
System.out.println("oops");
}
if (s.isPresent()) {
s = s.map(string -> string + "hello");
System.out.println(s.orElse(null));
}
s = Optional.of(s.orElse(null) + null);
System.out.println(s.get().trim());
s = Optional.empty();
}
}

View File

@@ -0,0 +1,19 @@
public class Test {
public static void main(String[] args) {
testString("World ");
testString(null);
}
static void testString(String s) {
if (s == null) {
System.out.println("oops");
}
if (s != null) {
s += "hello";
System.out.println(s);
}
s += null;
System.out.println(s.trim());
s = null;
}
}