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