001package daikon.tools.jtb; 002 003import static java.nio.charset.StandardCharsets.UTF_8; 004import static java.util.logging.Level.FINE; 005import static java.util.logging.Level.INFO; 006 007import daikon.*; 008import gnu.getopt.*; 009import java.io.IOException; 010import java.io.PrintWriter; 011import java.io.Reader; 012import java.nio.file.Files; 013import java.nio.file.Paths; 014import java.util.Collections; 015import java.util.List; 016import java.util.Map; 017import java.util.logging.Logger; 018import jtb.JavaParser; 019import jtb.ParseException; 020import jtb.syntaxtree.*; 021import org.checkerframework.checker.nullness.qual.KeyFor; 022import org.checkerframework.checker.nullness.qual.Nullable; 023import org.plumelib.util.CollectionsPlume; 024import org.plumelib.util.StringsPlume; 025 026/** 027 * Create a splitter info file from Java source. 028 * 029 * <p>The argument is a list of {@code .java} files. The original {@code .java} files are left 030 * unmodified. A {@code .spinfo} file is written for every {@code .java} file, or in the single file 031 * indicated as the {@code -o} command-line argument.. 032 */ 033public class CreateSpinfo { 034 035 // The expressions in the Java source are extracted as follows: 036 // For each method: 037 // * extracts all expressions in conditional statements 038 // ie. if, for, which, etc. 039 // * if the method body is a one-line return statement, it 040 // extracts it for later substitution into expressions which 041 // call this function. These statements are referred to as 042 // replace statements 043 // For each field declaration 044 // * if the field is a boolean, it stores the expression 045 // "<fieldname> == true" as a splitting condition. 046 // 047 // The method printSpinfoFile prints out these expressions and 048 // replace statements in splitter info file format. 049 050 /** Debug logger. */ 051 public static final Logger debug = Logger.getLogger("daikon.tools.jtb.CreateSpinfo"); 052 053 /** The usage message for this program. */ 054 private static String usage = 055 StringsPlume.joinLines( 056 "Usage: java daikon.tools.CreateSpinfo FILE.java ...", 057 " -o outputfile Put all output in specified file", 058 " -h Display this usage message"); 059 060 public static void main(String[] args) throws IOException { 061 try { 062 mainHelper(args); 063 } catch (Daikon.DaikonTerminationException e) { 064 Daikon.handleDaikonTerminationException(e); 065 } 066 } 067 068 /** 069 * This does the work of {@link #main(String[])}, but it never calls System.exit, so it is 070 * appropriate to be called progrmmatically. 071 */ 072 public static void mainHelper(final String[] args) throws IOException { 073 074 // If not set, put output in files named after the input (source) files. 075 String outputfilename = null; 076 077 daikon.LogHelper.setupLogs(INFO); 078 LongOpt[] longopts = 079 new LongOpt[] { 080 new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), 081 new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), 082 new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), 083 }; 084 085 Getopt g = new Getopt("daikon.tools.jtb.CreateSpinfo", args, "ho:", longopts); 086 int c; 087 while ((c = g.getopt()) != -1) { 088 switch (c) { 089 case 0: 090 // got a long option 091 String option_name = longopts[g.getLongind()].getName(); 092 if (Daikon.help_SWITCH.equals(option_name)) { 093 System.out.println(usage); 094 throw new Daikon.NormalTermination(); 095 } else if (Daikon.debugAll_SWITCH.equals(option_name)) { 096 Global.debugAll = true; 097 } else if (Daikon.debug_SWITCH.equals(option_name)) { 098 daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); 099 } else { 100 throw new RuntimeException("Unknown long option received: " + option_name); 101 } 102 break; 103 case 'o': 104 outputfilename = Daikon.getOptarg(g); 105 break; 106 case 'h': 107 System.out.println(usage); 108 throw new Daikon.NormalTermination(); 109 case '?': 110 break; // getopt() already printed an error 111 default: 112 System.out.println("getopt() returned " + c); 113 break; 114 } 115 } 116 117 // The index of the first non-option argument -- the name of the file 118 int argindex = g.getOptind(); 119 if (argindex >= args.length) { 120 throw new Daikon.UserError( 121 "Error: No .java file arguments supplied." + Global.lineSep + usage); 122 } 123 if (outputfilename != null) { 124 try (PrintWriter output = 125 new PrintWriter(Files.newBufferedWriter(Paths.get(outputfilename), UTF_8))) { 126 for (; argindex < args.length; argindex++) { 127 String javaFileName = args[argindex]; 128 writeSplitters(javaFileName, output); 129 } 130 output.flush(); 131 } 132 } else { 133 for (; argindex < args.length; argindex++) { 134 String javaFileName = args[argindex]; 135 String spinfoFileName = spinfoFileName(javaFileName); 136 try (PrintWriter output = 137 new PrintWriter(Files.newBufferedWriter(Paths.get(spinfoFileName), UTF_8))) { 138 writeSplitters(javaFileName, output); 139 output.flush(); 140 } 141 } 142 } 143 } 144 145 /** 146 * Returns the default name for a spinfo file created from a java file named javaFileName. 147 * 148 * @param javaFileName the name of the java file from which this spinfo file is being created 149 */ 150 private static String spinfoFileName(String javaFileName) { 151 if (javaFileName.endsWith(".java")) { 152 return javaFileName.substring(0, javaFileName.length() - 5) + ".spinfo"; 153 } 154 155 // The file does not end with ".java". Proceed, but issue a warning. 156 System.err.println( 157 "Warning: CreateSpinfo input file " + javaFileName + "does not end in .java."); 158 159 // change the file extension to .spinfo 160 int dotPos = javaFileName.indexOf("."); 161 if (dotPos == -1) { 162 return javaFileName + ".spinfo"; 163 } else { 164 return javaFileName.substring(0, dotPos) + ".spinfo"; 165 } 166 } 167 168 /** 169 * Write splitters for the Java file to the PrintWriter as a spinfo file. 170 * 171 * @param javaFileName the name of the java file from which this spinfo file is being made 172 * @param output the PrintWriter to which this spinfo file is being wrote 173 * @throws IOException if there is a problem reading or writing files 174 */ 175 private static void writeSplitters(String javaFileName, PrintWriter output) throws IOException { 176 Node root; 177 try (Reader input = Files.newBufferedReader(Paths.get(javaFileName), UTF_8)) { 178 JavaParser parser = new JavaParser(input); 179 root = parser.CompilationUnit(); 180 } catch (ParseException e) { 181 e.printStackTrace(); 182 throw new Daikon.UserError("ParseException"); 183 } 184 debug.fine("CreateSpinfo: processing file " + javaFileName); 185 ConditionExtractor extractor = new ConditionExtractor(); 186 root.accept(extractor); 187 // conditions: method name (String) to conditional expressions (String) 188 Map<String, List<String>> conditions = extractor.getConditionMap(); 189 // replaceStatements: method declaration (String) to method body (String) 190 Map<String, String> replaceStatements = extractor.getReplaceStatements(); 191 String packageName = extractor.getPackageName(); 192 filterConditions(conditions); 193 addOrigConditions(conditions); 194 printSpinfoFile(output, conditions, replaceStatements, packageName); 195 } 196 197 /** 198 * Remove redundant and trivial conditions from conditionMap. Side-effects conditionMap. 199 * 200 * @param conditionMap the map from which to remove redundant and trivial conditions 201 */ 202 private static void filterConditions(Map<String, List<String>> conditionMap) { 203 for (Map.Entry<String, List<String>> entry : conditionMap.entrySet()) { 204 List<String> conditions = entry.getValue(); 205 conditions = CollectionsPlume.withoutDuplicates(conditions); 206 conditions.remove("true"); 207 conditions.remove("false"); 208 entry.setValue(conditions); 209 } 210 } 211 212 /** 213 * For each condition in conditionMap, an additional condition is added which is identical to the 214 * initial condition with the exception that it is prefixed with "orig(" and suffixed with ")". 215 */ 216 private static void addOrigConditions(Map<String, List<String>> conditionMap) { 217 for (List<String> conditions : conditionMap.values()) { 218 int size = conditions.size(); 219 for (int i = 0; i < size; i++) { 220 conditions.add(addOrig(conditions.get(i))); 221 } 222 } 223 } 224 225 /** Returns condition prefixed with "orig(" and suffixed with ")". */ 226 private static String addOrig(String condition) { 227 return "orig(" + condition + ")"; 228 } 229 230 /** 231 * Writes the spinfo file specified by conditions, replaceStatements, and package name to output. 232 * 233 * @param output the PrintWriter to which the spinfo file is to be written 234 * @param conditions the conditions to be included in the spinfo file. conditions should be a map 235 * from method names to the conditional expressions for that method to split upon. 236 * @param replaceStatements the replace statements to be included in the spinfo file. 237 * replaceStatements should be a map from method declarations to method bodies. 238 * @param packageName the package name of the java file for which this spinfo file is being 239 * written 240 */ 241 private static void printSpinfoFile( 242 PrintWriter output, 243 Map<String, List<String>> conditions, 244 Map<String, String> replaceStatements, 245 @Nullable String packageName) 246 throws IOException { 247 if (!replaceStatements.values().isEmpty()) { 248 output.println("REPLACE"); 249 for ( 250 @KeyFor("replaceStatements") String declaration : CollectionsPlume.sortedKeySet(replaceStatements)) { 251 output.println(declaration); 252 String replacement = replaceStatements.get(declaration); 253 output.println(removeNewlines(replacement)); 254 } 255 output.println(); 256 } 257 for (@KeyFor("conditions") String method : CollectionsPlume.sortedKeySet(conditions)) { 258 List<String> method_conds = conditions.get(method); 259 Collections.sort(method_conds); 260 if (method_conds.size() > 0) { 261 String qualifiedMethod = (packageName == null) ? method : packageName + "." + method; 262 output.println("PPT_NAME " + qualifiedMethod); 263 for (int i = 0; i < method_conds.size(); i++) { 264 String cond = removeNewlines(method_conds.get(i)); 265 if (!(cond.equals("true") || cond.equals("false"))) { 266 output.println(cond); 267 } 268 } 269 output.println(); 270 } 271 } 272 } 273 274 /** 275 * Returns target with line separators and the whitespace around a line separator replaced by a 276 * single space. 277 */ 278 private static String removeNewlines(String target) { 279 String[] lines = StringsPlume.splitLines(target); 280 for (int i = 0; i < lines.length; i++) { 281 lines[i] = lines[i].trim(); 282 } 283 return String.join(" ", lines); 284 } 285}