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}