From fd800a75379c292f6fef25e6169e2c3695b19f0f Mon Sep 17 00:00:00 2001 From: peter Date: Tue, 23 Jul 2013 11:59:44 +0200 Subject: [PATCH] add @Contract annotation (IDEA-93372) --- .../dataFlow/ControlFlowAnalyzer.java | 55 +++++++++++++++++++ .../dataFlow/fixture/ContractAnnotation.java | 21 +++++++ .../DataFlowInspectionTest.java | 7 +++ .../org/jetbrains/annotations/Contract.java | 51 +++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 java/java-tests/testData/inspection/dataFlow/fixture/ContractAnnotation.java create mode 100644 platform/annotations/src/org/jetbrains/annotations/Contract.java diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ControlFlowAnalyzer.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ControlFlowAnalyzer.java index 99edf0827b02..a9b8fc556fbd 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ControlFlowAnalyzer.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ControlFlowAnalyzer.java @@ -15,6 +15,7 @@ */ package com.intellij.codeInspection.dataFlow; +import com.intellij.codeInsight.AnnotationUtil; import com.intellij.codeInsight.ConditionCheckManager; import com.intellij.codeInsight.ConditionChecker; import com.intellij.codeInsight.ExceptionUtil; @@ -22,6 +23,8 @@ import com.intellij.codeInspection.dataFlow.instructions.*; import com.intellij.codeInspection.dataFlow.value.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; @@ -1373,6 +1376,27 @@ class ControlFlowAnalyzer extends JavaElementVisitor { private static MethodContract getCallContract(PsiMethodCallExpression expression) { PsiMethod resolved = expression.resolveMethod(); if (resolved != null) { + final PsiAnnotation contractAnno = AnnotationUtil.findAnnotation(resolved, "org.jetbrains.annotations.Contract"); + if (contractAnno != null) { + final Project project = expression.getProject(); + return CachedValuesManager.getManager(project).getCachedValue(contractAnno, new CachedValueProvider() { + @Nullable + @Override + public Result compute() { + PsiAnnotationMemberValue value = contractAnno.findAttributeValue(null); + Object text = JavaPsiFacade.getInstance(project).getConstantEvaluationHelper().computeConstantExpression(value); + if (text instanceof String) { + try { + return Result.create(parseContract((String)text), contractAnno); + } + catch (Exception ignored) { + } + } + return Result.create(null, contractAnno); + } + }); + } + @NonNls String methodName = resolved.getName(); PsiExpression[] params = expression.getArgumentList().getExpressions(); @@ -1438,6 +1462,37 @@ class ControlFlowAnalyzer extends JavaElementVisitor { return null; } + private static MethodContract parseContract(String text) throws ParseException { + text = StringUtil.replace(text, " ", ""); + String arrow = "->"; + int arrowIndex = text.indexOf(arrow); + if (arrowIndex < 0) throw new ParseException("A contract must be in form arg1, ..., argN -> return-value"); + String[] argStrings = text.substring(0, arrowIndex).split(","); + ValueConstraint[] args = new ValueConstraint[argStrings.length]; + for (int i = 0; i < args.length; i++) { + args[i] = parseConstraint(argStrings[i]); + } + return new MethodContract(args, parseConstraint(text.substring(arrowIndex + arrow.length()))); + } + + private static ValueConstraint parseConstraint(String name) throws ParseException { + if (StringUtil.isEmpty(name)) throw new ParseException("Constraint should not be empty"); + if ("null".equals(name)) return ValueConstraint.NULL_VALUE; + if ("!null".equals(name)) return ValueConstraint.NOT_NULL_VALUE; + if ("true".equals(name)) return ValueConstraint.TRUE_VALUE; + if ("false".equals(name)) return ValueConstraint.FALSE_VALUE; + if ("exit".equals(name)) return ValueConstraint.SYSTEM_EXIT; + if ("fail".equals(name)) return ValueConstraint.THROW_EXCEPTION; + if ("any".equals(name)) return ValueConstraint.ANY_VALUE; + throw new ParseException("Constraint should be one of: null, !null, true, false, exit, fail, any. Found: " + name); + } + + private static class ParseException extends Exception { + private ParseException(String message) { + super(message); + } + } + private static ValueConstraint[] getAnyArgConstraints(PsiExpression[] params) { ValueConstraint[] args = new ValueConstraint[params.length]; for (int i = 0; i < args.length; i++) { diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/ContractAnnotation.java b/java/java-tests/testData/inspection/dataFlow/fixture/ContractAnnotation.java new file mode 100644 index 000000000000..22866486e898 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/ContractAnnotation.java @@ -0,0 +1,21 @@ +import org.jetbrains.annotations.Contract; + +import java.lang.*; +import java.lang.IllegalArgumentException; + +public class AssertIsNotNull { + void bar() { + final Object o = call(); + assertIsNotNull(o); + if(o == null) {} + } + + @Contract("null -> fail") + static void assertIsNotNull(Object o) { + if (o == null) { + throw new IllegalArgumentException(); + } + } + + Object call() {return new Object();} +} diff --git a/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTest.java b/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTest.java index 3b28d240cde7..f8971a275f5e 100644 --- a/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTest.java @@ -31,6 +31,11 @@ import java.io.IOException; * @author peter */ public class DataFlowInspectionTest extends LightCodeInsightFixtureTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + myFixture.addClass("package org.jetbrains.annotations; public @interface Contract { String value(); }"); + } @Override protected String getTestDataPath() { @@ -266,4 +271,6 @@ public class DataFlowInspectionTest extends LightCodeInsightFixtureTestCase { myFixture.enableInspections(inspection); myFixture.testHighlighting(true, false, true, getTestName(false) + ".java"); } + + public void testContractAnnotation() { doTest(); } } diff --git a/platform/annotations/src/org/jetbrains/annotations/Contract.java b/platform/annotations/src/org/jetbrains/annotations/Contract.java new file mode 100644 index 000000000000..000428ba6145 --- /dev/null +++ b/platform/annotations/src/org/jetbrains/annotations/Contract.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.annotations; + +import java.lang.annotation.*; + +/** + * Specifies some aspects of the method behavior depending on the arguments. Can be used by tools for advanced data flow analysis.

+ * + * Method contract has the following syntax:
+ * contract ::= args '->' effect
+ * args ::= ((arg ,)* arg )?
+ * arg ::= value-constraint
+ * value-constraint ::= 'any' | 'null' | '!null' | 'false' | 'true'
+ * effect ::= value-constraint | 'fail' | 'exit'

+ * + * The constraints denote the following:
+ *

    + *
  • any - any value + *
  • null - null value + *
  • !null - a value statically proved to be not-null + *
  • true - true boolean value + *
  • false - false boolean value + *
  • fail - the method throws exception, if the arguments satisfy argument constraints + *
  • exit - the method terminates the current process, if the arguments satisfy argument constraints + *
+ * Examples:

+ * @Contract("any, null -> null") - method returns null if its second argument is null
+ * @Contract("true -> fail") - a typical assertFalse method which throws an exception if true is passed to it
+ * + * @author peter + */ +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface Contract { + String value(); +}