1 /* 2 * Copyright (c) 2022 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.fixserialization; 24 25 import com.sun.tools.javac.code.Symbol; 26 import com.uber.nullaway.ErrorMessage; 27 import com.uber.nullaway.fixserialization.adapters.SerializationAdapter; 28 import com.uber.nullaway.fixserialization.out.ErrorInfo; 29 import com.uber.nullaway.fixserialization.out.FieldInitializationInfo; 30 import com.uber.nullaway.fixserialization.out.SuggestedNullableFixInfo; 31 import java.io.FileOutputStream; 32 import java.io.IOException; 33 import java.io.OutputStream; 34 import java.io.Writer; 35 import java.net.URI; 36 import java.nio.charset.Charset; 37 import java.nio.file.Files; 38 import java.nio.file.Path; 39 import java.nio.file.Paths; 40 import javax.annotation.Nullable; 41 42 /** 43 * Serializer class where all generated files in Fix Serialization package is created through APIs 44 * of this class. 45 */ 46 public class Serializer { 47 /** Path to write errors. */ 48 private final Path errorOutputPath; 49 50 /** Path to write suggested fix metadata. */ 51 private final Path suggestedFixesOutputPath; 52 53 /** Path to write suggested fix metadata. */ 54 private final Path fieldInitializationOutputPath; 55 56 /** 57 * Adapter used to serialize outputs. This adapter is capable of serializing outputs according to 58 * the requested serilization version and maintaining backward compatibility with previous 59 * versions of NullAway. 60 */ 61 private final SerializationAdapter serializationAdapter; 62 Serializer(FixSerializationConfig config, SerializationAdapter serializationAdapter)63 public Serializer(FixSerializationConfig config, SerializationAdapter serializationAdapter) { 64 String outputDirectory = config.outputDirectory; 65 this.errorOutputPath = Paths.get(outputDirectory, "errors.tsv"); 66 this.suggestedFixesOutputPath = Paths.get(outputDirectory, "fixes.tsv"); 67 this.fieldInitializationOutputPath = Paths.get(outputDirectory, "field_init.tsv"); 68 this.serializationAdapter = serializationAdapter; 69 serializeVersion(outputDirectory); 70 initializeOutputFiles(config); 71 } 72 73 /** 74 * Appends the string representation of the {@link SuggestedNullableFixInfo}. 75 * 76 * @param suggestedNullableFixInfo SuggestedFixInfo object. 77 * @param enclosing Flag to control if enclosing method and class should be included. 78 */ serializeSuggestedFixInfo( SuggestedNullableFixInfo suggestedNullableFixInfo, boolean enclosing)79 public void serializeSuggestedFixInfo( 80 SuggestedNullableFixInfo suggestedNullableFixInfo, boolean enclosing) { 81 if (enclosing) { 82 suggestedNullableFixInfo.initEnclosing(); 83 } 84 appendToFile( 85 suggestedNullableFixInfo.tabSeparatedToString(serializationAdapter), 86 suggestedFixesOutputPath); 87 } 88 89 /** 90 * Appends the string representation of the {@link ErrorMessage}. 91 * 92 * @param errorInfo ErrorMessage object. 93 */ serializeErrorInfo(ErrorInfo errorInfo)94 public void serializeErrorInfo(ErrorInfo errorInfo) { 95 errorInfo.initEnclosing(); 96 appendToFile(serializationAdapter.serializeError(errorInfo), errorOutputPath); 97 } 98 serializeFieldInitializationInfo(FieldInitializationInfo info)99 public void serializeFieldInitializationInfo(FieldInitializationInfo info) { 100 appendToFile(info.tabSeparatedToString(serializationAdapter), fieldInitializationOutputPath); 101 } 102 103 /** Cleared the content of the file if exists and writes the header in the first line. */ initializeFile(Path path, String header)104 private void initializeFile(Path path, String header) { 105 try { 106 Files.deleteIfExists(path); 107 } catch (IOException e) { 108 throw new RuntimeException("Could not clear file at: " + path, e); 109 } 110 try (OutputStream os = new FileOutputStream(path.toFile())) { 111 header += "\n"; 112 os.write(header.getBytes(Charset.defaultCharset()), 0, header.length()); 113 os.flush(); 114 } catch (IOException e) { 115 throw new RuntimeException("Could not finish resetting File at Path: " + path, e); 116 } 117 } 118 119 /** 120 * Returns the serialization version. 121 * 122 * @return The serialization version. 123 */ getSerializationVersion()124 public int getSerializationVersion() { 125 return serializationAdapter.getSerializationVersion(); 126 } 127 128 /** 129 * Serializes the using {@link SerializationAdapter} version as {@code string} in 130 * <b>serialization_version.txt</b> file under root output directory for all serialized outputs. 131 * 132 * @param outputDirectory Path to root directory for all serialized outputs. 133 */ serializeVersion(@ullable String outputDirectory)134 private void serializeVersion(@Nullable String outputDirectory) { 135 Path versionOutputPath = Paths.get(outputDirectory).resolve("serialization_version.txt"); 136 try (Writer fileWriter = 137 Files.newBufferedWriter(versionOutputPath.toFile().toPath(), Charset.defaultCharset())) { 138 fileWriter.write(Integer.toString(serializationAdapter.getSerializationVersion())); 139 } catch (IOException exception) { 140 throw new RuntimeException("Could not serialize output version", exception); 141 } 142 } 143 144 /** Initializes every file which will be re-generated in the new run of NullAway. */ initializeOutputFiles(FixSerializationConfig config)145 private void initializeOutputFiles(FixSerializationConfig config) { 146 try { 147 Files.createDirectories(Paths.get(config.outputDirectory)); 148 if (config.suggestEnabled) { 149 initializeFile(suggestedFixesOutputPath, SuggestedNullableFixInfo.header()); 150 } 151 if (config.fieldInitInfoEnabled) { 152 initializeFile(fieldInitializationOutputPath, FieldInitializationInfo.header()); 153 } 154 initializeFile(errorOutputPath, serializationAdapter.getErrorsOutputFileHeader()); 155 } catch (IOException e) { 156 throw new RuntimeException("Could not finish resetting serializer", e); 157 } 158 } 159 appendToFile(String row, Path path)160 private void appendToFile(String row, Path path) { 161 // Since there is no method available in API of either javac or errorprone to inform NullAway 162 // that the analysis is finished, we cannot open a single stream and flush it within a finalize 163 // method. Must open and close a new stream everytime we are appending a new line to a file. 164 if (row == null || row.equals("")) { 165 return; 166 } 167 row = row + "\n"; 168 try (OutputStream os = new FileOutputStream(path.toFile(), true)) { 169 os.write(row.getBytes(Charset.defaultCharset()), 0, row.length()); 170 os.flush(); 171 } catch (IOException e) { 172 throw new RuntimeException("Error happened for writing at file: " + path, e); 173 } 174 } 175 176 /** 177 * Converts the given uri to the real path. Note, in NullAway CI tests, source files exists in 178 * memory and there is no real path leading to those files. Instead, we just serialize the path 179 * from uri as the full paths are not checked in tests. 180 * 181 * @param uri Given uri. 182 * @return Real path for the give uri. 183 */ 184 @Nullable pathToSourceFileFromURI(@ullable URI uri)185 public static Path pathToSourceFileFromURI(@Nullable URI uri) { 186 if (uri == null) { 187 return null; 188 } 189 if ("jimfs".equals(uri.getScheme())) { 190 // In NullAway unit tests, files are stored in memory and have this scheme. 191 return Paths.get(uri); 192 } 193 if (!"file".equals(uri.getScheme())) { 194 return null; 195 } 196 Path path = Paths.get(uri); 197 try { 198 return path.toRealPath(); 199 } catch (IOException e) { 200 // In this case, we still would like to continue the serialization instead of returning null 201 // and not serializing anything. 202 return path; 203 } 204 } 205 206 /** 207 * Serializes the given {@link Symbol} to a string. 208 * 209 * @param symbol The symbol to serialize. 210 * @param adapter adapter used to serialize symbols. 211 * @return The serialized symbol. 212 */ serializeSymbol(@ullable Symbol symbol, SerializationAdapter adapter)213 public static String serializeSymbol(@Nullable Symbol symbol, SerializationAdapter adapter) { 214 if (symbol == null) { 215 return "null"; 216 } 217 switch (symbol.getKind()) { 218 case FIELD: 219 case PARAMETER: 220 return symbol.name.toString(); 221 case METHOD: 222 case CONSTRUCTOR: 223 return adapter.serializeMethodSignature((Symbol.MethodSymbol) symbol); 224 default: 225 return symbol.flatName().toString(); 226 } 227 } 228 } 229