Java: Infer nullability annotations for extracted method's parameters (IDEA-150243)

This commit is contained in:
Pavel Dolgov
2017-06-19 12:54:10 +03:00
parent 99bbe2e66e
commit f060340474
18 changed files with 483 additions and 4 deletions
@@ -15,14 +15,12 @@
*/
package com.intellij.refactoring.extractMethod;
import com.intellij.codeInsight.ChangeContextUtil;
import com.intellij.codeInsight.CodeInsightUtil;
import com.intellij.codeInsight.ExceptionUtil;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.*;
import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil;
import com.intellij.codeInsight.daemon.impl.quickfix.AnonymousTargetClassPreselectionUtil;
import com.intellij.codeInsight.generation.GenerateMembersUtil;
import com.intellij.codeInsight.highlighting.HighlightManager;
import com.intellij.codeInsight.intention.AddAnnotationPsiFix;
import com.intellij.codeInsight.intention.impl.AddNullableNotNullAnnotationFix;
import com.intellij.codeInsight.navigation.NavigationUtil;
import com.intellij.codeInspection.dataFlow.*;
@@ -1442,10 +1440,121 @@ public class ExtractMethodProcessor implements MatchProvider {
PsiModifierList parmModifierList = parm.getModifierList();
LOG.assertTrue(parmModifierList != null);
GenerateMembersUtil.copyAnnotations(modifierList, parmModifierList, SuppressWarnings.class.getName());
final NullableNotNullManager nullabilityManager = NullableNotNullManager.getInstance(myProject);
if (AnnotationUtil.isAnnotated(variable, nullabilityManager.getNullables()) ||
AnnotationUtil.isAnnotated(variable, nullabilityManager.getNotNulls()) ||
PropertiesComponent.getInstance(myProject).getBoolean(ExtractMethodDialog.EXTRACT_METHOD_GENERATE_ANNOTATIONS, false)) {
final Nullness definitelyNotNull = getDefinitelyNotNull((PsiParameter)variable);
final String toAdd;
final List<String> toKeep;
final List<String> toRemove;
switch (definitelyNotNull) {
case NOT_NULL:
toAdd = nullabilityManager.getDefaultNotNull();
toKeep = nullabilityManager.getNotNulls();
toRemove = nullabilityManager.getNullables();
break;
case NULLABLE:
toAdd = nullabilityManager.getDefaultNullable();
toKeep = nullabilityManager.getNullables();
toRemove = nullabilityManager.getNotNulls();
break;
default:
return;
}
AddAnnotationPsiFix.removePhysicalAnnotations(parm, toRemove.toArray(ArrayUtil.EMPTY_STRING_ARRAY));
if (!AnnotationUtil.isAnnotated(parm, toKeep)) {
final PsiAnnotation added = AddAnnotationPsiFix.addPhysicalAnnotation(toAdd, PsiNameValuePair.EMPTY_ARRAY, parmModifierList);
JavaCodeStyleManager.getInstance(myProject).shortenClassReferences(added);
}
}
}
}
}
@NotNull
private Nullness getDefinitelyNotNull(@NotNull PsiParameter variable) {
if (variable.getType() instanceof PsiPrimitiveType) {
return Nullness.UNKNOWN;
}
PsiElement parent = variable.getParent();
if (parent instanceof PsiParameterList) {
final PsiElement grandParent = parent.getParent();
String originalMethodText = null;
int extractedCodeRelativeOffset = 0;
// DFA doesn't work with a part of method body or with lambda body when checking a method/lambda parameter
// we have to copy the whole method or convert the whole lambda to a method
if (grandParent instanceof PsiMethod) {
originalMethodText = grandParent.getText();
final int methodOffset = grandParent.getTextRange().getStartOffset();
final int extractOffset = myElements[0].getTextRange().getStartOffset();
extractedCodeRelativeOffset = extractOffset - methodOffset;
}
else if (grandParent instanceof PsiLambdaExpression) {
final PsiLambdaExpression lambdaExpression = (PsiLambdaExpression)grandParent;
if (lambdaExpression.hasFormalParameterTypes()) {
final PsiElement lambdaBody = lambdaExpression.getBody();
if (lambdaBody instanceof PsiCodeBlock) {
final PsiMethod interfaceMethod = LambdaUtil.getFunctionalInterfaceMethod(grandParent);
if (interfaceMethod != null) {
PsiType returnType = interfaceMethod.getReturnType();
if (returnType != null) {
final PsiParameterList parameterList = lambdaExpression.getParameterList();
final String dummyMethodHeader = returnType.getCanonicalText() + " " + interfaceMethod.getName() + parameterList.getText();
originalMethodText = dummyMethodHeader + lambdaBody.getText();
final int bodyOffset = lambdaBody.getTextRange().getStartOffset();
final int extractOffset = myElements[0].getTextRange().getStartOffset();
extractedCodeRelativeOffset = extractOffset - bodyOffset + dummyMethodHeader.length();
}
}
}
}
}
if (originalMethodText != null) {
// insert a dummy usage of the variable before the extracted fragment, where we're going to check the nullness of the variable
final String dummyMethodText = originalMethodText.substring(0, extractedCodeRelativeOffset) +
"Object _Dummy_ = " + variable.getName() + ";" +
originalMethodText.substring(extractedCodeRelativeOffset);
final PsiElementFactory factory = JavaPsiFacade.getInstance(myProject).getElementFactory();
final PsiMethod dummyMethod;
try {
dummyMethod = factory.createMethodFromText(dummyMethodText, grandParent.getParent());
}
catch (IncorrectOperationException e) {
LOG.debug("Failed to parse dummy method", dummyMethodText); // probably incomplete code
return Nullness.UNKNOWN;
}
PsiElement atOffset = dummyMethod.findElementAt(extractedCodeRelativeOffset);
while (atOffset != null && atOffset.getStartOffsetInParent() == 0) {
atOffset = atOffset.getParent();
}
if (atOffset instanceof PsiDeclarationStatement) {
final PsiElement[] declaredElements = ((PsiDeclarationStatement)atOffset).getDeclaredElements();
if (declaredElements.length == 1) {
final PsiElement declaredElement = declaredElements[0];
if (declaredElement instanceof PsiLocalVariable) {
final PsiExpression initializer = ((PsiLocalVariable)declaredElement).getInitializer();
if (initializer instanceof PsiReferenceExpression) {
final int parameterIndex = ((PsiParameterList)parent).getParameterIndex(variable);
final PsiParameter dummyParameter = dummyMethod.getParameterList().getParameters()[parameterIndex];
if (((PsiReferenceExpression)initializer).isReferenceTo(dummyParameter)) {
final Nullness nullness = DfaUtil.checkNullness(dummyParameter, initializer);
return nullness == Nullness.NOT_NULL ? Nullness.NOT_NULL : Nullness.NULLABLE; // 'unknown' counts as 'nullable'
}
}
}
}
}
}
}
return Nullness.UNKNOWN;
}
@NotNull
protected PsiMethodCallExpression generateMethodCall(PsiExpression instanceQualifier, final boolean generateArgs) throws IncorrectOperationException {
@NonNls StringBuilder buffer = new StringBuilder();
@@ -0,0 +1,13 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f(@Nullable Object o) {
if (o != null) {
<selection>g(o);</selection>
}
}
void g(@NotNull Object o) {
}
}
@@ -0,0 +1,17 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f(@Nullable Object o) {
if (o != null) {
newMethod(o);
}
}
private void newMethod(@NotNull Object o) {
g(o);
}
void g(@NotNull Object o) {
}
}
@@ -0,0 +1,18 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f(@Nullable Object o) {
if (o != null) {<selection>
if (o instanceof String) {
o = 1;
} else {
System.out.println(o);
}
g(o);</selection>
}
}
void g(@NotNull Object o) {
}
}
@@ -0,0 +1,22 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f(@Nullable Object o) {
if (o != null) {
newMethod(o);
}
}
private void newMethod(@NotNull Object o) {
if (o instanceof String) {
o = 1;
} else {
System.out.println(o);
}
g(o);
}
void g(@NotNull Object o) {
}
}
@@ -0,0 +1,27 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f() {
I i = new I() {
@Override
public void m(@Nullable Object o) {
if (o != null) {<selection>
if (o instanceof String) {
o = 2;
} else {
System.out.println(o);
}
g(o);</selection>
}
}
};
}
void g(@NotNull Object o) {
}
interface I {
void m(Object o);
}
}
@@ -0,0 +1,31 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f() {
I i = new I() {
@Override
public void m(@Nullable Object o) {
if (o != null) {
newMethod(o);
}
}
};
}
private void newMethod(@NotNull Object o) {
if (o instanceof String) {
o = 2;
} else {
System.out.println(o);
}
g(o);
}
void g(@NotNull Object o) {
}
interface I {
void m(Object o);
}
}
@@ -0,0 +1,26 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f() {
I i = (@Nullable Object o) -> {
if (o != null) {<selection>
if (o instanceof String) {
o = 3;
}
else {
System.out.println(o);
}
g(o);</selection>
}
};
}
void g(@NotNull Object o) {
}
interface I {
void m(Object o);
}
}
@@ -0,0 +1,30 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f() {
I i = (@Nullable Object o) -> {
if (o != null) {
newMethod(o);
}
};
}
private void newMethod(@NotNull Object o) {
if (o instanceof String) {
o = 3;
}
else {
System.out.println(o);
}
g(o);
}
void g(@NotNull Object o) {
}
interface I {
void m(Object o);
}
}
@@ -0,0 +1,27 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f() {
I i = (@Nullable String o) -> {
if (o != null) {<selection>
if (o instanceof String) {
o = "4";
}
else {
System.out.println(o);
}
g(o);</selection>
}
return "";
};
}
void g(@NotNull Object o) {
}
interface I<T, R> {
R m(T t);
}
}
@@ -0,0 +1,31 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f() {
I i = (@Nullable String o) -> {
if (o != null) {
newMethod(o);
}
return "";
};
}
private void newMethod(@NotNull String o) {
if (o instanceof String) {
o = "4";
}
else {
System.out.println(o);
}
g(o);
}
void g(@NotNull Object o) {
}
interface I<T, R> {
R m(T t);
}
}
@@ -0,0 +1,13 @@
import org.jetbrains.annotations.NotNull;
class C {
void f(@NotNull Object o, boolean b) {
if (b) {
o = null;
}
<selection>g(o);</selection>
}
void g(Object o) {
}
}
@@ -0,0 +1,18 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f(@NotNull Object o, boolean b) {
if (b) {
o = null;
}
newMethod(o);
}
private void newMethod(@Nullable Object o) {
g(o);
}
void g(Object o) {
}
}
@@ -0,0 +1,13 @@
import org.jetbrains.annotations.Nullable;
class C {
void f(@Nullable Object o, boolean b) {
if (b) {
o = null;
}
<selection>g(o);</selection>
}
void g(Object o) {
}
}
@@ -0,0 +1,17 @@
import org.jetbrains.annotations.Nullable;
class C {
void f(@Nullable Object o, boolean b) {
if (b) {
o = null;
}
newMethod(o);
}
private void newMethod(@Nullable Object o) {
g(o);
}
void g(Object o) {
}
}
@@ -0,0 +1,15 @@
import org.jetbrains.annotations.Nullable;
class C {
void f(@Nullable Object o, boolean b) {
while (o != null) {
if (b) {
o = 7;
}
<selection>g(o);</selection>
}
}
void g(Object o) {
}
}
@@ -0,0 +1,20 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class C {
void f(@Nullable Object o, boolean b) {
while (o != null) {
if (b) {
o = 7;
}
newMethod(o);
}
}
private void newMethod(@NotNull Object o) {
g(o);
}
void g(Object o) {
}
}
@@ -922,6 +922,38 @@ public class ExtractMethodTest extends LightCodeInsightTestCase {
doTest();
}
public void testNotNullArgument0() throws Exception {
doTest();
}
public void testNotNullArgument1() throws Exception {
doTest();
}
public void testNotNullArgument2() throws Exception {
doTest();
}
public void testNotNullArgument3() throws Exception {
doTest();
}
public void testNotNullArgument4() throws Exception {
doTest();
}
public void testNotNullArgument5() throws Exception {
doTest();
}
public void testNotNullArgument6() throws Exception {
doTest();
}
public void testNotNullArgument7() throws Exception {
doTest();
}
public void testQualifyWhenConflictingNamePresent() throws Exception {
final CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(getProject());
settings.ELSE_ON_NEW_LINE = true;