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;
import com.sun.istack.internal.NotNull;
import com.sun.istack.internal.Nullable;
import org.jetbrains.org.objectweb.asm.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
@@ -100,6 +100,15 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
myClassName = name;
}
private static class NotNullState {
@Nullable String message;
@NotNull String exceptionType;
NotNullState(String exceptionType) {
this.exceptionType = exceptionType;
}
}
@Override
public MethodVisitor visitMethod(final int access, final String name, String desc, String signature, String[] exceptions) {
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 Map<Integer, String> paramNames = myMethodParamNames.get(myClassName + '.' + name + desc);
return new MethodVisitor(Opcodes.ASM5, v) {
private final List<Integer> myNotNullParams = new ArrayList<Integer>();
private final Map<Integer, NotNullState> myNotNullParams = new LinkedHashMap<Integer, NotNullState>();
private int mySyntheticCount = 0;
private boolean myIsNotNull = false;
private String myMessage = null;
private NotNullState myMethodNotNull;
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) {
AnnotationVisitor av = mv.visitParameterAnnotation(parameter, anno, visible);
if (isReferenceType(args[parameter]) && anno.equals(NOT_NULL_TYPE)) {
myNotNullParams.add(new Integer(parameter));
av = new AnnotationVisitor(Opcodes.ASM5, av) {
@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);
}
};
NotNullState state = new NotNullState(IAE_CLASS_NAME);
myNotNullParams.put(new Integer(parameter), state);
av = collectNotNullArgs(av, state);
}
else if (anno.equals(SYNTHETIC_TYPE)) {
// 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) {
AnnotationVisitor av = mv.visitAnnotation(anno, isRuntime);
if (isReferenceType(returnType) && anno.equals(NOT_NULL_TYPE)) {
myIsNotNull = true;
av = new AnnotationVisitor(Opcodes.ASM5, av) {
@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);
}
};
myMethodNotNull = new NotNullState(ISE_CLASS_NAME);
av = collectNotNullArgs(av, myMethodNotNull);
}
return av;
@@ -167,7 +168,8 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
myStartGeneratedCodeLabel = new Label();
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;
for (int i = 0; i < param; ++i) {
var += args[i].getSize();
@@ -177,14 +179,15 @@ public class NotNullVerifyingInstrumenter extends ClassVisitor implements Opcode
Label end = new Label();
mv.visitJumpInsn(IFNONNULL, end);
NotNullState state = entry.getValue();
String paramName = paramNames == null ? null : paramNames.get(param);
String descrPattern = myMessage != null
? myMessage
String descrPattern = state.message != null
? state.message
: paramName != null ? NULL_ARG_MESSAGE_NAMED : NULL_ARG_MESSAGE_INDEXED;
String[] args = myMessage != null
String[] args = state.message != null
? EMPTY_STRING_ARRAY
: 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
public void visitInsn(int opcode) {
if (opcode == ARETURN) {
if (myIsNotNull) {
if (myMethodNotNull != null) {
mv.visitInsn(DUP);
final Label skipLabel = new Label();
mv.visitJumpInsn(IFNONNULL, skipLabel);
String descrPattern = myMessage != null ? myMessage : NULL_RESULT_MESSAGE;
String[] args = myMessage != null ? EMPTY_STRING_ARRAY : new String[]{myClassName, name};
generateThrow(ISE_CLASS_NAME, skipLabel, descrPattern, args);
String descrPattern = myMethodNotNull.message != null ? myMethodNotNull.message : NULL_RESULT_MESSAGE;
String[] args = myMethodNotNull.message != null ? EMPTY_STRING_ARRAY : new String[]{myClassName, name};
generateThrow(myMethodNotNull.exceptionType, skipLabel, descrPattern, args);
}
}

View File

@@ -16,7 +16,6 @@
package com.intellij.codeInspection.nullable;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.InferredAnnotationsManager;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInsight.intention.AddAnnotationPsiFix;
@@ -204,6 +203,33 @@ public class NullableStuffInspectionBase extends BaseJavaBatchLocalInspectionToo
if (!PsiUtil.isLanguageLevel5OrHigher(parameter)) return;
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 isDeclaredNullable = AnnotationUtil.findAnnotation(parameter, manager.getNullables());
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())) {
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 testNullableFieldNotnullParam() throws Exception{ doTest(); }
public void testNotNullFieldNullableParam() throws Exception{ doTest(); }
public void testNotNullCustomException() throws Exception{ doTest(); }
public void testGetterSetterProblems() throws Exception{ doTest(); }
public void testOverriddenMethods() throws Exception{

View File

@@ -67,48 +67,48 @@ public class NotNullVerifyingInstrumenterTest extends UsefulTestCase {
}
public void testSimpleReturn() throws Exception {
Class testClass = prepareTest();
Class<?> testClass = prepareTest();
Object instance = testClass.newInstance();
Method method = testClass.getMethod("test");
verifyCallThrowsException("@NotNull method SimpleReturn.test must not return null", instance, method);
}
public void testSimpleReturnWithMessage() throws Exception {
Class testClass = prepareTest();
Class<?> testClass = prepareTest();
Object instance = testClass.newInstance();
Method method = testClass.getMethod("test");
verifyCallThrowsException("This method cannot return null", instance, method);
}
public void testMultipleReturns() throws Exception {
Class testClass = prepareTest();
Class<?> testClass = prepareTest();
Object instance = testClass.newInstance();
Method method = testClass.getMethod("test", int.class);
verifyCallThrowsException("@NotNull method MultipleReturns.test must not return null", instance, method, 1);
}
public void testSimpleParam() throws Exception {
Class testClass = prepareTest();
Class<?> testClass = prepareTest();
Object instance = testClass.newInstance();
Method method = testClass.getMethod("test", Object.class);
verifyCallThrowsException("Argument 0 for @NotNull parameter of SimpleParam.test must not be null", instance, method, (Object)null);
}
public void testSimpleParamWithMessage() throws Exception {
Class testClass = prepareTest();
Class<?> testClass = prepareTest();
Object instance = testClass.newInstance();
Method method = testClass.getMethod("test", Object.class);
verifyCallThrowsException("SimpleParamWithMessage.test(o) cant be null", instance, method, (Object)null);
}
public void testConstructorParam() throws Exception {
Class testClass = prepareTest();
Class<?> testClass = prepareTest();
Constructor method = testClass.getConstructor(Object.class);
verifyCallThrowsException("Argument 0 for @NotNull parameter of ConstructorParam.<init> must not be null", null, method, (Object)null);
}
public void testConstructorParamWithMessage() throws Exception {
Class testClass = prepareTest();
Class<?> testClass = prepareTest();
Constructor method = testClass.getConstructor(Object.class);
verifyCallThrowsException("ConstructorParam.ConstructorParam.o cant be null", null, method, (Object)null);
}
@@ -132,6 +132,19 @@ public class NotNullVerifyingInstrumenterTest extends UsefulTestCase {
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 {
Class testClass = prepareTest();
Object field = testClass.getField("Value");

View File

@@ -30,5 +30,17 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})
public @interface NotNull {
/**
* @return Custom exception message
*/
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;
}