mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-13 15:52:01 +07:00
IDEA-126173 Specify exception type thrown by @NotNull annotations
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class CustomExceptionType {
|
||||
public void foo(Object obj, @NotNull(exception = NullPointerException.class) Object obj2) { }
|
||||
}
|
||||
Binary file not shown.
@@ -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 {}
|
||||
@@ -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{
|
||||
|
||||
@@ -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");
|
||||
|
||||
Binary file not shown.
@@ -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} — on null return value.
|
||||
*/
|
||||
Class<? extends Exception> exception() default Exception.class;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user