Main.java revision 553a9a9a17ce582f701841b59edfadb491ed54ad
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.Collection;
10import java.util.Collections;
11import java.util.Comparator;
12import java.util.HashMap;
13import java.util.LinkedHashSet;
14import java.util.List;
15import java.util.Map;
16import java.util.Set;
17import java.util.TreeSet;
18import java.util.regex.Matcher;
19import java.util.regex.Pattern;
20
21import plume.FileIOException;
22import plume.Option;
23import plume.OptionGroup;
24import plume.Options;
25import plume.Pair;
26import plume.UtilMDE;
27import type.Type;
28import annotations.Annotation;
29import annotations.el.ABlock;
30import annotations.el.AClass;
31import annotations.el.ADeclaration;
32import annotations.el.AElement;
33import annotations.el.AExpression;
34import annotations.el.AField;
35import annotations.el.AMethod;
36import annotations.el.AScene;
37import annotations.el.ATypeElement;
38import annotations.el.ATypeElementWithType;
39import annotations.el.AnnotationDef;
40import annotations.el.DefException;
41import annotations.el.ElementVisitor;
42import annotations.el.LocalLocation;
43import annotations.io.ASTIndex;
44import annotations.io.ASTPath;
45import annotations.io.ASTRecord;
46import annotations.io.DebugWriter;
47import annotations.io.IndexFileParser;
48import annotations.io.IndexFileWriter;
49import annotations.util.coll.VivifyingMap;
50import annotator.find.AnnotationInsertion;
51import annotator.find.CastInsertion;
52import annotator.find.ConstructorInsertion;
53import annotator.find.Criteria;
54import annotator.find.GenericArrayLocationCriterion;
55import annotator.find.Insertion;
56import annotator.find.Insertions;
57import annotator.find.NewInsertion;
58import annotator.find.ReceiverInsertion;
59import annotator.find.TreeFinder;
60import annotator.find.TypedInsertion;
61import annotator.scanner.LocalVariableScanner;
62import annotator.specification.IndexFileSpecification;
63
64import com.google.common.collect.LinkedHashMultimap;
65import com.google.common.collect.Multimap;
66import com.google.common.collect.SetMultimap;
67import com.sun.source.tree.CompilationUnitTree;
68import com.sun.source.tree.ExpressionTree;
69import com.sun.source.tree.Tree;
70import com.sun.source.util.TreePath;
71import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry;
72import com.sun.tools.javac.main.CommandLine;
73import com.sun.tools.javac.tree.JCTree;
74
75/**
76 * This is the main class for the annotator, which inserts annotations in
77 * Java source code.  You can call it as <tt>java annotator.Main</tt> or by
78 * using the shell script <tt>insert-annotations-to-source</tt>.
79 * <p>
80 *
81 * It takes as input
82 * <ul>
83 *   <li>annotation (index) files, which indicate the annotations to insert</li>
84 *   <li>Java source files, into which the annotator inserts annotations</li>
85 * </ul>
86 * Annotations that are not for the specified Java files are ignored.
87 * <p>
88 *
89 * The <a name="command-line-options">command-line options</a> are as follows:
90 * <!-- start options doc (DO NOT EDIT BY HAND) -->
91 * <ul>
92 *   <li id="optiongroup:General-options">General options
93 *     <ul>
94 *       <li id="option:outdir"><b>-d</b> <b>--outdir=</b><i>directory</i>. Directory in which output files are written. [default annotated/]</li>
95 *       <li id="option:in-place"><b>-i</b> <b>--in-place=</b><i>boolean</i>. If true, overwrite original source files (making a backup first).
96 *  Furthermore, if the backup files already exist, they are used instead
97 *  of the .java files.  This behavior permits a user to tweak the .jaif
98 *  file and re-run the annotator.
99 *  <p>
100 *
101 *  Note that if the user runs the annotator with --in-place, makes edits,
102 *  and then re-runs the annotator with this --in-place option, those
103 *  edits are lost.  Similarly, if the user runs the annotator twice in a
104 *  row with --in-place, only the last set of annotations will appear in
105 *  the codebase at the end.
106 *  <p>
107 *
108 *  To preserve changes when using the --in-place option, first remove the
109 *  backup files.  Or, use the <tt>-d .</tt> option, which makes (and
110 *  reads) no backup, instead of --in-place. [default false]</li>
111 *       <li id="option:abbreviate"><b>-a</b> <b>--abbreviate=</b><i>boolean</i>. Abbreviate annotation names [default true]</li>
112 *       <li id="option:comments"><b>-c</b> <b>--comments=</b><i>boolean</i>. Insert annotations in comments [default false]</li>
113 *       <li id="option:omit-annotation"><b>-o</b> <b>--omit-annotation=</b><i>string</i>. Omit given annotation</li>
114 *       <li id="option:nowarn"><b>--nowarn=</b><i>boolean</i>. Suppress warnings about disallowed insertions [default false]</li>
115 *       <li id="option:convert-jaifs"><b>--convert-jaifs=</b><i>boolean</i>. Convert JAIFs to new format [default false]</li>
116 *       <li id="option:help"><b>-h</b> <b>--help=</b><i>boolean</i>. Print usage information and exit [default false]</li>
117 *     </ul>
118 *   </li>
119 *   <li id="optiongroup:Debugging-options">Debugging options
120 *     <ul>
121 *       <li id="option:verbose"><b>-v</b> <b>--verbose=</b><i>boolean</i>. Verbose (print progress information) [default false]</li>
122 *       <li id="option:debug"><b>--debug=</b><i>boolean</i>. Debug (print debug information) [default false]</li>
123 *       <li id="option:print-error-stack"><b>--print-error-stack=</b><i>boolean</i>. Print error stack [default false]</li>
124 *     </ul>
125 *   </li>
126 * </ul>
127 * <!-- end options doc -->
128 */
129public class Main {
130
131  /** Directory in which output files are written. */
132  @OptionGroup("General options")
133  @Option("-d <directory> Directory in which output files are written")
134  public static String outdir = "annotated/";
135
136  /**
137   * If true, overwrite original source files (making a backup first).
138   * Furthermore, if the backup files already exist, they are used instead
139   * of the .java files.  This behavior permits a user to tweak the .jaif
140   * file and re-run the annotator.
141   * <p>
142   *
143   * Note that if the user runs the annotator with --in-place, makes edits,
144   * and then re-runs the annotator with this --in-place option, those
145   * edits are lost.  Similarly, if the user runs the annotator twice in a
146   * row with --in-place, only the last set of annotations will appear in
147   * the codebase at the end.
148   * <p>
149   *
150   * To preserve changes when using the --in-place option, first remove the
151   * backup files.  Or, use the <tt>-d .</tt> option, which makes (and
152   * reads) no backup, instead of --in-place.
153   */
154  @Option("-i Overwrite original source files")
155  public static boolean in_place = false;
156
157  @Option("-a Abbreviate annotation names")
158  public static boolean abbreviate = true;
159
160  @Option("-c Insert annotations in comments")
161  public static boolean comments = false;
162
163  @Option("-o Omit given annotation")
164  public static String omit_annotation;
165
166  @Option("Suppress warnings about disallowed insertions")
167  public static boolean nowarn;
168
169  // Instead of doing insertions, create new JAIFs using AST paths
170  //  extracted from existing JAIFs and source files they match
171  @Option("Convert JAIFs to AST Path format")
172  public static boolean convert_jaifs = false;
173
174  @Option("-h Print usage information and exit")
175  public static boolean help = false;
176
177  // Debugging options go below here.
178
179  @OptionGroup("Debugging options")
180  @Option("-v Verbose (print progress information)")
181  public static boolean verbose;
182
183  @Option("Debug (print debug information)")
184  public static boolean debug = false;
185
186  @Option("Print error stack")
187  public static boolean print_error_stack = false;
188
189  private static ElementVisitor<Void, AElement> classFilter =
190      new ElementVisitor<Void, AElement>() {
191    <K, V extends AElement>
192    Void filter(VivifyingMap<K, V> vm0, VivifyingMap<K, V> vm1) {
193      for (Map.Entry<K, V> entry : vm0.entrySet()) {
194        entry.getValue().accept(this, vm1.vivify(entry.getKey()));
195      }
196      return null;
197    }
198
199    @Override
200    public Void visitAnnotationDef(AnnotationDef def, AElement el) {
201      // not used, since package declarations not handled here
202      return null;
203    }
204
205    @Override
206    public Void visitBlock(ABlock el0, AElement el) {
207      ABlock el1 = (ABlock) el;
208      filter(el0.locals, el1.locals);
209      return visitExpression(el0, el);
210    }
211
212    @Override
213    public Void visitClass(AClass el0, AElement el) {
214      AClass el1 = (AClass) el;
215      filter(el0.methods, el1.methods);
216      filter(el0.fields, el1.fields);
217      filter(el0.fieldInits, el1.fieldInits);
218      filter(el0.staticInits, el1.staticInits);
219      filter(el0.instanceInits, el1.instanceInits);
220      return visitDeclaration(el0, el);
221    }
222
223    @Override
224    public Void visitDeclaration(ADeclaration el0, AElement el) {
225      ADeclaration el1 = (ADeclaration) el;
226      VivifyingMap<ASTPath, ATypeElement> insertAnnotations =
227          el1.insertAnnotations;
228      VivifyingMap<ASTPath, ATypeElementWithType> insertTypecasts =
229          el1.insertTypecasts;
230      for (Map.Entry<ASTPath, ATypeElement> entry :
231          el0.insertAnnotations.entrySet()) {
232        ASTPath p = entry.getKey();
233        ATypeElement e = entry.getValue();
234        insertAnnotations.put(p, e);
235        //visitTypeElement(e, insertAnnotations.vivify(p));
236      }
237      for (Map.Entry<ASTPath, ATypeElementWithType> entry :
238          el0.insertTypecasts.entrySet()) {
239        ASTPath p = entry.getKey();
240        ATypeElementWithType e = entry.getValue();
241        type.Type type = e.getType();
242        if (type instanceof type.DeclaredType
243            && ((type.DeclaredType) type).getName().isEmpty()) {
244          insertAnnotations.put(p, e);
245          //visitTypeElement(e, insertAnnotations.vivify(p));
246        } else {
247          insertTypecasts.put(p, e);
248          //visitTypeElementWithType(e, insertTypecasts.vivify(p));
249        }
250      }
251      return null;
252    }
253
254    @Override
255    public Void visitExpression(AExpression el0, AElement el) {
256      AExpression el1 = (AExpression) el;
257      filter(el0.typecasts, el1.typecasts);
258      filter(el0.instanceofs, el1.instanceofs);
259      filter(el0.news, el1.news);
260      return null;
261    }
262
263    @Override
264    public Void visitField(AField el0, AElement el) {
265      return visitDeclaration(el0, el);
266    }
267
268    @Override
269    public Void visitMethod(AMethod el0, AElement el) {
270      AMethod el1 = (AMethod) el;
271      filter(el0.bounds, el1.bounds);
272      filter(el0.parameters, el1.parameters);
273      filter(el0.throwsException, el1.throwsException);
274      el0.returnType.accept(this, el1.returnType);
275      el0.receiver.accept(this, el1.receiver);
276      el0.body.accept(this, el1.body);
277      return visitDeclaration(el0, el);
278    }
279
280    @Override
281    public Void visitTypeElement(ATypeElement el0, AElement el) {
282      ATypeElement el1 = (ATypeElement) el;
283      filter(el0.innerTypes, el1.innerTypes);
284      return null;
285    }
286
287    @Override
288    public Void visitTypeElementWithType(ATypeElementWithType el0,
289        AElement el) {
290      ATypeElementWithType el1 = (ATypeElementWithType) el;
291      el1.setType(el0.getType());
292      return visitTypeElement(el0, el);
293    }
294
295    @Override
296    public Void visitElement(AElement el, AElement arg) {
297      return null;
298    }
299  };
300
301  private static AScene filteredScene(final AScene scene) {
302    final AScene filtered = new AScene();
303    filtered.packages.putAll(scene.packages);
304    filtered.imports.putAll(scene.imports);
305    for (Map.Entry<String, AClass> entry : scene.classes.entrySet()) {
306      String key = entry.getKey();
307      AClass clazz0 = entry.getValue();
308      AClass clazz1 = filtered.classes.vivify(key);
309      clazz0.accept(classFilter, clazz1);
310    }
311    filtered.prune();
312    return filtered;
313  }
314
315  private static ATypeElement findInnerTypeElement(Tree t,
316      ASTRecord rec, ADeclaration decl, Type type, Insertion ins) {
317    ASTPath astPath = rec.astPath;
318    GenericArrayLocationCriterion galc =
319        ins.getCriteria().getGenericArrayLocation();
320    assert astPath != null && galc != null;
321    List<TypePathEntry> tpes = galc.getLocation();
322    ASTPath.ASTEntry entry;
323    for (TypePathEntry tpe : tpes) {
324      switch (tpe.tag) {
325      case ARRAY:
326        if (!astPath.isEmpty()) {
327          entry = astPath.get(-1);
328          if (entry.getTreeKind() == Tree.Kind.NEW_ARRAY
329              && entry.childSelectorIs(ASTPath.TYPE)) {
330            entry = new ASTPath.ASTEntry(Tree.Kind.NEW_ARRAY,
331                ASTPath.TYPE, entry.getArgument() + 1);
332            break;
333          }
334        }
335        entry = new ASTPath.ASTEntry(Tree.Kind.ARRAY_TYPE,
336            ASTPath.TYPE);
337        break;
338      case INNER_TYPE:
339        entry = new ASTPath.ASTEntry(Tree.Kind.MEMBER_SELECT,
340            ASTPath.EXPRESSION);
341        break;
342      case TYPE_ARGUMENT:
343        entry = new ASTPath.ASTEntry(Tree.Kind.PARAMETERIZED_TYPE,
344            ASTPath.TYPE_ARGUMENT, tpe.arg);
345        break;
346      case WILDCARD:
347        entry = new ASTPath.ASTEntry(Tree.Kind.UNBOUNDED_WILDCARD,
348            ASTPath.BOUND);
349        break;
350      default:
351        throw new IllegalArgumentException("unknown type tag " + tpe.tag);
352      }
353      astPath = astPath.extend(entry);
354    }
355
356    return decl.insertAnnotations.vivify(astPath);
357  }
358
359  private static void convertInsertion(String pkg,
360      JCTree.JCCompilationUnit tree, ASTRecord rec, Insertion ins,
361      AScene scene, Multimap<Insertion, Annotation> insertionSources) {
362    Collection<Annotation> annos = insertionSources.get(ins);
363    if (rec == null) {
364      if (ins.getCriteria().isOnPackage()) {
365        for (Annotation anno : annos) {
366          scene.packages.get(pkg).tlAnnotationsHere.add(anno);
367        }
368      }
369    } else if (scene != null && rec.className != null) {
370      AClass clazz = scene.classes.vivify(rec.className);
371      ADeclaration decl = null;  // insertion target
372      if (ins.getCriteria().onBoundZero()) {
373        int n = rec.astPath.size();
374        if (!rec.astPath.get(n-1).childSelectorIs(ASTPath.BOUND)) {
375          ASTPath astPath = ASTPath.empty();
376          for (int i = 0; i < n; i++) {
377            astPath = astPath.extend(rec.astPath.get(i));
378          }
379          astPath = astPath.extend(
380              new ASTPath.ASTEntry(Tree.Kind.TYPE_PARAMETER,
381                  ASTPath.BOUND, 0));
382          rec = rec.replacePath(astPath);
383        }
384      }
385      if (rec.methodName == null) {
386        decl = rec.varName == null ? clazz
387            : clazz.fields.vivify(rec.varName);
388      } else {
389        AMethod meth = clazz.methods.vivify(rec.methodName);
390        if (rec.varName == null) {
391          decl = meth;  // ?
392        } else {
393          try {
394            int i = Integer.parseInt(rec.varName);
395            decl = i < 0 ? meth.receiver
396                : meth.parameters.vivify(i);
397          } catch (NumberFormatException e) {
398            TreePath path = ASTIndex.getTreePath(tree, rec);
399            JCTree.JCVariableDecl varTree = null;
400            JCTree.JCMethodDecl methTree = null;
401            JCTree.JCClassDecl classTree = null;
402            loop:
403              while (path != null) {
404                Tree leaf = path.getLeaf();
405                switch (leaf.getKind()) {
406                case VARIABLE:
407                  varTree = (JCTree.JCVariableDecl) leaf;
408                  break;
409                case METHOD:
410                  methTree = (JCTree.JCMethodDecl) leaf;
411                  break;
412                case ANNOTATION:
413                case CLASS:
414                case ENUM:
415                case INTERFACE:
416                  break loop;
417                default:
418                  path = path.getParentPath();
419                }
420              }
421            while (path != null) {
422              Tree leaf = path.getLeaf();
423              Tree.Kind kind = leaf.getKind();
424              if (kind == Tree.Kind.METHOD) {
425                methTree = (JCTree.JCMethodDecl) leaf;
426                int i = LocalVariableScanner.indexOfVarTree(path,
427                    varTree, rec.varName);
428                int m = methTree.getStartPosition();
429                int a = varTree.getStartPosition();
430                int b = varTree.getEndPosition(tree.endPositions);
431                LocalLocation loc = new LocalLocation(i, a-m, b-a);
432                decl = meth.body.locals.vivify(loc);
433                break;
434              }
435              if (ASTPath.isClassEquiv(kind)) {
436                classTree = (JCTree.JCClassDecl) leaf;
437                // ???
438                    break;
439              }
440              path = path.getParentPath();
441            }
442          }
443        }
444      }
445      if (decl != null) {
446        AElement el;
447        if (rec.astPath.isEmpty()) {
448          el = decl;
449        } else if (ins.getKind() == Insertion.Kind.CAST) {
450          annotations.el.ATypeElementWithType elem =
451              decl.insertTypecasts.vivify(rec.astPath);
452          elem.setType(((CastInsertion) ins).getType());
453          el = elem;
454        } else {
455          el = decl.insertAnnotations.vivify(rec.astPath);
456        }
457        for (Annotation anno : annos) {
458          el.tlAnnotationsHere.add(anno);
459        }
460        if (ins instanceof TypedInsertion) {
461          TypedInsertion ti = (TypedInsertion) ins;
462          if (!rec.astPath.isEmpty()) {
463            //addInnerTypePaths(decl, rec, ti, insertionSources);
464          }
465          for (Insertion inner : ti.getInnerTypeInsertions()) {
466            Tree t = ASTIndex.getNode(tree, rec);
467            if (t != null) {
468              ATypeElement elem = findInnerTypeElement(t,
469                  rec, decl, ti.getType(), inner);
470              for (Annotation a : insertionSources.get(inner)) {
471                elem.tlAnnotationsHere.add(a);
472              }
473            }
474          }
475        }
476      }
477    }
478  }
479
480
481  // Implementation details:
482  //  1. The annotator partially compiles source
483  //     files using the compiler API (JSR-199), obtaining an AST.
484  //  2. The annotator reads the specification file, producing a set of
485  //     annotator.find.Insertions.  Insertions completely specify what to
486  //     write (as a String, which is ultimately translated according to the
487  //     keyword file) and how to write it (as annotator.find.Criteria).
488  //  3. It then traverses the tree, looking for nodes that satisfy the
489  //     Insertion Criteria, translating the Insertion text against the
490  //     keyword file, and inserting the annotations into the source file.
491
492  /**
493   * Runs the annotator, parsing the source and spec files and applying
494   * the annotations.
495   */
496  public static void main(String[] args) throws IOException {
497
498    if (verbose) {
499      System.out.printf("insert-annotations-to-source (%s)",
500                        annotations.io.classfile.ClassFileReader.INDEX_UTILS_VERSION);
501    }
502
503    Options options = new Options(
504        "Main [options] { ann-file | java-file | @arg-file } ...\n"
505            + "(Contents of argfiles are expanded into the argument list.)",
506        Main.class);
507    String[] file_args;
508    try {
509      String[] cl_args = CommandLine.parse(args);
510      file_args = options.parse_or_usage(cl_args);
511    } catch (IOException ex) {
512      System.err.println(ex);
513      System.err.println("(For non-argfile beginning with \"@\", use \"@@\" for initial \"@\".");
514      System.err.println("Alternative for filenames: indicate directory, e.g. as './@file'.");
515      System.err.println("Alternative for flags: use '=', as in '-o=@Deprecated'.)");
516      file_args = null;  // Eclipse compiler issue workaround
517      System.exit(1);
518    }
519
520    DebugWriter dbug = new DebugWriter();
521    DebugWriter verb = new DebugWriter();
522    DebugWriter both = dbug.or(verb);
523    dbug.setEnabled(debug);
524    verb.setEnabled(verbose);
525    TreeFinder.warn.setEnabled(!nowarn);
526    TreeFinder.stak.setEnabled(print_error_stack);
527    TreeFinder.dbug.setEnabled(debug);
528    Criteria.dbug.setEnabled(debug);
529
530    if (help) {
531      options.print_usage();
532      System.exit(0);
533    }
534
535    if (in_place && outdir != "annotated/") { // interned
536      options.print_usage("The --outdir and --in-place options are mutually exclusive.");
537      System.exit(1);
538    }
539
540    if (file_args.length < 2) {
541      options.print_usage("Supplied %d arguments, at least 2 needed%n", file_args.length);
542      System.exit(1);
543    }
544
545    // The insertions specified by the annotation files.
546    Insertions insertions = new Insertions();
547    // The Java files into which to insert.
548    List<String> javafiles = new ArrayList<String>();
549    // Imports required to resolve annotations (when abbreviate==true).
550    Set<String> imports = new LinkedHashSet<String>();
551
552    // Indices to maintain insertion source traces.
553    Map<String, Multimap<Insertion, Annotation>> insertionIndex =
554        new HashMap<String, Multimap<Insertion, Annotation>>();
555    Map<Insertion, String> insertionOrigins = new HashMap<Insertion, String>();
556    Map<String, AScene> scenes = new HashMap<String, AScene>();
557
558    IndexFileParser.setAbbreviate(abbreviate);
559    for (String arg : file_args) {
560      if (arg.endsWith(".java")) {
561        javafiles.add(arg);
562      } else if (arg.endsWith(".jaif") ||
563                 arg.endsWith(".jann")) {
564        IndexFileSpecification spec = new IndexFileSpecification(arg);
565        try {
566          List<Insertion> parsedSpec = spec.parse();
567          AScene scene = spec.getScene();
568          Collections.sort(parsedSpec, new Comparator<Insertion>() {
569            @Override
570            public int compare(Insertion i1, Insertion i2) {
571              ASTPath p1 = i1.getCriteria().getASTPath();
572              ASTPath p2 = i2.getCriteria().getASTPath();
573              return p1 == null
574                  ? p2 == null ? 0 : -1
575                  : p2 == null ? 1 : p1.compareTo(p2);
576            }
577          });
578          if (convert_jaifs) {
579            scenes.put(arg, convert_jaifs ? filteredScene(scene) : scene);
580            for (Insertion ins : parsedSpec) {
581              insertionOrigins.put(ins, arg);
582            }
583            if (!insertionIndex.containsKey(arg)) {
584              insertionIndex.put(arg,
585                  LinkedHashMultimap.<Insertion, Annotation>create());
586            }
587            insertionIndex.get(arg).putAll(spec.insertionSources());
588          }
589          if (abbreviate) {
590            Map<String, Set<String>> annotationImports =
591                spec.annotationImports();
592            for (Set<String> set : annotationImports.values()) {
593              imports.addAll(set);
594            }
595          }
596          both.debug("Read %d annotations from %s%n", parsedSpec.size(), arg);
597          if (omit_annotation != null) {
598            List<Insertion> filtered =
599                new ArrayList<Insertion>(parsedSpec.size());
600            for (Insertion insertion : parsedSpec) {
601              // TODO: this won't omit annotations if the insertion is more than
602              // just the annotation (such as if the insertion is a cast
603              // insertion or a 'this' parameter in a method declaration).
604              if (! omit_annotation.equals(insertion.getText())) {
605                filtered.add(insertion);
606              }
607            }
608            parsedSpec = filtered;
609            both.debug("After filtering: %d annotations from %s%n",
610                parsedSpec.size(), arg);
611          }
612          insertions.addAll(parsedSpec);
613        } catch (RuntimeException e) {
614          if (e.getCause() != null
615              && e.getCause() instanceof FileNotFoundException) {
616            System.err.println("File not found: " + arg);
617            System.exit(1);
618          } else {
619            throw e;
620          }
621        } catch (FileIOException e) {
622          // Add 1 to the line number since line numbers in text editors are usually one-based.
623          System.err.println("Error while parsing annotation file " + arg + " at line "
624              + (e.lineNumber + 1) + ":");
625          if (e.getMessage() != null) {
626            System.err.println('\t' + e.getMessage());
627          }
628          if (e.getCause() != null && e.getCause().getMessage() != null) {
629            System.err.println('\t' + e.getCause().getMessage());
630          }
631          if (print_error_stack) {
632            e.printStackTrace();
633          }
634          System.exit(1);
635        }
636      } else {
637        throw new Error("Unrecognized file extension: " + arg);
638      }
639    }
640
641    if (dbug.isEnabled()) {
642      dbug.debug("%d insertions, %d .java files%n",
643          insertions.size(), javafiles.size());
644      dbug.debug("Insertions:%n");
645      for (Insertion insertion : insertions) {
646        dbug.debug("  %s%n", insertion);
647      }
648    }
649
650    for (String javafilename : javafiles) {
651      verb.debug("Processing %s%n", javafilename);
652
653      File javafile = new File(javafilename);
654      File unannotated = new File(javafilename + ".unannotated");
655      if (in_place) {
656        // It doesn't make sense to check timestamps;
657        // if the .java.unannotated file exists, then just use it.
658        // A user can rename that file back to just .java to cause the
659        // .java file to be read.
660        if (unannotated.exists()) {
661          verb.debug("Renaming %s to %s%n", unannotated, javafile);
662          boolean success = unannotated.renameTo(javafile);
663          if (! success) {
664            throw new Error(String.format("Failed renaming %s to %s",
665                                          unannotated, javafile));
666          }
667        }
668      }
669
670      String fileSep = System.getProperty("file.separator");
671      String fileLineSep = System.getProperty("line.separator");
672      Source src;
673      // Get the source file, and use it to obtain parse trees.
674      try {
675        // fileLineSep is set here so that exceptions can be caught
676        fileLineSep = UtilMDE.inferLineSeparator(javafilename);
677        src = new Source(javafilename);
678        verb.debug("Parsed %s%n", javafilename);
679      } catch (Source.CompilerException e) {
680        e.printStackTrace();
681        return;
682      } catch (IOException e) {
683        e.printStackTrace();
684        return;
685      }
686
687      int num_insertions = 0;
688      String pkg = "";
689
690      for (CompilationUnitTree cut : src.parse()) {
691        JCTree.JCCompilationUnit tree = (JCTree.JCCompilationUnit) cut;
692        ExpressionTree pkgExp = cut.getPackageName();
693        pkg = pkgExp == null ? "" : pkgExp.toString();
694
695        // Create a finder, and use it to get positions.
696        TreeFinder finder = new TreeFinder(tree);
697        SetMultimap<Pair<Integer, ASTPath>, Insertion> positions =
698            finder.getPositions(tree, insertions);
699
700        if (convert_jaifs) {
701          // program used only for JAIF conversion; execute following
702          // block and then skip remainder of loop
703          Multimap<ASTRecord, Insertion> astInsertions =
704              finder.getPaths();
705          for (Map.Entry<ASTRecord, Collection<Insertion>> entry :
706              astInsertions.asMap().entrySet()) {
707            ASTRecord rec = entry.getKey();
708            for (Insertion ins : entry.getValue()) {
709              if (ins.getCriteria().getASTPath() != null) { continue; }
710              String arg = insertionOrigins.get(ins);
711              AScene scene = scenes.get(arg);
712              Multimap<Insertion, Annotation> insertionSources =
713                  insertionIndex.get(arg);
714              //String text =
715              //  ins.getText(comments, abbreviate, false, 0, '\0');
716
717              // TODO: adjust for missing end of path (?)
718
719              if (insertionSources.containsKey(ins)) {
720                convertInsertion(pkg, tree, rec, ins, scene, insertionSources);
721              }
722            }
723          }
724          continue;
725        }
726
727        // Apply the positions to the source file.
728        if (both.isEnabled()) {
729          System.err.printf(
730              "getPositions returned %d positions in tree for %s%n",
731              positions.size(), javafilename);
732        }
733
734        Set<Pair<Integer, ASTPath>> positionKeysUnsorted =
735            positions.keySet();
736        Set<Pair<Integer, ASTPath>> positionKeysSorted =
737          new TreeSet<Pair<Integer, ASTPath>>(
738              new Comparator<Pair<Integer, ASTPath>>() {
739                @Override
740                public int compare(Pair<Integer, ASTPath> p1,
741                    Pair<Integer, ASTPath> p2) {
742                  int c = Integer.compare(p2.a, p1.a);
743                  if (c == 0) {
744                    c = p2.b == null ? p1.b == null ? 0 : -1
745                        : p1.b == null ? 1 : p2.b.compareTo(p1.b);
746                  }
747                  return c;
748                }
749              });
750        positionKeysSorted.addAll(positionKeysUnsorted);
751        for (Pair<Integer, ASTPath> pair : positionKeysSorted) {
752          boolean receiverInserted = false;
753          boolean newInserted = false;
754          boolean constructorInserted = false;
755          Set<String> seen = new TreeSet<String>();
756          List<Insertion> toInsertList = new ArrayList<Insertion>(positions.get(pair));
757          Collections.reverse(toInsertList);
758          dbug.debug("insertion pos: %d%n", pair.a);
759          assert pair.a >= 0
760            : "pos is negative: " + pair.a + " " + toInsertList.get(0) + " " + javafilename;
761          for (Insertion iToInsert : toInsertList) {
762            // Possibly add whitespace after the insertion
763            String trailingWhitespace = "";
764            boolean gotSeparateLine = false;
765            int pos = pair.a;  // reset each iteration in case of dyn adjustment
766            if (iToInsert.getSeparateLine()) {
767              // System.out.printf("getSeparateLine=true for insertion at pos %d: %s%n", pos, iToInsert);
768              int indentation = 0;
769              while ((pos - indentation != 0)
770                     // horizontal whitespace
771                     && (src.charAt(pos-indentation-1) == ' '
772                         || src.charAt(pos-indentation-1) == '\t')) {
773                // System.out.printf("src.charAt(pos-indentation-1 == %d-%d-1)='%s'%n",
774                //                   pos, indentation, src.charAt(pos-indentation-1));
775                indentation++;
776              }
777              if ((pos - indentation == 0)
778                  // horizontal whitespace
779                  || (src.charAt(pos-indentation-1) == '\f'
780                      || src.charAt(pos-indentation-1) == '\n'
781                      || src.charAt(pos-indentation-1) == '\r')) {
782                trailingWhitespace = fileLineSep + src.substring(pos-indentation, pos);
783                gotSeparateLine = true;
784              }
785            }
786
787            char precedingChar;
788            if (pos != 0) {
789              precedingChar = src.charAt(pos - 1);
790            } else {
791              precedingChar = '\0';
792            }
793
794            if (iToInsert.getKind() == Insertion.Kind.ANNOTATION) {
795              AnnotationInsertion ai = (AnnotationInsertion) iToInsert;
796              if (ai.isGenerateBound()) {  // avoid multiple ampersands
797                try {
798                  String s = src.substring(pos, pos+9);
799                  if ("Object & ".equals(s)) {
800                    ai.setGenerateBound(false);
801                    precedingChar = '.';  // suppress leading space
802                  }
803                } catch (StringIndexOutOfBoundsException e) {}
804              }
805              if (ai.isGenerateExtends()) {  // avoid multiple "extends"
806                try {
807                  String s = src.substring(pos, pos+9);
808                  if (" extends ".equals(s)) {
809                    ai.setGenerateExtends(false);
810                    pos += 8;
811                  }
812                } catch (StringIndexOutOfBoundsException e) {}
813              }
814            } else if (iToInsert.getKind() == Insertion.Kind.CAST) {
815                ((CastInsertion) iToInsert)
816                        .setOnArrayLiteral(src.charAt(pos) == '{');
817            } else if (iToInsert.getKind() == Insertion.Kind.RECEIVER) {
818              ReceiverInsertion ri = (ReceiverInsertion) iToInsert;
819              ri.setAnnotationsOnly(receiverInserted);
820              receiverInserted = true;
821            } else if (iToInsert.getKind() == Insertion.Kind.NEW) {
822              NewInsertion ni = (NewInsertion) iToInsert;
823              ni.setAnnotationsOnly(newInserted);
824              newInserted = true;
825            } else if (iToInsert.getKind() == Insertion.Kind.CONSTRUCTOR) {
826              ConstructorInsertion ci = (ConstructorInsertion) iToInsert;
827              if (constructorInserted) { ci.setAnnotationsOnly(true); }
828              constructorInserted = true;
829            }
830
831            String toInsert = iToInsert.getText(comments, abbreviate,
832                gotSeparateLine, pos, precedingChar) + trailingWhitespace;
833            if (seen.contains(toInsert)) { continue; }  // eliminate duplicates
834            seen.add(toInsert);
835            if (abbreviate) {
836              Set<String> packageNames = iToInsert.getPackageNames();
837              dbug.debug("Need import %s%n  due to insertion %s%n",
838                  packageNames, toInsert);
839              imports.addAll(packageNames);
840            }
841
842            // If it's already there, don't re-insert.  This is a hack!
843            // Also, I think this is already checked when constructing the
844            // insertions.
845            int precedingTextPos = pos-toInsert.length()-1;
846            if (precedingTextPos >= 0) {
847              String precedingTextPlusChar
848                = src.getString().substring(precedingTextPos, pos);
849              if (toInsert.equals(
850                      precedingTextPlusChar.substring(0, toInsert.length()))
851                  || toInsert.equals(precedingTextPlusChar.substring(1))) {
852                dbug.debug(
853                    "Inserting %s at %d in code of length %d with preceding text '%s'%n",
854                    toInsert, pos, src.getString().length(),
855                    precedingTextPlusChar);
856                dbug.debug("Already present, skipping%n");
857                continue;
858              }
859            }
860
861            // TODO: Neither the above hack nor this check should be
862            // necessary.  Find out why re-insertions still occur and
863            // fix properly.
864            if (iToInsert.getInserted()) { continue; }
865            src.insert(pos, toInsert);
866            if (verbose && !debug) {
867              System.out.print(".");
868              num_insertions++;
869              if ((num_insertions % 50) == 0) {
870                System.out.println();   // terminate the line that contains dots
871              }
872            }
873            dbug.debug("Post-insertion source: %n" + src.getString());
874          }
875        }
876      }
877
878      if (convert_jaifs) {
879        for (Map.Entry<String, AScene> entry : scenes.entrySet()) {
880          String filename = entry.getKey();
881          AScene scene = entry.getValue();
882          try {
883            IndexFileWriter.write(scene, filename + ".converted");
884          } catch (DefException e) {
885            System.err.println(filename + ": " + " format error in conversion");
886            if (print_error_stack) {
887              e.printStackTrace();
888            }
889          }
890        }
891        return;  // done with conversion
892      }
893
894      if (verbose) {
895        if ((num_insertions % 50) != 0) {
896          System.out.println();   // terminate the line that contains dots
897        }
898      }
899
900      if (dbug.isEnabled()) {
901        dbug.debug("%d imports to insert%n", imports.size());
902        for (String classname : imports) {
903          dbug.debug("  %s%n", classname);
904        }
905      }
906
907      // insert import statements
908      {
909        Pattern importPattern = Pattern.compile("(?m)^import\\b");
910        Pattern packagePattern = Pattern.compile("(?m)^package\\b.*;(\\n|\\r\\n?)");
911        int importIndex = 0;      // default: beginning of file
912        String srcString = src.getString();
913        Matcher m = importPattern.matcher(srcString);
914        if (m.find()) {
915          importIndex = m.start();
916        } else {
917          // Debug.info("Didn't find import in " + srcString);
918          m = packagePattern.matcher(srcString);
919          if (m.find()) {
920            importIndex = m.end();
921          }
922        }
923        for (String classname : imports) {
924          String toInsert = "import " + classname + ";" + fileLineSep;
925          src.insert(importIndex, toInsert);
926          importIndex += toInsert.length();
927        }
928      }
929
930      // Write the source file.
931      File outfile = null;
932      try {
933        if (in_place) {
934          outfile = javafile;
935          if (verbose) {
936            System.out.printf("Renaming %s to %s%n", javafile, unannotated);
937          }
938          boolean success = javafile.renameTo(unannotated);
939          if (! success) {
940            throw new Error(String.format("Failed renaming %s to %s",
941                                          javafile, unannotated));
942          }
943        } else {
944          if (pkg.isEmpty()) {
945            outfile = new File(outdir, javafile.getName());
946          } else {
947            String[] pkgPath = pkg.split("\\.");
948            StringBuilder sb = new StringBuilder(outdir);
949            for (int i = 0 ; i < pkgPath.length ; i++) {
950              sb.append(fileSep).append(pkgPath[i]);
951            }
952            outfile = new File(sb.toString(), javafile.getName());
953          }
954          outfile.getParentFile().mkdirs();
955        }
956        OutputStream output = new FileOutputStream(outfile);
957        if (verbose) {
958          System.out.printf("Writing %s%n", outfile);
959        }
960        src.write(output);
961        output.close();
962      } catch (IOException e) {
963        System.err.println("Problem while writing file " + outfile);
964        e.printStackTrace();
965        System.exit(1);
966      }
967    }
968  }
969
970  public static String pathToString(TreePath path) {
971    if (path == null)
972      return "null";
973    return treeToString(path.getLeaf());
974  }
975
976  public static String treeToString(Tree node) {
977    String asString = node.toString();
978    String oneLine = firstLine(asString);
979    return "\"" + oneLine + "\"";
980  }
981
982  /**
983   * Return the first non-empty line of the string, adding an ellipsis
984   * (...) if the string was truncated.
985   */
986  public static String firstLine(String s) {
987    while (s.startsWith("\n")) {
988      s = s.substring(1);
989    }
990    int newlineIndex = s.indexOf('\n');
991    if (newlineIndex == -1) {
992      return s;
993    } else {
994      return s.substring(0, newlineIndex) + "...";
995    }
996  }
997
998  /**
999   * Separates the annotation class from its arguments.
1000   *
1001   * @return given <code>@foo(bar)</code> it returns the pair <code>{ @foo, (bar) }</code>.
1002   */
1003  public static Pair<String,String> removeArgs(String s) {
1004    int pidx = s.indexOf("(");
1005    return (pidx == -1) ?
1006        Pair.of(s, (String)null) :
1007        Pair.of(s.substring(0, pidx), s.substring(pidx));
1008  }
1009}
1010