IDEA-126173 Specify exception type thrown by @NotNull annotations

This commit is contained in:
peter
2014-07-16 21:54:06 +02:00
parent f4d3703103
commit a7ec8b7660
9 changed files with 118 additions and 50 deletions

View File

@@ -15,11 +15,11 @@
*/ */
package com.intellij.compiler.notNullVerification; package com.intellij.compiler.notNullVerification;
import com.sun.istack.internal.NotNull;
import com.sun.istack.internal.Nullable;
import org.jetbrains.org.objectweb.asm.*; import org.jetbrains.org.objectweb.asm.*;
import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@@ -100,6 +100,15 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
myClassName = name; myClassName = name;
} }
private static class NotNullState {
@Nullable String message;
@NotNull String exceptionType;
NotNullState(String exceptionType) {
this.exceptionType = exceptionType;
}
}
@Override @Override
public MethodVisitor visitMethod(final int access, final String name, String desc, String signature, String[] exceptions) { public MethodVisitor visitMethod(final int access, final String name, String desc, String signature, String[] exceptions) {
final Type[] args = Type.getArgumentTypes(desc); final Type[] args = Type.getArgumentTypes(desc);
@@ -107,29 +116,32 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
final MethodVisitor v = cv.visitMethod(access, name, desc, signature, exceptions); final MethodVisitor v = cv.visitMethod(access, name, desc, signature, exceptions);
final Map<Integer, String> paramNames = myMethodParamNames.get(myClassName + '.' + name + desc); final Map<Integer, String> paramNames = myMethodParamNames.get(myClassName + '.' + name + desc);
return new MethodVisitor(Opcodes.ASM5, v) { return new MethodVisitor(Opcodes.ASM5, v) {
private final Map<Integer, NotNullState> myNotNullParams = new LinkedHashMap<Integer, NotNullState>();
private final List<Integer> myNotNullParams = new ArrayList<Integer>();
private int mySyntheticCount = 0; private int mySyntheticCount = 0;
private boolean myIsNotNull = false; private NotNullState myMethodNotNull;
private String myMessage = null;
private Label myStartGeneratedCodeLabel; private Label myStartGeneratedCodeLabel;
private AnnotationVisitor collectNotNullArgs(AnnotationVisitor base, final NotNullState state) {
return new AnnotationVisitor(Opcodes.ASM5, base) {
@Override
public void visit(String methodName, Object o) {
if (ANNOTATION_DEFAULT_METHOD.equals(methodName) && !((String) o).isEmpty()) {
state.message = (String) o;
}
else if ("exception".equals(methodName) && o instanceof Type && !((Type)o).getClassName().equals(Exception.class.getName())) {
state.exceptionType = ((Type)o).getInternalName();
}
super.visit(methodName, o);
}
};
}
public AnnotationVisitor visitParameterAnnotation(final int parameter, final String anno, final boolean visible) { public AnnotationVisitor visitParameterAnnotation(final int parameter, final String anno, final boolean visible) {
AnnotationVisitor av = mv.visitParameterAnnotation(parameter, anno, visible); AnnotationVisitor av = mv.visitParameterAnnotation(parameter, anno, visible);
if (isReferenceType(args[parameter]) && anno.equals(NOT_NULL_TYPE)) { if (isReferenceType(args[parameter]) && anno.equals(NOT_NULL_TYPE)) {
myNotNullParams.add(new Integer(parameter)); NotNullState state = new NotNullState(IAE_CLASS_NAME);
av = new AnnotationVisitor(Opcodes.ASM5, av) { myNotNullParams.put(new Integer(parameter), state);
@Override av = collectNotNullArgs(av, state);
public void visit(String methodName, Object o) {
if(ANNOTATION_DEFAULT_METHOD.equals(methodName)) {
String message = (String) o;
if(!message.isEmpty()) {
myMessage = message;
}
}
super.visit(methodName, o);
}
};
} }
else if (anno.equals(SYNTHETIC_TYPE)) { else if (anno.equals(SYNTHETIC_TYPE)) {
// see http://forge.ow2.org/tracker/?aid=307392&group_id=23&atid=100023&func=detail // see http://forge.ow2.org/tracker/?aid=307392&group_id=23&atid=100023&func=detail
@@ -143,19 +155,8 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
public AnnotationVisitor visitAnnotation(String anno, boolean isRuntime) { public AnnotationVisitor visitAnnotation(String anno, boolean isRuntime) {
AnnotationVisitor av = mv.visitAnnotation(anno, isRuntime); AnnotationVisitor av = mv.visitAnnotation(anno, isRuntime);
if (isReferenceType(returnType) && anno.equals(NOT_NULL_TYPE)) { if (isReferenceType(returnType) && anno.equals(NOT_NULL_TYPE)) {
myIsNotNull = true; myMethodNotNull = new NotNullState(ISE_CLASS_NAME);
av = new AnnotationVisitor(Opcodes.ASM5, av) { av = collectNotNullArgs(av, myMethodNotNull);
@Override
public void visit(String methodName, Object o) {
if(ANNOTATION_DEFAULT_METHOD.equals(methodName)) {
String message = (String) o;
if(!message.isEmpty()) {
myMessage = message;
}
}
super.visit(methodName, o);
}
};
} }
return av; return av;
@@ -167,7 +168,8 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
myStartGeneratedCodeLabel = new Label(); myStartGeneratedCodeLabel = new Label();
mv.visitLabel(myStartGeneratedCodeLabel); mv.visitLabel(myStartGeneratedCodeLabel);
} }
for (Integer param : myNotNullParams) { for (Map.Entry<Integer, NotNullState> entry : myNotNullParams.entrySet()) {
Integer param = entry.getKey();
int var = ((access & ACC_STATIC) == 0) ? 1 : 0; int var = ((access & ACC_STATIC) == 0) ? 1 : 0;
for (int i = 0; i < param; ++i) { for (int i = 0; i < param; ++i) {
var += args[i].getSize(); var += args[i].getSize();
@@ -177,14 +179,15 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
Label end = new Label(); Label end = new Label();
mv.visitJumpInsn(IFNONNULL, end); mv.visitJumpInsn(IFNONNULL, end);
NotNullState state = entry.getValue();
String paramName = paramNames == null ? null : paramNames.get(param); String paramName = paramNames == null ? null : paramNames.get(param);
String descrPattern = myMessage != null String descrPattern = state.message != null
? myMessage ? state.message
: paramName != null ? NULL_ARG_MESSAGE_NAMED : NULL_ARG_MESSAGE_INDEXED; : paramName != null ? NULL_ARG_MESSAGE_NAMED : NULL_ARG_MESSAGE_INDEXED;
String[] args = myMessage != null String[] args = state.message != null
? EMPTY_STRING_ARRAY ? EMPTY_STRING_ARRAY
: new String[]{paramName != null ? paramName : String.valueOf(param - mySyntheticCount), myClassName, name}; : new String[]{paramName != null ? paramName : String.valueOf(param - mySyntheticCount), myClassName, name};
generateThrow(IAE_CLASS_NAME, end, descrPattern, args); generateThrow(state.exceptionType, end, descrPattern, args);
} }
} }
@@ -199,13 +202,13 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
@Override @Override
public void visitInsn(int opcode) { public void visitInsn(int opcode) {
if (opcode == ARETURN) { if (opcode == ARETURN) {
if (myIsNotNull) { if (myMethodNotNull != null) {
mv.visitInsn(DUP); mv.visitInsn(DUP);
final Label skipLabel = new Label(); final Label skipLabel = new Label();
mv.visitJumpInsn(IFNONNULL, skipLabel); mv.visitJumpInsn(IFNONNULL, skipLabel);
String descrPattern = myMessage != null ? myMessage : NULL_RESULT_MESSAGE; String descrPattern = myMethodNotNull.message != null ? myMethodNotNull.message : NULL_RESULT_MESSAGE;
String[] args = myMessage != null ? EMPTY_STRING_ARRAY : new String[]{myClassName, name}; String[] args = myMethodNotNull.message != null ? EMPTY_STRING_ARRAY : new String[]{myClassName, name};
generateThrow(ISE_CLASS_NAME, skipLabel, descrPattern, args); generateThrow(myMethodNotNull.exceptionType, skipLabel, descrPattern, args);
} }
} }

View File

@@ -16,7 +16,6 @@
package com.intellij.codeInspection.nullable; package com.intellij.codeInspection.nullable;
import com.intellij.codeInsight.AnnotationUtil; import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.InferredAnnotationsManager;
import com.intellij.codeInsight.NullableNotNullManager; import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.daemon.GroupNames; import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInsight.intention.AddAnnotationPsiFix; import com.intellij.codeInsight.intention.AddAnnotationPsiFix;
@@ -204,6 +203,33 @@ public class NullableStuffInspectionBase extends BaseJavaBatchLocalInspectionToo
if (!PsiUtil.isLanguageLevel5OrHigher(parameter)) return; if (!PsiUtil.isLanguageLevel5OrHigher(parameter)) return;
check(parameter, holder, parameter.getType()); check(parameter, holder, parameter.getType());
} }
@Override
public void visitAnnotation(PsiAnnotation annotation) {
if (!AnnotationUtil.NOT_NULL.equals(annotation.getQualifiedName())) return;
PsiAnnotationMemberValue value = annotation.findDeclaredAttributeValue("exception");
if (value instanceof PsiClassObjectAccessExpression) {
PsiClass psiClass = PsiUtil.resolveClassInClassTypeOnly(((PsiClassObjectAccessExpression)value).getOperand().getType());
if (psiClass != null && !hasStringConstructor(psiClass)) {
holder.registerProblem(value,
"Custom exception class should have a constructor with a single message parameter of String type",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
}
}
}
private boolean hasStringConstructor(PsiClass aClass) {
for (PsiMethod method : aClass.getConstructors()) {
PsiParameterList list = method.getParameterList();
if (list.getParametersCount() == 1 &&
list.getParameters()[0].getType().equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
return true;
}
}
return false;
}
}; };
} }
@@ -232,7 +258,7 @@ public class NullableStuffInspectionBase extends BaseJavaBatchLocalInspectionToo
PsiAnnotation isDeclaredNotNull = AnnotationUtil.findAnnotation(parameter, manager.getNotNulls()); PsiAnnotation isDeclaredNotNull = AnnotationUtil.findAnnotation(parameter, manager.getNotNulls());
PsiAnnotation isDeclaredNullable = AnnotationUtil.findAnnotation(parameter, manager.getNullables()); PsiAnnotation isDeclaredNullable = AnnotationUtil.findAnnotation(parameter, manager.getNullables());
if (isDeclaredNullable != null && isDeclaredNotNull != null) { if (isDeclaredNullable != null && isDeclaredNotNull != null) {
reportNullableNotNullConflict(holder, parameter, isDeclaredNullable, isDeclaredNotNull); reportNullableNotNullConflict(holder, parameter, isDeclaredNullable, isDeclaredNotNull);
} }
if ((isDeclaredNotNull != null || isDeclaredNullable != null) && type != null && TypeConversionUtil.isPrimitive(type.getCanonicalText())) { if ((isDeclaredNotNull != null || isDeclaredNullable != null) && type != null && TypeConversionUtil.isPrimitive(type.getCanonicalText())) {
PsiAnnotation annotation = isDeclaredNotNull == null ? isDeclaredNullable : isDeclaredNotNull; PsiAnnotation annotation = isDeclaredNotNull == null ? isDeclaredNullable : isDeclaredNotNull;

View File

@@ -0,0 +1,5 @@
import org.jetbrains.annotations.NotNull;
public class CustomExceptionType {
public void foo(Object obj, @NotNull(exception = NullPointerException.class) Object obj2) { }
}

View File

@@ -0,0 +1,8 @@
import org.jetbrains.annotations.*;
class Test {
public void foo(@NotNull(exception = NullPointerException.class) String a) { }
public void foo2(@NotNull(exception = <warning descr="Custom exception class should have a constructor with a single message parameter of String type">CustomException.class</warning>) String a) { }
}
class CustomException extends Exception {}

View File

@@ -30,6 +30,7 @@ public class NullableStuffInspectionTest extends LightCodeInsightFixtureTestCase
public void testProblems2() throws Exception{ doTest(); } public void testProblems2() throws Exception{ doTest(); }
public void testNullableFieldNotnullParam() throws Exception{ doTest(); } public void testNullableFieldNotnullParam() throws Exception{ doTest(); }
public void testNotNullFieldNullableParam() throws Exception{ doTest(); } public void testNotNullFieldNullableParam() throws Exception{ doTest(); }
public void testNotNullCustomException() throws Exception{ doTest(); }
public void testGetterSetterProblems() throws Exception{ doTest(); } public void testGetterSetterProblems() throws Exception{ doTest(); }
public void testOverriddenMethods() throws Exception{ public void testOverriddenMethods() throws Exception{

View File

@@ -67,48 +67,48 @@ public class NotNullVerifyingInstrumenterTest extends UsefulTestCase {
} }
public void testSimpleReturn() throws Exception { public void testSimpleReturn() throws Exception {
Class testClass = prepareTest(); Class<?> testClass = prepareTest();
Object instance = testClass.newInstance(); Object instance = testClass.newInstance();
Method method = testClass.getMethod("test"); Method method = testClass.getMethod("test");
verifyCallThrowsException("@NotNull method SimpleReturn.test must not return null", instance, method); verifyCallThrowsException("@NotNull method SimpleReturn.test must not return null", instance, method);
} }
public void testSimpleReturnWithMessage() throws Exception { public void testSimpleReturnWithMessage() throws Exception {
Class testClass = prepareTest(); Class<?> testClass = prepareTest();
Object instance = testClass.newInstance(); Object instance = testClass.newInstance();
Method method = testClass.getMethod("test"); Method method = testClass.getMethod("test");
verifyCallThrowsException("This method cannot return null", instance, method); verifyCallThrowsException("This method cannot return null", instance, method);
} }
public void testMultipleReturns() throws Exception { public void testMultipleReturns() throws Exception {
Class testClass = prepareTest(); Class<?> testClass = prepareTest();
Object instance = testClass.newInstance(); Object instance = testClass.newInstance();
Method method = testClass.getMethod("test", int.class); Method method = testClass.getMethod("test", int.class);
verifyCallThrowsException("@NotNull method MultipleReturns.test must not return null", instance, method, 1); verifyCallThrowsException("@NotNull method MultipleReturns.test must not return null", instance, method, 1);
} }
public void testSimpleParam() throws Exception { public void testSimpleParam() throws Exception {
Class testClass = prepareTest(); Class<?> testClass = prepareTest();
Object instance = testClass.newInstance(); Object instance = testClass.newInstance();
Method method = testClass.getMethod("test", Object.class); Method method = testClass.getMethod("test", Object.class);
verifyCallThrowsException("Argument 0 for @NotNull parameter of SimpleParam.test must not be null", instance, method, (Object)null); verifyCallThrowsException("Argument 0 for @NotNull parameter of SimpleParam.test must not be null", instance, method, (Object)null);
} }
public void testSimpleParamWithMessage() throws Exception { public void testSimpleParamWithMessage() throws Exception {
Class testClass = prepareTest(); Class<?> testClass = prepareTest();
Object instance = testClass.newInstance(); Object instance = testClass.newInstance();
Method method = testClass.getMethod("test", Object.class); Method method = testClass.getMethod("test", Object.class);
verifyCallThrowsException("SimpleParamWithMessage.test(o) cant be null", instance, method, (Object)null); verifyCallThrowsException("SimpleParamWithMessage.test(o) cant be null", instance, method, (Object)null);
} }
public void testConstructorParam() throws Exception { public void testConstructorParam() throws Exception {
Class testClass = prepareTest(); Class<?> testClass = prepareTest();
Constructor method = testClass.getConstructor(Object.class); Constructor method = testClass.getConstructor(Object.class);
verifyCallThrowsException("Argument 0 for @NotNull parameter of ConstructorParam.<init> must not be null", null, method, (Object)null); verifyCallThrowsException("Argument 0 for @NotNull parameter of ConstructorParam.<init> must not be null", null, method, (Object)null);
} }
public void testConstructorParamWithMessage() throws Exception { public void testConstructorParamWithMessage() throws Exception {
Class testClass = prepareTest(); Class<?> testClass = prepareTest();
Constructor method = testClass.getConstructor(Object.class); Constructor method = testClass.getConstructor(Object.class);
verifyCallThrowsException("ConstructorParam.ConstructorParam.o cant be null", null, method, (Object)null); verifyCallThrowsException("ConstructorParam.ConstructorParam.o cant be null", null, method, (Object)null);
} }
@@ -132,6 +132,19 @@ public class NotNullVerifyingInstrumenterTest extends UsefulTestCase {
assertNotNull(field); assertNotNull(field);
} }
public void testCustomExceptionType() throws Exception {
Class<?> testClass = prepareTest();
try {
testClass.getMethod("foo", Object.class, Object.class).invoke(testClass.newInstance(), null, null);
fail();
}
catch (InvocationTargetException e) {
//noinspection ThrowableResultOfMethodCallIgnored
assertInstanceOf(e.getCause(), NullPointerException.class);
assertEquals("Argument 1 for @NotNull parameter of CustomExceptionType.foo must not be null", e.getCause().getMessage());
}
}
public void testEnumConstructorSecondParam() throws Exception { public void testEnumConstructorSecondParam() throws Exception {
Class testClass = prepareTest(); Class testClass = prepareTest();
Object field = testClass.getField("Value"); Object field = testClass.getField("Value");

View File

@@ -30,5 +30,17 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.CLASS) @Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})
public @interface NotNull { public @interface NotNull {
/**
* @return Custom exception message
*/
String value() default ""; String value() default "";
/**
* @return Custom exception type that should be thrown when not-nullity contract is violated.
* The exception class should have a constructor with one String argument (message).
*
* By default, {@link java.lang.IllegalArgumentException} is thrown on null method arguments and
* {@link java.lang.IllegalStateException} &mdash; on null return value.
*/
Class<? extends Exception> exception() default Exception.class;
} }