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}