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