• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package annotator;
2 
3 import java.io.File;
4 import java.io.FileNotFoundException;
5 import java.io.FileOutputStream;
6 import java.io.IOException;
7 import java.io.OutputStream;
8 import java.util.ArrayList;
9 import java.util.Collection;
10 import java.util.Collections;
11 import java.util.Comparator;
12 import java.util.HashMap;
13 import java.util.LinkedHashMap;
14 import java.util.LinkedHashSet;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Set;
18 import java.util.TreeSet;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
21 
22 import plume.FileIOException;
23 import plume.Option;
24 import plume.OptionGroup;
25 import plume.Options;
26 import plume.Pair;
27 import plume.UtilMDE;
28 import type.Type;
29 import annotations.Annotation;
30 import annotations.el.ABlock;
31 import annotations.el.AClass;
32 import annotations.el.ADeclaration;
33 import annotations.el.AElement;
34 import annotations.el.AExpression;
35 import annotations.el.AField;
36 import annotations.el.AMethod;
37 import annotations.el.AScene;
38 import annotations.el.ATypeElement;
39 import annotations.el.ATypeElementWithType;
40 import annotations.el.AnnotationDef;
41 import annotations.el.DefException;
42 import annotations.el.ElementVisitor;
43 import annotations.el.LocalLocation;
44 import annotations.io.ASTIndex;
45 import annotations.io.ASTPath;
46 import annotations.io.ASTRecord;
47 import annotations.io.DebugWriter;
48 import annotations.io.IndexFileParser;
49 import annotations.io.IndexFileWriter;
50 import annotations.util.coll.VivifyingMap;
51 import annotator.find.AnnotationInsertion;
52 import annotator.find.CastInsertion;
53 import annotator.find.ConstructorInsertion;
54 import annotator.find.Criteria;
55 import annotator.find.GenericArrayLocationCriterion;
56 import annotator.find.Insertion;
57 import annotator.find.Insertions;
58 import annotator.find.NewInsertion;
59 import annotator.find.ReceiverInsertion;
60 import annotator.find.TreeFinder;
61 import annotator.find.TypedInsertion;
62 import annotator.scanner.LocalVariableScanner;
63 import annotator.specification.IndexFileSpecification;
64 
65 import com.google.common.collect.LinkedHashMultimap;
66 import com.google.common.collect.Multimap;
67 import com.google.common.collect.SetMultimap;
68 import com.sun.source.tree.CompilationUnitTree;
69 import com.sun.source.tree.ExpressionTree;
70 import com.sun.source.tree.Tree;
71 import com.sun.source.util.TreePath;
72 import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry;
73 import com.sun.tools.javac.main.CommandLine;
74 import 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  */
130 public 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 
filteredScene(final AScene scene)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 
findInnerTypeElement(Tree t, ASTRecord rec, ADeclaration decl, Type type, Insertion ins)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 
convertInsertion(String pkg, JCTree.JCCompilationUnit tree, ASTRecord rec, Insertion ins, AScene scene, Multimap<Insertion, Annotation> insertionSources)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 
pathToString(TreePath path)972   public static String pathToString(TreePath path) {
973     if (path == null) {
974       return "null";
975     }
976     return treeToString(path.getLeaf());
977   }
978 
treeToString(Tree node)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    */
firstLine(String s)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    */
removeArgs(String s)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