diff --git a/infer/annotations/com/facebook/infer/annotprocess/AnnotationProcessor.java b/infer/annotations/com/facebook/infer/annotprocess/AnnotationProcessor.java index 6d1bc165d..32584a988 100644 --- a/infer/annotations/com/facebook/infer/annotprocess/AnnotationProcessor.java +++ b/infer/annotations/com/facebook/infer/annotprocess/AnnotationProcessor.java @@ -26,18 +26,60 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; @SupportedAnnotationTypes({ "java.lang.SuppressWarnings" }) public class AnnotationProcessor extends AbstractProcessor { private static final String ANNOTATION_ENV_VAR = "INFER_ANNOTATIONS_OUT"; - private String mOutputFilename = "suppress_warnings_map.txt"; + private String mOutputFilename; // map of (classes -> methods in class). an empty set means suppress all // warnings on class public Map> mSuppressMap = new LinkedHashMap>(); + // total number of classes/methods with a SuppressWarnings annotation + private int mNumToSuppress = 0; + + // print a comma between all objects except for the last one + private void outputCommaIfNotLast(PrintWriter out, int elemCount) { + if (elemCount == mNumToSuppress) { + out.println(""); + } else { + out.println(","); + } + } + + // output a method to suppress in JSON format + // clearly, we should be using an existing JSON output library like Jackson here. however, we + // can't do this because we do not want to add Jackson (or another JSON library) to the classpath + // of the Java program we are building (along with the JAR for this processor). the reason is that + // if there is a different version of the JSON parser library somewhere in the classpath for the + // project, it could cause *very* strange problems. instead, we rolled our own library to avoid + // introducing additional dependencies + private void outputMethod(PrintWriter out, String clazz, String method, int elemCount) { + String TAB1 = " "; + String TAB2 = TAB1 + TAB1; + out.println(TAB1 + "{"); + out.println(TAB2 +"\"language\": \"Java\","); + out.print(TAB2 + "\"class\": \"" + clazz + "\""); + if (method != null) { + out.println(","); + out.println(TAB2 + "\"method\": \"" + method + "\""); + } else { + out.println(); + } + out.print(TAB1 + "}"); + outputCommaIfNotLast(out, elemCount); + } + + // output a class to suppress in JSON format + private void outputClass(PrintWriter out, String clazz, int elemCount) { + outputMethod(out, clazz, null, elemCount); + } + + // write the methods/classes to suppress to .inferconfig-style JSON private void exportSuppressMap() throws FileNotFoundException, IOException { Map env = System.getenv(); if (env.get(ANNOTATION_ENV_VAR) == null) { @@ -45,42 +87,66 @@ public class AnnotationProcessor extends AbstractProcessor { } else { mOutputFilename = env.get(ANNOTATION_ENV_VAR); } + + // output .inferconfig format file in JSON try (PrintWriter out = new PrintWriter(mOutputFilename)) { + int elemCount = 0; + out.println("{ \"suppress_procedures\": ["); for (Map.Entry> entry : mSuppressMap.entrySet()) { - out.write(entry.getKey() + " " + entry.getValue()); + String clazz = entry.getKey(); + Set methods = entry.getValue(); + if (methods.isEmpty()) { // empty set of methods means annotation is on class + outputClass(out, clazz, ++elemCount); + } else { + for (String method : methods) { + outputMethod(out, clazz, method, ++elemCount); + } + } } + out.println("] }"); } } + // we only care about @SuppressWarnings("null") and @SuppressWarnings("infer"); ignore otherwise + private boolean shouldProcess(SuppressWarnings annot) { + for (String value : annot.value()) { + if (value.equals("null") || value.equals("infer")) return true; + } + return false; + } + + // collect all of the SuppressWarnings annotations from the Java source files being compiled public boolean process(Set annotations, RoundEnvironment env) { + Elements elements = processingEnv.getElementUtils(); for (TypeElement te : annotations) { for (Element e : env.getElementsAnnotatedWith(te)) { - if (e instanceof TypeElement) { // class - String className = ((TypeElement) e).getQualifiedName().toString(); - mSuppressMap.put(className, Collections.EMPTY_SET); - } else if (e instanceof ExecutableElement) { // method - - String classname = e.getEnclosingElement().toString(); - java.util.Set suppressMethods = mSuppressMap.get(classname); - if (suppressMethods != null && suppressMethods.isEmpty()) { - // empty set means suppress warnings on all methods in class; do - // nothing - continue; + SuppressWarnings annot = e.getAnnotation(SuppressWarnings.class); + if (shouldProcess(annot)) { + if (e instanceof TypeElement) { // class + String className = elements.getBinaryName((TypeElement) e).toString(); + mSuppressMap.put(className, Collections.EMPTY_SET); + mNumToSuppress++; + } else if (e instanceof ExecutableElement) { // method + String classname = e.getEnclosingElement().toString(); + java.util.Set suppressMethods = mSuppressMap.get(classname); + if (suppressMethods != null && suppressMethods.isEmpty()) { + // empty set means suppress warnings on all methods in class; do nothing + continue; + } + if (suppressMethods == null) { + suppressMethods = new LinkedHashSet(); + } + suppressMethods.add(e.getSimpleName().toString()); + mSuppressMap.put(classname, suppressMethods); + mNumToSuppress++; } - if (suppressMethods == null) { - suppressMethods = new LinkedHashSet(); - } - suppressMethods.add(e.toString()); - mSuppressMap.put(classname, suppressMethods); } } } - if (env.processingOver()) { + if (env.processingOver() && mNumToSuppress > 0) { try { exportSuppressMap(); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/infer/bin/inferlib.py b/infer/bin/inferlib.py index c8f8f788c..fe148e5f0 100644 --- a/infer/bin/inferlib.py +++ b/infer/bin/inferlib.py @@ -214,7 +214,7 @@ def create_results_dir(results_dir): mkdir_if_not_exists(os.path.join(results_dir, 'sources')) -def clean_infer_out(infer_out): +def clean(infer_out, annotations_out): directories = ['multicore', 'classnames', 'sources', jwlib.FILELISTS] extensions = ['.cfg', '.cg'] @@ -230,6 +230,8 @@ def clean_infer_out(infer_out): path = os.path.join(root, f) os.remove(path) + os.remove(annotations_out) + def help_exit(message): print(message) @@ -586,7 +588,7 @@ class Infer: exit_status += make_status if self.args.buck and exit_status == os.EX_OK: - clean_infer_out(self.args.infer_out) + clean(self.args.infer_out, self.javac.annotations_out) return exit_status @@ -620,7 +622,6 @@ class Infer: if self.javac.annotations_out is not None: infer_print_options += [ '-local_config', self.javac.annotations_out] - exit_status = subprocess.check_call( infer_print_cmd + infer_print_options ) @@ -666,7 +667,6 @@ class Infer: def close(self): if self.args.analyzer != COMPILE: os.remove(self.javac.verbose_out) - os.remove(self.javac.annotations_out) def analyze_and_report(self): if self.args.analyzer not in [COMPILE, CAPTURE]: diff --git a/infer/tests/codetoanalyze/java/eradicate/SuppressWarningsExample.java b/infer/tests/codetoanalyze/java/eradicate/SuppressWarningsExample.java new file mode 100644 index 000000000..f7bf36e9e --- /dev/null +++ b/infer/tests/codetoanalyze/java/eradicate/SuppressWarningsExample.java @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2015 - present Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD style license found in the +* LICENSE file in the root directory of this source tree. An additional grant +* of patent rights can be found in the PATENTS file in the same directory. +*/ + +package codetoanalyze.java.eradicate; + +import java.lang.SuppressWarnings; + +import javax.annotation.Nullable; + +public class SuppressWarningsExample { + + @SuppressWarnings("null") + static class DoSuppress { + + @Nullable Object mFld; + + void doSuppress1(@Nullable Object o) { + o.toString(); + } + + void doSuppress2() { + mFld.toString(); + } + } + + @SuppressWarnings("null") + public void doSuppress(@Nullable Object o) { + o.toString(); + } + + @SuppressWarnings("infer") + public void doSuppressInferAnnot(@Nullable Object o) { + o.toString(); + } + + @SuppressWarnings("unchecked") + public void doNotSuppressWrongAnnot(@Nullable Object o) { + o.toString(); + } + + public void doNotSuppressNoAnnot(@Nullable Object o) { + o.toString(); + } + +} diff --git a/infer/tests/endtoend/java/eradicate/SuppressWarningsTest.java b/infer/tests/endtoend/java/eradicate/SuppressWarningsTest.java new file mode 100644 index 000000000..a3111ab53 --- /dev/null +++ b/infer/tests/endtoend/java/eradicate/SuppressWarningsTest.java @@ -0,0 +1,56 @@ +/* +* Copyright (c) 2015 - present Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD style license found in the +* LICENSE file in the root directory of this source tree. An additional grant +* of patent rights can be found in the PATENTS file in the same directory. +*/ + +package endtoend.java.eradicate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static utils.matchers.ResultContainsNoErrorInMethod.doesNotContain; +import static utils.matchers.ResultContainsExactly.containsExactly; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; + +import utils.InferException; +import utils.InferResults; + +public class SuppressWarningsTest { + + public static final String SOURCE_FILE = + "infer/tests/codetoanalyze/java/eradicate/SuppressWarningsExample.java"; + + public static final String NULL_METHOD_CALL = "ERADICATE_NULL_METHOD_CALL"; + + private static InferResults inferResults; + + @BeforeClass + public static void loadResults() throws InterruptedException, IOException { + inferResults = InferResults.loadEradicateResults( + SuppressWarningsTest.class, + SOURCE_FILE); + } + + @Test + public void matchErrors() + throws IOException, InterruptedException, InferException { + String[] methods = { + "doNotSuppressWrongAnnot", + "doNotSuppressNoAnnot" + }; + assertThat( + "Results should contain " + NULL_METHOD_CALL, + inferResults, + containsExactly( + NULL_METHOD_CALL, + SOURCE_FILE, + methods)); + } + +}