• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2021 Uber Technologies, Inc.
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in
12  * all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20  * THE SOFTWARE.
21  */
22 
23 package com.uber.nullaway.jmh;
24 
25 import java.io.File;
26 import java.io.IOException;
27 import java.net.URI;
28 import java.nio.charset.StandardCharsets;
29 import java.nio.file.Files;
30 import java.nio.file.Path;
31 import java.nio.file.Paths;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collections;
35 import java.util.List;
36 import javax.annotation.Nullable;
37 import javax.tools.DiagnosticListener;
38 import javax.tools.JavaCompiler;
39 import javax.tools.JavaFileObject;
40 import javax.tools.SimpleJavaFileObject;
41 import javax.tools.StandardJavaFileManager;
42 import javax.tools.ToolProvider;
43 
44 /**
45  * Code to run Javac with NullAway enabled, designed to aid benchmarking. Construction of {@code
46  * NullawayJavac} objects performs one-time operations whose cost we do not care to benchmark, so
47  * that {@link #compile()} can be run repeatedly to measure performance in the steady state.
48  */
49 public class NullawayJavac {
50 
51   //////////////////////
52   // state required to run javac via the standard APIs
53   //////////////////////
54   private List<JavaFileObject> compilationUnits;
55   private JavaCompiler compiler;
56   @Nullable private DiagnosticListener<JavaFileObject> diagnosticListener;
57   private StandardJavaFileManager fileManager;
58   private List<String> options;
59 
60   /**
61    * Sets up compilation for a simple single source file, for testing / sanity checking purposes.
62    * Running {@link #compile()} on the resulting object will return {@code false}, as the sample
63    * input source has NullAway errors.
64    *
65    * @throws IOException if output temporary directory cannot be created
66    */
createSimpleTest()67   public static NullawayJavac createSimpleTest() throws IOException {
68     String testClass =
69         "package com.uber;\n"
70             + "import java.util.*;\n"
71             + "class Test {   \n"
72             + "  public static void main(String args[]) {\n"
73             + "    Set<Short> s = null;\n"
74             + "    for (short i = 0; i < 100; i++) {\n"
75             + "      s.add(i);\n"
76             + "      s.remove(i - 1);\n"
77             + "    }\n"
78             + "    System.out.println(s.size());"
79             + "  }\n"
80             + "}\n";
81     return new NullawayJavac(
82         Collections.singletonList(new JavaSourceFromString("Test", testClass)),
83         "com.uber",
84         null,
85         Collections.emptyList(),
86         "");
87   }
88 
89   /**
90    * Creates a NullawayJavac object to compile a set of source files.
91    *
92    * @param sourceFileNames absolute paths to the source files to be compiled
93    * @param annotatedPackages argument to pass for "-XepOpt:NullAway:AnnotatedPackages" option
94    * @param classpath classpath for the benchmark
95    * @param extraErrorProneArgs extra arguments to pass to Error Prone
96    * @param extraProcessorPath additional elements to concatenate to the processor path
97    * @throws IOException if a temporary output directory cannot be created
98    */
create( List<String> sourceFileNames, String annotatedPackages, String classpath, List<String> extraErrorProneArgs, String extraProcessorPath)99   public static NullawayJavac create(
100       List<String> sourceFileNames,
101       String annotatedPackages,
102       String classpath,
103       List<String> extraErrorProneArgs,
104       String extraProcessorPath)
105       throws IOException {
106     List<JavaFileObject> compilationUnits = new ArrayList<>();
107     for (String sourceFileName : sourceFileNames) {
108       // we read every source file into memory in the prepare phase, to avoid some I/O during
109       // compilations
110       String content = readFile(sourceFileName);
111       String classname =
112           sourceFileName.substring(
113               sourceFileName.lastIndexOf(File.separatorChar) + 1, sourceFileName.indexOf(".java"));
114       compilationUnits.add(new JavaSourceFromString(classname, content));
115     }
116 
117     return new NullawayJavac(
118         compilationUnits, annotatedPackages, classpath, extraErrorProneArgs, extraProcessorPath);
119   }
120 
121   /**
122    * Create a NullawayJavac object to compile a single source file given as a String. This only
123    * supports cases where no additional classpath, Error Prone arguments, or processor path
124    * arguments need to be specified.
125    *
126    * @param className name of the class to compile
127    * @param source source code of the class to compile
128    * @param annotatedPackages argument to pass for "-XepOpt:NullAway:AnnotatedPackages" option
129    * @throws IOException if a temporary output directory cannot be created
130    */
createFromSourceString( String className, String source, String annotatedPackages)131   public static NullawayJavac createFromSourceString(
132       String className, String source, String annotatedPackages) throws IOException {
133     return new NullawayJavac(
134         Collections.singletonList(new JavaSourceFromString(className, source)),
135         annotatedPackages,
136         null,
137         Collections.emptyList(),
138         "");
139   }
140 
141   /**
142    * Configures compilation with javac and NullAway.
143    *
144    * <p>To pass NullAway in the {@code -processorpath} argument to the spawned javac and ensure it
145    * gets JIT-compiled during benchmarking, we make this project depend on NullAway and Error Prone
146    * Core, and then pass our own classpath as the processorpath. Note that this makes (dependencies
147    * of) NullAway and Error Prone visible on the <emph>classpath</emph> for the spawned javac
148    * instance as well. Note that this could lead to problems for benchmarks that depend on a
149    * conflicting version of a library that NullAway depends on.
150    *
151    * @param compilationUnits input sources to be compiled
152    * @param annotatedPackages argument to pass for "-XepOpt:NullAway:AnnotatedPackages" option
153    * @param classpath classpath for the program to be compiled
154    * @param extraErrorProneArgs additional arguments to pass to Error Prone
155    * @param extraProcessorPath additional elements to concatenate to the processor path
156    * @throws IOException if a temporary output directory cannot be created
157    */
NullawayJavac( List<JavaFileObject> compilationUnits, String annotatedPackages, @Nullable String classpath, List<String> extraErrorProneArgs, String extraProcessorPath)158   private NullawayJavac(
159       List<JavaFileObject> compilationUnits,
160       String annotatedPackages,
161       @Nullable String classpath,
162       List<String> extraErrorProneArgs,
163       String extraProcessorPath)
164       throws IOException {
165     this.compilationUnits = compilationUnits;
166     this.compiler = ToolProvider.getSystemJavaCompiler();
167     this.diagnosticListener =
168         diagnostic -> {
169           // do nothing
170         };
171     // uncomment this if you want to see compile errors get printed out
172     // this.diagnosticListener = null;
173     this.fileManager = compiler.getStandardFileManager(diagnosticListener, null, null);
174     Path outputDir = Files.createTempDirectory("classes");
175     outputDir.toFile().deleteOnExit();
176     this.options = new ArrayList<>();
177     if (classpath != null) {
178       options.addAll(Arrays.asList("-classpath", classpath));
179     }
180     String processorPath =
181         System.getProperty("java.class.path") + File.pathSeparator + extraProcessorPath;
182     options.addAll(
183         Arrays.asList(
184             "-processorpath",
185             processorPath,
186             "-d",
187             outputDir.toAbsolutePath().toString(),
188             "-XDcompilePolicy=simple",
189             "-Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages="
190                 + annotatedPackages
191                 + String.join(" ", extraErrorProneArgs)));
192     // add these options since we have at least one benchmark that only compiles with access to
193     // javac-internal APIs
194     options.addAll(
195         Arrays.asList(
196             "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
197             "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
198             "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
199             "--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
200             "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
201             "--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
202             "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
203             "--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
204             "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
205             "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
206             "--add-exports=jdk.compiler/com.sun.source.tree=ALL-UNNAMED"));
207   }
208 
209   /**
210    * Runs the compilation.
211    *
212    * @return true if the input files compile without error; false otherwise
213    */
compile()214   public boolean compile() {
215     JavaCompiler.CompilationTask task =
216         compiler.getTask(null, fileManager, diagnosticListener, options, null, compilationUnits);
217     return task.call();
218   }
219 
readFile(String path)220   private static String readFile(String path) throws IOException {
221     byte[] encoded = Files.readAllBytes(Paths.get(path));
222     return new String(encoded, StandardCharsets.UTF_8);
223   }
224 
225   /**
226    * This class allows code to be generated directly from a String, instead of having to be on disk.
227    *
228    * <p>Based on code in Apache Pig; see <a
229    * href="https://github.com/apache/pig/blob/59ec4a326079c9f937a052194405415b1e3a2b06/src/org/apache/pig/impl/util/JavaCompilerHelper.java#L42-L58">here</a>.
230    */
231   private static class JavaSourceFromString extends SimpleJavaFileObject {
232     final String code;
233 
JavaSourceFromString(String name, String code)234     JavaSourceFromString(String name, String code) {
235       super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
236       this.code = code;
237     }
238 
239     @Override
getCharContent(boolean ignoreEncodingErrors)240     public CharSequence getCharContent(boolean ignoreEncodingErrors) {
241       return code;
242     }
243   }
244 }
245