001package daikon.config; 002 003import com.sun.source.doctree.DocCommentTree; 004import com.sun.source.doctree.DocTree; 005import com.sun.source.util.DocTrees; 006import java.io.FileOutputStream; 007import java.io.PrintStream; 008import java.lang.reflect.Field; 009import java.util.ArrayList; 010import java.util.Comparator; 011import java.util.List; 012import java.util.Locale; 013import java.util.Map; 014import java.util.Set; 015import java.util.TreeMap; 016import java.util.TreeSet; 017import javax.lang.model.SourceVersion; 018import javax.lang.model.element.Element; 019import javax.lang.model.element.ElementKind; 020import javax.lang.model.element.Modifier; 021import javax.lang.model.element.NestingKind; 022import javax.lang.model.element.TypeElement; 023import javax.lang.model.type.TypeMirror; 024import javax.tools.Diagnostic; 025import jdk.javadoc.doclet.Doclet; 026import jdk.javadoc.doclet.DocletEnvironment; 027import jdk.javadoc.doclet.Reporter; 028import org.checkerframework.checker.nullness.qual.KeyFor; 029import org.checkerframework.checker.nullness.qual.NonNull; 030import org.checkerframework.checker.nullness.qual.Nullable; 031import org.checkerframework.checker.regex.qual.Regex; 032import org.checkerframework.checker.signature.qual.ClassGetName; 033import org.plumelib.reflection.ReflectionPlume; 034 035/** 036 * InvariantDoclet is a Javadoc doclet that collects information about the invariants defined within 037 * Daikon. Class documentation is collected about each class that is derived (either directly or 038 * indirectly) from daikon.inv.Invariant. To specify the output format, use one of the following: 039 * 040 * <dl> 041 * <dt>{@code --texinfo FILENAME} 042 * <dd>Texinfo format, for inclusion in the manual. 043 * <dt>{@code --text FILENAME} 044 * <dd>Text format, with each name preceded by "+" characters to indicate depth in the tree. 045 * </dl> 046 */ 047@SuppressWarnings({ 048 "nullness", // need help with this 049 "keyfor" // need help with this 050}) 051public class InvariantDoclet implements Doclet { 052 053 /** System line separator value. */ 054 private static final String lineSep = System.lineSeparator(); 055 056 /** 057 * Invariants that match any of the specified regular expressions are purposefully missing enable 058 * variables. Any others will throw an exception. 059 */ 060 private static @Regex String[] invs_without_enables = 061 new @Regex String[] { 062 "FunctionBinary.*", 063 ".*RangeFloat.*", 064 ".*RangeInt.*", 065 "AndJoiner", 066 "DummyInvariant", 067 "Equality", 068 "GuardingImplication", 069 "Implication", 070 }; 071 072 /** A value that indicates a method completed successfully. */ 073 private static final boolean OK = true; 074 075 /** A value that indicates a method did not complete successfully. */ 076 private static final boolean FAILED = false; 077 078 /** File name of destination for output. */ 079 private @Nullable String outFilename = null; 080 081 /** If true, then output format is Texinfo. */ 082 private boolean formatTexinfo = false; 083 084 /** The DocTrees instance assocated with the DocletEnvironment. */ 085 private DocTrees docTrees; 086 087 /** Used to report errors. */ 088 private Reporter reporter; 089 090 /** Map from a class name to its ClassInfo. */ 091 private Map<String, ClassInfo> cmap; 092 093 /** 094 * This method mimics the JDK 8 com.sun.javadoc.Doc.name() method. It returned a 'non-qualified 095 * name' which differs from JDK 11+ javax.lang.model.element.TypeElement.getSimpleName() for a 096 * nested class. name() would return 'Outer$Inner' while getSimpleName() returns 'Inner'. Note 097 * that this version returns 'Outer.Inner'. 098 * 099 * @param te a class TypeElement 100 * @return the class's 'partially' qualified name 101 */ 102 public static String getBinaryName(TypeElement te) { 103 String qualifiedName = te.getQualifiedName().toString(); 104 int i2 = qualifiedName.lastIndexOf('.'); 105 if (te.getNestingKind() == NestingKind.MEMBER) { 106 i2 = qualifiedName.substring(0, i2).lastIndexOf('.'); 107 } 108 return qualifiedName.substring(i2 + 1); 109 } 110 111 /** Compare TypeElements by their simple name. */ 112 static class sortBySimpleName implements Comparator<TypeElement> { 113 @Override 114 public int compare(TypeElement te1, TypeElement te2) { 115 if (te1 == null && te2 == null) { 116 return 0; 117 } 118 if (te1 == null) { 119 return -1; 120 } 121 if (te2 == null) { 122 return 1; 123 } 124 return te1.getSimpleName() 125 .toString() 126 .toLowerCase() 127 .compareTo(te2.getSimpleName().toString().toLowerCase()); 128 } 129 } 130 131 /** Compare TypeElements by their binary name. */ 132 static class sortByBinaryName implements Comparator<TypeElement> { 133 @Override 134 public int compare(TypeElement te1, TypeElement te2) { 135 if (te1 == null && te2 == null) { 136 return 0; 137 } 138 if (te1 == null) { 139 return -1; 140 } 141 if (te2 == null) { 142 return 1; 143 } 144 return getBinaryName(te1).toLowerCase().compareTo(getBinaryName(te2).toLowerCase()); 145 } 146 } 147 148 /** Compare TypeElements by their fully qualified name. */ 149 static class sortByQualifiedName implements Comparator<TypeElement> { 150 @Override 151 public int compare(TypeElement te1, TypeElement te2) { 152 if (te1 == null && te2 == null) { 153 return 0; 154 } 155 if (te1 == null) { 156 return -1; 157 } 158 if (te2 == null) { 159 return 1; 160 } 161 return te1.getQualifiedName() 162 .toString() 163 .toLowerCase() 164 .compareTo(te2.getQualifiedName().toString().toLowerCase()); 165 } 166 } 167 168 /** Container for class information. */ 169 static final class ClassInfo { 170 /** Javadoc class information. */ 171 final TypeElement classInfo; 172 173 /** 174 * Name of classInfo's superclass. Value is null if classInfo is a top level class or its parent 175 * is not included in list of classes passed to this doclet. 176 */ 177 String superClass; 178 179 /** Set of classInfo's derived classes. */ 180 final Set<TypeElement> derivedClasses; 181 182 /** 183 * Constructor for ClassInfo. 184 * 185 * @param classInfo initial value 186 * @param derivedClasses initial value 187 */ 188 public ClassInfo(TypeElement classInfo, Set<TypeElement> derivedClasses) { 189 this.classInfo = classInfo; 190 this.superClass = null; 191 this.derivedClasses = derivedClasses; 192 } 193 194 /** 195 * Setter for superClass. 196 * 197 * @param className new superClass value 198 */ 199 void setSuperClass(String className) { 200 superClass = className; 201 } 202 203 /** 204 * Getter for classInfo. 205 * 206 * @return classInfo 207 */ 208 TypeElement classInfo() { 209 return classInfo; 210 } 211 212 /** 213 * Getter for superClass. 214 * 215 * @return superClass 216 */ 217 String superClass() { 218 return superClass; 219 } 220 221 /** 222 * Getter for derivedClasses. 223 * 224 * @return derivedClasses 225 */ 226 Set<TypeElement> derivedClasses() { 227 return derivedClasses; 228 } 229 } 230 231 /////////////////////////////////////////////////////////////////////////// 232 /// Doclet-specific methods 233 /// 234 235 @Override 236 public void init(Locale locale, Reporter reporter) { 237 this.reporter = reporter; 238 } 239 240 @Override 241 public String getName() { 242 return getClass().getSimpleName(); 243 } 244 245 @Override 246 public Set<? extends Doclet.Option> getSupportedOptions() { 247 return docletOptions; 248 } 249 250 @Override 251 public SourceVersion getSupportedSourceVersion() { 252 return SourceVersion.latest(); 253 } 254 255 /** Entry point for this doclet (invoked by javadoc). */ 256 @Override 257 public boolean run(DocletEnvironment denv) { 258 boolean dump_class_tree = false; 259 260 postprocessOptions(); 261 docTrees = denv.getDocTrees(); 262 cmap = new TreeMap<>(); 263 264 // Save a list of class elements for subsequent processing 265 List<TypeElement> clazzes = new ArrayList<>(); 266 for (Element item : denv.getSpecifiedElements()) { 267 if (!isTypeElement(item)) { 268 throw new Error( 269 String.format("Unexpected specified element of kind %s: %s", item.getKind(), item)); 270 } 271 272 // save item for subsequent processing 273 clazzes.add((TypeElement) item); 274 // note that for TypeElement item: 275 // item.toString() == item.getQualifiedName().toString() 276 cmap.put( 277 item.toString(), 278 new ClassInfo((TypeElement) item, new TreeSet<TypeElement>(new sortByQualifiedName()))); 279 } 280 281 // go through the list again and put in the derived class information 282 for (TypeElement te : clazzes) { 283 TypeMirror superClass = te.getSuperclass(); 284 // TypeMirror toString may include type parameters, we need to remove them 285 String superName = superClass.toString().replaceFirst("<.*>", ""); 286 // System.out.println (te + ", " + superName + ", " + superClass.getKind()); 287 if (!superName.equals("none")) { 288 if (!cmap.containsKey(superName)) { 289 // System.out.println ("NO SUPER: " + te + " s: " + superName); 290 } else { 291 // System.out.println (" SUPER: " + te + " s: " + superName); 292 cmap.get(te.toString()).setSuperClass(superName); 293 Set<TypeElement> derived = cmap.get(superName).derivedClasses(); 294 derived.add(te); 295 } 296 } else { 297 // System.out.println ("NO SUPER: " + te + " s: null"); 298 } 299 } 300 301 if (dump_class_tree) { 302 // loop through each class in order 303 for (String className : cmap.keySet()) { 304 ClassInfo cd = cmap.get(className); 305 306 // if this is a top level class 307 if (cd.superClass() == null) { 308 process_class_tree_txt(System.out, cd, 0); 309 } 310 } 311 } 312 313 // do the specified work 314 System.out.println("Opening " + outFilename + " for output..."); 315 try (PrintStream outf = new PrintStream(new FileOutputStream(outFilename))) { 316 ClassInfo inv = cmap.get("daikon.inv.Invariant"); 317 if (formatTexinfo) { 318 process_class_sorted_texinfo(outf, inv); 319 } else { 320 process_class_tree_txt(outf, inv, 0); 321 } 322 } catch (Exception e) { 323 throw new Error("Unable to open file: " + outFilename, e); 324 } 325 326 return OK; 327 } 328 329 /** 330 * Prints a class and all of its derived classes as a simple indented tree. 331 * 332 * @param out stream to which to print 333 * @param cd starting class 334 * @param indent starting indent for the derived class (normally 0) 335 */ 336 public void process_class_tree_txt(PrintStream out, ClassInfo cd, int indent) { 337 338 // create the prefix string 339 String prefix = ""; 340 for (int i = 0; i < indent; i++) { 341 prefix += "+"; 342 } 343 344 TypeElement item = cd.classInfo(); 345 346 // put out this class 347 String is_abstract = ""; 348 if (item.getModifiers().contains(Modifier.ABSTRACT)) is_abstract = " (Abstract)"; 349 out.println(prefix + item + is_abstract); 350 String comment = getDocComment(docTrees.getDocCommentTree(item)); 351 comment = " " + comment; 352 comment = comment.replace(lineSep, lineSep + " "); 353 out.println(comment); 354 355 // put out each derived class 356 Set<TypeElement> derived = cd.derivedClasses(); 357 for (TypeElement dc : derived) { 358 process_class_tree_txt(out, cmap.get(dc.toString()), indent + 1); 359 } 360 } 361 362 /** 363 * Prints a class and all of its derived classes with their documentation in a simple sorted (by 364 * name) list in Texinfo format. Suitable for inclusion in the manual. 365 * 366 * @param out stream to which write output 367 * @param cd class to process 368 */ 369 public void process_class_sorted_texinfo(PrintStream out, @KeyFor("this.cmap") ClassInfo cd) { 370 371 out.println("@c BEGIN AUTO-GENERATED INVARIANTS LISTING"); 372 out.println("@c Automatically generated by " + getClass()); 373 out.println(); 374 375 // Function binary values 376 String last_fb = ""; 377 String fb_type = ""; 378 String permutes = ""; 379 String last_comment = ""; 380 int permute_cnt = 0; 381 382 TreeSet<TypeElement> list = new TreeSet<>(new sortByBinaryName()); 383 gather_derived_classes(cd, list); 384 for (TypeElement dc : list) { 385 if (dc.getModifiers().contains(Modifier.ABSTRACT)) { 386 continue; 387 } 388 // skip daikon.test.diff.DiffDummyInvariant 389 if (dc.getQualifiedName().toString().indexOf(".test.") != -1) { 390 continue; 391 } 392 393 // setup the comment for info 394 String comment = getDocComment(docTrees.getDocCommentTree(dc)); 395 396 // System.out.println(dc.getQualifiedName() + ": COMMENT: " + comment); 397 comment = HtmlToTexinfo.javadocHtmlToTexinfo(comment); 398 399 String name = getBinaryName(dc); 400 if (name.startsWith("FunctionBinary")) { 401 String[] parts = name.split("[._]"); 402 String fb_function = parts[1]; 403 String fb_permute = parts[2]; 404 if (last_fb.equals(fb_function)) { 405 permutes += ", " + fb_permute; 406 permute_cnt++; 407 } else /* new type of function binary */ { 408 if (!last_fb.equals("")) { // actually, == test would work here 409 out.println(); 410 out.println("@item " + fb_type + "." + last_fb + "_@{" + permutes + "@}"); 411 out.println(last_comment); 412 assert (permute_cnt == 3) || (permute_cnt == 6); 413 if (permute_cnt == 3) { 414 out.println( 415 "Since the function is symmetric, only the " 416 + "permutations xyz, yxz, and zxy are checked."); 417 } else { 418 out.println( 419 "Since the function is non-symmetric, all six " 420 + "permutations of the variables are checked."); 421 } 422 } 423 last_fb = fb_function; 424 permutes = fb_permute; 425 last_comment = comment; 426 fb_type = parts[0]; 427 permute_cnt = 1; 428 } 429 } else { 430 out.println(); 431 out.println("@item " + name); 432 out.println(comment); 433 } 434 435 // Make sure that all invariants have enable variables 436 Boolean enabledInitValue = find_enabled(dc); 437 if (enabledInitValue == null) { 438 boolean ok_without_enable = false; 439 for (String re : invs_without_enables) { 440 if (name.matches("^" + re + "$")) { 441 ok_without_enable = true; 442 break; 443 } 444 } 445 if (!ok_without_enable) { 446 throw new Error("No enable variable for " + name); 447 } 448 } 449 450 // Note whether this invariant is turned off by default 451 if (enabledInitValue != null && !enabledInitValue) { 452 out.println(); 453 out.println("This invariant is not enabled by default. See the configuration option"); 454 out.println("@samp{" + dc + ".enabled}."); 455 } 456 457 // get a list of any other configuration variables 458 List<Element> config_vars = find_fields(dc, Configuration.PREFIX); 459 for (int i = 0; i < config_vars.size(); i++) { 460 Element f = config_vars.get(i); 461 if (f.getSimpleName().toString().equals(Configuration.PREFIX + "enabled")) { 462 config_vars.remove(i); 463 break; 464 } 465 } 466 467 // Note the other configuration variables 468 469 if (config_vars.size() > 0) { 470 out.println(); 471 out.println( 472 "See also the following configuration option" 473 + (config_vars.size() > 1 ? "s" : "") 474 + ":"); 475 out.println(" @itemize @bullet"); 476 for (Element f : config_vars) { 477 out.print(" @item "); 478 String fullname = f.getEnclosingElement().toString() + "." + f.getSimpleName(); 479 out.println("@samp{" + fullname.replace(Configuration.PREFIX, "") + "}"); 480 } 481 out.println(" @end itemize"); 482 } 483 } 484 485 out.println(); 486 out.println("@c END AUTO-GENERATED INVARIANTS LISTING"); 487 } 488 489 /** 490 * Gathers up all of the classes under cd and adds them to the specified TreeSet. They are sorted 491 * by their name. 492 * 493 * @param cd the base class from which to start the search 494 * @param set the set to add classes to. Should start out empty. 495 */ 496 public void gather_derived_classes(@KeyFor("this.cmap") ClassInfo cd, TreeSet<TypeElement> set) { 497 assert cmap.containsKey(cd.classInfo().toString()); 498 499 // System.out.println("Processing " + cd.classInfo()); 500 Set<TypeElement> derived = cd.derivedClasses(); 501 for (TypeElement te : derived) { 502 set.add(te); 503 gather_derived_classes(cmap.get(te.toString()), set); 504 } 505 } 506 507 /** 508 * Looks for a field named dkconfig_enabled in the class and find out what it is initialized to. 509 * 510 * @param cd class in which to look for dkconfig_enabled 511 * @return the setting for the dkconfig_enabled variable in the class, or null if no such field 512 */ 513 public @Nullable Boolean find_enabled(TypeElement cd) { 514 515 String enable_name = Configuration.PREFIX + "enabled"; 516 // System.out.println("Looking for " + enable_name); 517 518 for (Element field : cd.getEnclosedElements()) { 519 if (field.getKind() != ElementKind.FIELD) continue; 520 if (enable_name.equals(field.getSimpleName().toString())) { 521 // System.out.println("Found in " + field.getEnclosingElement()); 522 try { 523 String fullname = field.getEnclosingElement().toString() + "." + field.getSimpleName(); 524 int i = fullname.lastIndexOf('.'); 525 @SuppressWarnings("signature") // application invariant, substring 526 @ClassGetName String classname = fullname.substring(0, i); 527 Class<?> c; 528 try { 529 c = ReflectionPlume.classForName(classname); 530 } catch (Throwable e) { 531 throw new Error( 532 String.format( 533 "Exception in ReflectionPlume.classForName(%s); fullname=%s%n", 534 classname, fullname), 535 e); 536 } 537 Field f = c.getField(enable_name); 538 @SuppressWarnings("nullness") // f has boolean type, so result is non-null Boolean 539 @NonNull Object value = f.get(null); 540 return (Boolean) value; 541 } catch (Exception e) { 542 throw new Error("In find_enabled(" + cd + ")", e); 543 } 544 } 545 } 546 return null; 547 } 548 549 /** 550 * Look for fields in the specified class that begin with the specified prefix. 551 * 552 * @param cd the class to search 553 * @param prefix string that must be at the beginning of the field name 554 * @return vector of FieldDoc entries for each field that matches. If no fields are found, a zero 555 * length vector is returned (not null). 556 */ 557 public List<Element> find_fields(TypeElement cd, String prefix) { 558 559 List<Element> list = new ArrayList<>(); 560 561 for (Element field : cd.getEnclosedElements()) { 562 if (field.getKind() != ElementKind.FIELD) continue; 563 if (field.getSimpleName().toString().startsWith(prefix)) { 564 list.add(field); 565 } 566 } 567 568 return list; 569 } 570 571 /////////////////////////////////////////////////////////////////////////// 572 /// Helper methods 573 /// 574 575 /** 576 * Fetch the comment string from a DocCommentTree node. 577 * 578 * @param node the DocCommentTree to process 579 * @return the entire body of the documentation comment as a string. If no comments are found, an 580 * empty string is returned (not null). 581 */ 582 public String getDocComment(DocCommentTree node) { 583 if (node == null) return ""; 584 StringBuilder sb = new StringBuilder(); 585 for (DocTree t : node.getFullBody()) { 586 sb.append(t.toString()); 587 } 588 return sb.toString(); 589 } 590 591 /** 592 * Returns true if the given element kind is a type, i.e., a class, enum, interface, or annotation 593 * type. 594 * 595 * @param element the element to test 596 * @return true, iff the given kind is a type kind 597 */ 598 public static boolean isTypeElement(Element element) { 599 ElementKind elementKind = element.getKind(); 600 return elementKind.isClass() || elementKind.isInterface(); 601 } 602 603 /////////////////////////////////////////////////////////////////////////// 604 /// Javadoc command-line options 605 /// 606 607 // The doclet cannot use the Options class itself because Javadoc specifies its own way of 608 // handling command-line arguments. 609 610 /** 611 * A command-line option to the Javadoc doclet; implements the {@link Doclet.Option} interface. 612 * 613 * <p>Is abstract because it does not provide an implementation of the {@code process} method. 614 */ 615 abstract static class DocletOption implements Doclet.Option { 616 /** The number of arguments this option will consume. */ 617 private int argumentCount; 618 619 /** The user-friendly description of the option. */ 620 private String description; 621 622 /** 623 * The list of names and aliases that may be used to identify the option. Each starts with "-" 624 * or "--". 625 */ 626 private List<String> names; 627 628 /** 629 * A user-friendly string description of the option's parameters, or the empty string if this 630 * option has no parameters. 631 */ 632 private String parameters; 633 634 /** 635 * Creates a new DocletOption. 636 * 637 * @param name the option's name, starting with "-" or "--" 638 * @param parameters a user-friendly string description of the option's parameters, or the empty 639 * string if this option has no parameters 640 * @param argumentCount the number of arguments this option will consume 641 * @param description the user-friendly description of the option 642 */ 643 DocletOption(String name, String parameters, int argumentCount, String description) { 644 this.argumentCount = argumentCount; 645 this.description = description; 646 this.names = List.of(name); 647 this.parameters = parameters; 648 } 649 650 /** 651 * Creates a new DocletOption with an aliased name 652 * 653 * @param name the option's name, starting with "-" or "--" 654 * @param alias the option's alias, starting with "-" or "--" 655 * @param parameters a user-friendly string description of the option's parameters, or the empty 656 * string if this option has no parameters 657 * @param argumentCount the number of arguments this option will consume 658 * @param description the user-friendly description of the option 659 */ 660 DocletOption( 661 String name, String alias, String parameters, int argumentCount, String description) { 662 this.argumentCount = argumentCount; 663 this.description = description; 664 this.names = List.of(name, alias); 665 this.parameters = parameters; 666 } 667 668 @Override 669 public int getArgumentCount() { 670 return argumentCount; 671 } 672 673 @Override 674 public String getDescription() { 675 return description; 676 } 677 678 @Override 679 public Doclet.Option.Kind getKind() { 680 return Doclet.Option.Kind.STANDARD; 681 } 682 683 @Override 684 public List<String> getNames() { 685 return names; 686 } 687 688 @Override 689 public String getParameters() { 690 return parameters; 691 } 692 } 693 694 /** The command-line options for OptionsDoclet. */ 695 @SuppressWarnings( 696 "nullness:method.invocation" // when methods such as printError() are called, the receiver 697 // (an OptionsDoclet) is initialized 698 ) 699 private final Set<DocletOption> docletOptions = 700 Set.of( 701 new DocletOption( 702 "--texinfo", "-texinfo", "file", 1, "write texinfo format output to this file") { 703 @Override 704 public boolean process(String option, List<String> arguments) { 705 if (outFilename != null) { 706 printError("must only specify one output option"); 707 return FAILED; 708 } 709 outFilename = arguments.get(0); 710 formatTexinfo = true; 711 return OK; 712 } 713 }, 714 new DocletOption("--text", "-text", "file", 1, "write text format output to this file") { 715 @Override 716 public boolean process(String option, List<String> arguments) { 717 if (outFilename != null) { 718 printError("must only specify one output option"); 719 return FAILED; 720 } 721 outFilename = arguments.get(0); 722 formatTexinfo = false; 723 return OK; 724 } 725 }); 726 727 /** 728 * Sets variables that can only be set after all command-line options have been processed. Isuses 729 * errors and halts if any command-line options are incompatible with one another. 730 */ 731 private void postprocessOptions() { 732 boolean hasError = false; 733 if (outFilename == null) { 734 printError("either --texinfo or --text must be specified"); 735 hasError = true; 736 } 737 738 if (hasError) { 739 System.exit(1); 740 } 741 } 742 743 /** 744 * Print error message via delegation to {@link #reporter}. 745 * 746 * @param msg message to print 747 */ 748 private void printError(String msg) { 749 reporter.print(Diagnostic.Kind.ERROR, msg); 750 } 751}