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