Main.java revision 2441c4b66f26bf96ea7a8749645a933aa09a2d62
1package annotator;
2
3import java.io.File;
4import java.io.FileNotFoundException;
5import java.io.FileOutputStream;
6import java.io.IOException;
7import java.io.OutputStream;
8import java.util.ArrayList;
9import java.util.Collections;
10import java.util.LinkedHashSet;
11import java.util.List;
12import java.util.Set;
13import java.util.TreeSet;
14import java.util.regex.Matcher;
15import java.util.regex.Pattern;
16
17import plume.FileIOException;
18import plume.Option;
19import plume.Options;
20import plume.Pair;
21import plume.UtilMDE;
22import annotator.Source.CompilerException;
23import annotator.find.Criteria;
24import annotator.find.Insertion;
25import annotator.find.TreeFinder;
26import annotator.specification.IndexFileSpecification;
27import annotator.specification.Specification;
28
29import com.google.common.collect.SetMultimap;
30import com.sun.source.tree.CompilationUnitTree;
31import com.sun.source.tree.Tree;
32import com.sun.source.util.TreePath;
33
34/**
35 * This is the main class for the annotator, which inserts annotations in
36 * Java source code.  You can call it as <tt>java annotator.Main</tt> or by
37 * using the shell script <tt>insert-annotations-to-source</tt>.
38 * <p>
39 *
40 * It takes as input
41 * <ul>
42 *   <li>annotation (index) files, which indicate the annotations to insert</li>
43 *   <li>Java source files, into which the annotator inserts annotations</li>
44 * </ul>
45 * Use the --help option for full usage details.
46 * <p>
47 *
48 * Annotations that are not for the specified Java files are ignored.
49 */
50public class Main {
51
52  /** Directory in which output files are written. */
53  @Option("-d <directory> Directory in which output files are written")
54  public static String outdir = "annotated/";
55
56  /**
57   * If true, overwrite original source files (making a backup first).
58   * Furthermore, if the backup files already exist, they are used instead
59   * of the .java files.  This behavior permits a user to tweak the .jaif
60   * file and re-run the annotator.
61   * <p>
62   *
63   * Note that if the user runs the annotator with --in-place, makes edits,
64   * and then re-runs the annotator with this --in-place option, those
65   * edits are lost.  Similarly, if the user runs the annotator twice in a
66   * row with --in-place, only the last set of annotations will appear in
67   * the codebase at the end.
68   * <p>
69   *
70   * To preserve changes when using the --in-place option, first remove the
71   * backup files.  Or, use the <tt>-d .</tt> option, which makes (and
72   * reads) no backup, instead of --in-place.
73   */
74  @Option("-i Overwrite original source files")
75  public static boolean in_place = false;
76
77  @Option("-h Print usage information and exit")
78  public static boolean help = false;
79
80  @Option("-a Abbreviate annotation names")
81  public static boolean abbreviate = true;
82
83  @Option("-c Insert annotations in comments")
84  public static boolean comments = false;
85
86  @Option("-o Omit given annotation")
87  public static String omit_annotation;
88
89  @Option("-v Verbose (print progress information)")
90  public static boolean verbose;
91
92  @Option("Debug (print debug information)")
93  public static boolean debug = false;
94
95  // Implementation details:
96  //  1. The annotator partially compiles source
97  //     files using the compiler API (JSR-199), obtaining an AST.
98  //  2. The annotator reads the specification file, producing a set of
99  //     annotator.find.Insertions.  Insertions completely specify what to
100  //     write (as a String, which is ultimately translated according to the
101  //     keyword file) and how to write it (as annotator.find.Criteria).
102  //  3. It then traverses the tree, looking for nodes that satisfy the
103  //     Insertion Criteria, translating the Insertion text against the
104  //     keyword file, and inserting the annotations into the source file.
105
106  /**
107   * Runs the annotator, parsing the source and spec files and applying
108   * the annotations.
109   */
110  public static void main(String[] args) {
111
112    if (verbose) {
113      System.out.printf("insert-annotations-to-source (%s)",
114                        annotations.io.classfile.ClassFileReader.INDEX_UTILS_VERSION);
115    }
116
117    Options options = new Options("Main [options] ann-file... java-file...", Main.class);
118    String[] file_args = options.parse_or_usage (args);
119
120    if (debug) {
121      TreeFinder.debug = true;
122      Criteria.debug = true;
123    }
124
125    if (help) {
126      options.print_usage();
127      System.exit(0);
128    }
129
130    if (in_place && outdir != "annotated/") { // interned
131      options.print_usage("The --outdir and --in-place options are mutually exclusive.");
132      System.exit(1);
133    }
134
135    if (file_args.length < 2) {
136      options.print_usage("Supplied %d arguments, at least 2 needed%n", file_args.length);
137      System.exit(1);
138    }
139
140    // The insertions specified by the annotation files.
141    List<Insertion> insertions = new ArrayList<Insertion>();
142    // The Java files into which to insert.
143    List<String> javafiles = new ArrayList<String>();
144
145    for (String arg : file_args) {
146      if (arg.endsWith(".java")) {
147        javafiles.add(arg);
148      } else if (arg.endsWith(".jaif") ||
149                 arg.endsWith(".jann")) {
150        try {
151          Specification spec = new IndexFileSpecification(arg);
152          List<Insertion> parsedSpec = spec.parse();
153          if (verbose || debug) {
154            System.out.printf("Read %d annotations from %s%n",
155                              parsedSpec.size(), arg);
156          }
157          if (omit_annotation != null) {
158            List<Insertion> filtered = new ArrayList<Insertion>(parsedSpec.size());
159            for (Insertion insertion : parsedSpec) {
160              // TODO: this won't omit annotations if the insertion is more than
161              // just the annotation (such as if the insertion is a cast
162              // insertion or a 'this' parameter in a method declaration).
163              if (! omit_annotation.equals(insertion.getText())) {
164                filtered.add(insertion);
165              }
166            }
167            parsedSpec = filtered;
168            if (verbose || debug) {
169              System.out.printf("After filtering: %d annotations from %s%n",
170                                parsedSpec.size(), arg);
171            }
172          }
173          insertions.addAll(parsedSpec);
174        } catch (RuntimeException e) {
175          if (e.getCause() != null
176              && e.getCause() instanceof FileNotFoundException) {
177            System.err.println("File not found: " + arg);
178            System.exit(1);
179          } else {
180            throw e;
181          }
182        } catch (FileIOException e) {
183          System.err.println("Error while parsing annotation file " + arg);
184          if (e.getMessage() != null) {
185            System.err.println(e.getMessage());
186          }
187          e.printStackTrace();
188          System.exit(1);
189        }
190      } else {
191        throw new Error("Unrecognized file extension: " + arg);
192      }
193    }
194
195    if (debug) {
196      System.out.printf("%d insertions, %d .java files%n", insertions.size(), javafiles.size());
197    }
198    if (debug) {
199      System.out.printf("Insertions:%n");
200      for (Insertion insertion : insertions) {
201        System.out.printf("  %s%n", insertion);
202      }
203    }
204
205    for (String javafilename : javafiles) {
206
207      if (verbose) {
208        System.out.println("Processing " + javafilename);
209      }
210
211      File javafile = new File(javafilename);
212
213      File outfile;
214      File unannotated = new File(javafilename + ".unannotated");
215      if (in_place) {
216        // It doesn't make sense to check timestamps;
217        // if the .java.unannotated file exists, then just use it.
218        // A user can rename that file back to just .java to cause the
219        // .java file to be read.
220        if (unannotated.exists()) {
221          if (verbose) {
222            System.out.printf("Renaming %s to %s%n", unannotated, javafile);
223          }
224          boolean success = unannotated.renameTo(javafile);
225          if (! success) {
226            throw new Error(String.format("Failed renaming %s to %s",
227                                          unannotated, javafile));
228          }
229        }
230        outfile = javafile;
231      } else {
232        String baseName;
233        if (javafile.isAbsolute()) {
234          baseName = javafile.getName();
235        } else {
236          baseName = javafile.getPath();
237        }
238        outfile = new File(outdir, baseName);
239      }
240
241      Set<String> imports = new LinkedHashSet<String>();
242
243      String fileLineSep = System.getProperty("line.separator");
244      Source src;
245      // Get the source file, and use it to obtain parse trees.
246      try {
247        // fileLineSep is set here so that exceptions can be caught
248        fileLineSep = UtilMDE.inferLineSeparator(javafilename);
249        src = new Source(javafilename);
250        if (verbose) {
251          System.out.printf("Parsed %s%n", javafilename);
252        }
253      } catch (CompilerException e) {
254        e.printStackTrace();
255        return;
256      } catch (IOException e) {
257        e.printStackTrace();
258        return;
259      }
260
261      int num_insertions = 0;
262
263      for (CompilationUnitTree tree : src.parse()) {
264
265        // Create a finder, and use it to get positions.
266        TreeFinder finder = new TreeFinder(tree);
267        if (debug) {
268          TreeFinder.debug = true;
269        }
270        SetMultimap<Integer, Insertion> positions = finder.getPositions(tree, insertions);
271
272        // Apply the positions to the source file.
273        if (debug || verbose) {
274          System.err.printf("getPositions returned %d positions in tree for %s%n", positions.size(), javafilename);
275        }
276
277        Set<Integer> positionKeysUnsorted = positions.keySet();
278        Set<Integer> positionKeysSorted = new TreeSet<Integer>(new TreeFinder.ReverseIntegerComparator());
279        positionKeysSorted.addAll(positionKeysUnsorted);
280        for (Integer pos : positionKeysSorted) {
281          List<Insertion> toInsertList = new ArrayList<Insertion>(positions.get(pos));
282          Collections.reverse(toInsertList);
283          if (debug) {
284            System.out.printf("insertion pos: %d%n", pos);
285          }
286          assert pos >= 0
287            : "pos is negative: " + pos + " " + toInsertList.get(0) + " " + javafilename;
288          for (Insertion iToInsert : toInsertList) {
289            String toInsert = iToInsert.getText(comments, abbreviate);
290            if (abbreviate) {
291              Set<String> packageNames = iToInsert.getPackageNames();
292              if (debug) {
293                System.out.printf("Need import %s%n  due to insertion %s%n",
294                                  packageNames, toInsert);
295              }
296              imports.addAll(packageNames);
297            }
298
299            // Possibly add whitespace after the insertion
300            boolean gotSeparateLine = false;
301            if (iToInsert.getSeparateLine()) {
302              // System.out.printf("getSeparateLine=true for insertion at pos %d: %s%n", pos, iToInsert);
303              int indentation = 0;
304              while ((pos - indentation != 0)
305                     // horizontal whitespace
306                     && (src.charAt(pos-indentation-1) == ' '
307                         || src.charAt(pos-indentation-1) == '\t')) {
308                // System.out.printf("src.charAt(pos-indentation-1 == %d-%d-1)='%s'%n",
309                //                   pos, indentation, src.charAt(pos-indentation-1));
310                indentation++;
311              }
312              if ((pos - indentation == 0)
313                  // horizontal whitespace
314                  || (src.charAt(pos-indentation-1) == '\f'
315                      || src.charAt(pos-indentation-1) == '\n'
316                      || src.charAt(pos-indentation-1) == '\r')) {
317                toInsert = toInsert + fileLineSep + src.substring(pos-indentation, pos);
318                gotSeparateLine = true;
319              }
320            }
321
322            // Possibly add a leading space before the insertion
323            if ((! gotSeparateLine) && (pos != 0)) {
324              char precedingChar = src.charAt(pos-1);
325              if (! (Character.isWhitespace(precedingChar)
326                     // No space if it's the first formal or generic parameter
327                     // or if it's a CloseParenthesesInsertion
328                     || precedingChar == '('
329                     || precedingChar == '<'
330                     || precedingChar == '['
331                     || toInsert.equals("))"))) {
332                toInsert = " " + toInsert;
333              }
334            }
335
336            // If it's already there, don't re-insert.  This is a hack!
337            // Also, I think this is already checked when constructing the
338            // insertions.
339            int precedingTextPos = pos-toInsert.length()-1;
340            if (precedingTextPos >= 0) {
341              String precedingTextPlusChar
342                = src.getString().substring(precedingTextPos, pos);
343              // System.out.println("Inserting " + toInsert + " at " + pos + " in code of length " + src.getString().length() + " with preceding text '" + precedingTextPlusChar + "'");
344              if (toInsert.equals(precedingTextPlusChar.substring(0, toInsert.length()))
345                  || toInsert.equals(precedingTextPlusChar.substring(1))) {
346                if (debug) {
347                    System.out.println("Inserting " + toInsert + " at " + pos + " in code of length " + src.getString().length() + " with preceding text '" + precedingTextPlusChar + "'");
348                    System.out.println("Already present, skipping");
349                }
350                continue;
351              }
352            }
353            // add trailing whitespace
354            // (test is not for "extends " because we just added a leading space, above)
355            if ((! gotSeparateLine) && (! toInsert.startsWith(" extends "))
356                    && (! toInsert.startsWith("((")) && (! toInsert.startsWith(" (("))
357                    && (! toInsert.equals("))")) && (! toInsert.endsWith(" this"))) {
358              toInsert = toInsert + " ";
359            }
360            src.insert(pos, toInsert);
361            if (verbose) {
362              System.out.print(".");
363              num_insertions++;
364              if ((num_insertions % 50) == 0) {
365                System.out.println();   // terminate the line that contains dots
366              }
367            }
368            if (debug) {
369              System.out.println("Post-insertion source: " + src.getString());
370            }
371          }
372        }
373      }
374      if (verbose) {
375        if ((num_insertions % 50) != 0) {
376          System.out.println();   // terminate the line that contains dots
377        }
378      }
379
380      // insert import statements
381      {
382        if (debug) {
383          System.out.println(imports.size() + " imports to insert");
384          for (String classname : imports) {
385            System.out.println("  " + classname);
386          }
387        }
388        Pattern importPattern = Pattern.compile("(?m)^import\\b");
389        Pattern packagePattern = Pattern.compile("(?m)^package\\b.*;(\\n|\\r\\n?)");
390        int importIndex = 0;      // default: beginning of file
391        String srcString = src.getString();
392        Matcher m;
393        m = importPattern.matcher(srcString);
394        if (m.find()) {
395          importIndex = m.start();
396        } else {
397          // if (debug) {
398          //   System.out.println("Didn't find import in " + srcString);
399          // }
400          m = packagePattern.matcher(srcString);
401          if (m.find()) {
402            importIndex = m.end();
403          }
404        }
405        for (String classname : imports) {
406          String toInsert = "import " + classname + ";" + fileLineSep;
407          src.insert(importIndex, toInsert);
408          importIndex += toInsert.length();
409        }
410      }
411
412      // Write the source file.
413      try {
414        if (in_place) {
415          if (verbose) {
416            System.out.printf("Renaming %s to %s%n", javafile, unannotated);
417          }
418          boolean success = javafile.renameTo(unannotated);
419          if (! success) {
420            throw new Error(String.format("Failed renaming %s to %s",
421                                          javafile, unannotated));
422          }
423        } else {
424          outfile.getParentFile().mkdirs();
425        }
426        OutputStream output = new FileOutputStream(outfile);
427        if (verbose) {
428          System.out.printf("Writing %s%n", outfile);
429        }
430        src.write(output);
431        output.close();
432      } catch (IOException e) {
433        System.err.println("Problem while writing file " + outfile);
434        e.printStackTrace();
435        System.exit(1);
436      }
437    }
438  }
439
440  ///
441  /// Utility methods
442  ///
443
444  public static String pathToString(TreePath path) {
445    if (path == null)
446      return "null";
447    return treeToString(path.getLeaf());
448  }
449
450  public static String treeToString(Tree node) {
451    String asString = node.toString();
452    String oneLine = firstLine(asString);
453    return "\"" + oneLine + "\"";
454  }
455
456  /**
457   * Return the first non-empty line of the string, adding an ellipsis
458   * (...) if the string was truncated.
459   */
460  public static String firstLine(String s) {
461    while (s.startsWith("\n")) {
462      s = s.substring(1);
463    }
464    int newlineIndex = s.indexOf('\n');
465    if (newlineIndex == -1) {
466      return s;
467    } else {
468      return s.substring(0, newlineIndex) + "...";
469    }
470  }
471
472  /**
473   * Separates the annotation class from its arguments.
474   *
475   * @return given <code>@foo(bar)</code> it returns the pair <code>{ @foo, (bar) }</code>.
476   */
477  public static Pair<String,String> removeArgs(String s) {
478    int pidx = s.indexOf("(");
479    return (pidx == -1) ?
480        Pair.of(s, (String)null) :
481        Pair.of(s.substring(0, pidx), s.substring(pidx));
482  }
483
484}
485