diff --git a/java/compiler/instrumentation-util/src/com/intellij/compiler/notNullVerification/NotNullVerifyingInstrumenter.java b/java/compiler/instrumentation-util/src/com/intellij/compiler/notNullVerification/NotNullVerifyingInstrumenter.java index 3bc0dc093fc8..686791e94c04 100644 --- a/java/compiler/instrumentation-util/src/com/intellij/compiler/notNullVerification/NotNullVerifyingInstrumenter.java +++ b/java/compiler/instrumentation-util/src/com/intellij/compiler/notNullVerification/NotNullVerifyingInstrumenter.java @@ -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 paramNames = myMethodParamNames.get(myClassName + '.' + name + desc); return new MethodVisitor(Opcodes.ASM5, v) { - - private final List myNotNullParams = new ArrayList(); + private final Map myNotNullParams = new LinkedHashMap(); 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 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); } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java b/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java index 1d39b0a090d6..9486af61a559 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java @@ -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; diff --git a/java/java-tests/testData/compiler/notNullVerification/CustomExceptionType.java b/java/java-tests/testData/compiler/notNullVerification/CustomExceptionType.java new file mode 100644 index 000000000000..055b339c5f9c --- /dev/null +++ b/java/java-tests/testData/compiler/notNullVerification/CustomExceptionType.java @@ -0,0 +1,5 @@ +import org.jetbrains.annotations.NotNull; + +public class CustomExceptionType { + public void foo(Object obj, @NotNull(exception = NullPointerException.class) Object obj2) { } +} \ No newline at end of file diff --git a/java/java-tests/testData/compiler/notNullVerification/annotations.jar b/java/java-tests/testData/compiler/notNullVerification/annotations.jar index 45860d829e91..d96bb1d6bed4 100644 Binary files a/java/java-tests/testData/compiler/notNullVerification/annotations.jar and b/java/java-tests/testData/compiler/notNullVerification/annotations.jar differ diff --git a/java/java-tests/testData/inspection/nullableProblems/NotNullCustomException.java b/java/java-tests/testData/inspection/nullableProblems/NotNullCustomException.java new file mode 100644 index 000000000000..42e2798ed1ab --- /dev/null +++ b/java/java-tests/testData/inspection/nullableProblems/NotNullCustomException.java @@ -0,0 +1,8 @@ +import org.jetbrains.annotations.*; + +class Test { + public void foo(@NotNull(exception = NullPointerException.class) String a) { } + public void foo2(@NotNull(exception = CustomException.class) String a) { } +} + +class CustomException extends Exception {} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/codeInspection/NullableStuffInspectionTest.java b/java/java-tests/testSrc/com/intellij/codeInspection/NullableStuffInspectionTest.java index 843dcbbb8bfe..648794e3003b 100644 --- a/java/java-tests/testSrc/com/intellij/codeInspection/NullableStuffInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInspection/NullableStuffInspectionTest.java @@ -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{ diff --git a/java/java-tests/testSrc/com/intellij/compiler/notNullVerification/NotNullVerifyingInstrumenterTest.java b/java/java-tests/testSrc/com/intellij/compiler/notNullVerification/NotNullVerifyingInstrumenterTest.java index 498a1172fa9a..e9d1da3aaf18 100644 --- a/java/java-tests/testSrc/com/intellij/compiler/notNullVerification/NotNullVerifyingInstrumenterTest.java +++ b/java/java-tests/testSrc/com/intellij/compiler/notNullVerification/NotNullVerifyingInstrumenterTest.java @@ -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. 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"); diff --git a/java/mockJDK-1.7/jre/lib/annotations.jar b/java/mockJDK-1.7/jre/lib/annotations.jar index 22b47ab9bcbc..d96bb1d6bed4 100644 Binary files a/java/mockJDK-1.7/jre/lib/annotations.jar and b/java/mockJDK-1.7/jre/lib/annotations.jar differ diff --git a/platform/annotations/src/org/jetbrains/annotations/NotNull.java b/platform/annotations/src/org/jetbrains/annotations/NotNull.java index aa7c78b3d225..3ad6003f554c 100644 --- a/platform/annotations/src/org/jetbrains/annotations/NotNull.java +++ b/platform/annotations/src/org/jetbrains/annotations/NotNull.java @@ -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 exception() default Exception.class; }