001package daikon;
002
003import daikon.PptTopLevel.PptType;
004import java.io.File;
005import java.io.IOException;
006import java.util.ArrayList;
007import java.util.LinkedHashMap;
008import java.util.List;
009import java.util.Map;
010import org.checkerframework.checker.interning.qual.Interned;
011import org.checkerframework.checker.nullness.qual.KeyFor;
012import org.checkerframework.checker.nullness.qual.NonNull;
013import org.checkerframework.checker.nullness.qual.Nullable;
014import org.checkerframework.dataflow.qual.Pure;
015import org.plumelib.options.Option;
016import org.plumelib.options.Options;
017import org.plumelib.reflection.Signatures;
018
019/**
020 * AnnotateNullable reads a Daikon invariant file and determines which reference variables have seen
021 * any null values. It writes to standard out an <a
022 * href="https://checkerframework.org/annotation-file-utilities/annotation-file-format.html">annotation
023 * file</a> with those variables. It determines which variables have seen null values by looking at
024 * the NonZero invariant. If that invariant is NOT present, then the variable must have been null at
025 * least once.
026 *
027 * <p>Since only the NonZero invariant is used, Daikon processing time can be significantly reduced
028 * by turning off derived variables and all invariants other than daikon.inv.unary.scalar.NonZero.
029 * This is not necessary, however, for correct operation. File {@code annotate_nullable.config} in
030 * the distribution does this.
031 */
032public class AnnotateNullable {
033
034  // Why is this variable static?
035  static PptMap ppts = new PptMap(); // dummy value, to satisfy Nullness Checker
036
037  // static SimpleLog verbose = new SimpleLog(/*enabled=*/ false);
038  // static SimpleLog debug = new SimpleLog(/*enabled=*/ false);
039
040  /** Map from a class name to the list of static functions for that class. */
041  static Map<String, List<PptTopLevel>> class_map = new LinkedHashMap<>();
042
043  // The package for the previous class.  Used to reduce duplication in
044  // output file.
045  static @Interned String last_package = "";
046
047  /**
048   * Write an output file in the stub class format (see the Checker Framework Manual), instead of in
049   * annotation file format.
050   */
051  @Option("Use the stub class file format")
052  public static boolean stub_format = false;
053
054  /**
055   * Adds NonNull annotations as well as Nullable annotations. Unlike Nullable annotations, NonNull
056   * annotations are not necessarily correct.
057   */
058  @Option("-n Insert NonNull as well as Nullable annotations")
059  public static boolean nonnull_annotations = false;
060
061  public static void main(String[] args) throws IOException {
062
063    Options options =
064        new Options("plume.AnnotateNullable [options] <inv_file>", AnnotateNullable.class);
065    String[] inv_files = options.parse(true, args);
066    assert inv_files.length == 1;
067
068    // Read the serialized invariant file
069    File inv_file = new File(inv_files[0]);
070    ppts = FileIO.read_serialized_pptmap(inv_file, true);
071    Daikon.all_ppts = ppts;
072    // verbose.log("Finished reading %d program points", ppts.size());
073
074    // Setup the list of proto invariants and initialize NIS suppressions
075    Daikon.setup_proto_invs();
076    Daikon.setup_NISuppression();
077
078    // Write out the definitions of our annotations
079    if (stub_format) {
080      System.out.println("import org.checkerframework.checker.nullness.qual.Nullable;");
081      System.out.println("import org.checkerframework.checker.nullness.qual.NonNull;");
082      System.out.println();
083    } else {
084      System.out.println("package org.checkerframework.checker.nullness.qual:");
085      System.out.println("annotation @Nullable:");
086      System.out.println("annotation @NonNull:");
087      System.out.println();
088    }
089
090    // Find all exit ppts that do not have a parent and determine what
091    // class they are associated with.  These are static methods for classes
092    // without any static variables (no class ppt is created if there are no
093    // static variables)
094
095    // First find all of the classes
096    for (PptTopLevel ppt : ppts.pptIterable()) {
097      if (ppt.is_object()) {
098        String classname = ppt.name().replace(":::OBJECT", "");
099        assert !class_map.containsKey(classname) : classname;
100        List<PptTopLevel> static_methods = new ArrayList<>();
101        class_map.put(classname, static_methods);
102      }
103    }
104
105    // Then, add combined exit points for static methods to their class.  A
106    // static method can be identified because it will not have the OBJECT
107    // point as a parent.
108    for (PptTopLevel ppt : ppts.pptIterable()) {
109      if (!ppt.is_combined_exit() || !is_static_method(ppt)) {
110        continue;
111      }
112
113      String name = ppt.name().replaceFirst("[(].*$", "");
114      int lastdot = name.lastIndexOf('.');
115      @SuppressWarnings("keyfor") // application invariant:  KeyFor and substring
116      // @KeyFor because class_map has entry per class, and this method is in some class
117      @KeyFor("class_map") String classname = name.substring(0, lastdot);
118      // System.out.printf("classname for ppt %s is '%s'%n", name, classname);
119      @NonNull List<PptTopLevel> static_methods = class_map.get(classname);
120      assert static_methods != null : classname;
121      static_methods.add(ppt);
122    }
123
124    // Debug print all of the static methods
125    if (false) {
126      for (String classname : class_map.keySet()) {
127        System.out.printf("class %s static methods: %s%n", classname, class_map.get(classname));
128      }
129    }
130
131    // Make sure that the static methods found by inference, match those
132    // found for any class ppts
133    for (PptTopLevel ppt : ppts.pptIterable()) {
134      if (ppt.is_class()) {
135        @SuppressWarnings(
136            "nullness") // map: retrieve class name from class Ppt name, with string manipulation
137        @NonNull List<PptTopLevel> static_methods =
138            class_map.get(ppt.name().replace(":::CLASS", ""));
139        int child_cnt = 0;
140        // TODO: Once Checker Framework issue 565 has been fixed (https://tinyurl.com/cfissue/565),
141        // change the following two lines back to
142        // for (PptRelation child_rel : ppt.children) {
143        for (int i = 0; i < ppt.children.size(); i++) {
144          PptRelation child_rel = ppt.children.get(i);
145          PptTopLevel child = child_rel.child;
146          // Skip enter ppts, all of the info is at the exit.
147          if ((child.type == PptType.ENTER) || (child.type == PptType.OBJECT)) {
148            continue;
149          }
150          child_cnt++;
151          assert static_methods.contains(child) : child;
152        }
153        assert child_cnt == static_methods.size() : static_methods;
154      }
155    }
156
157    // Process each class.
158    for (PptTopLevel ppt : ppts.pptIterable()) {
159
160      // Skip synthetic program points
161      if (ppt.name().startsWith("$")) {
162        continue;
163      }
164
165      // Skip program points that are not OBJECT ppts
166      if (ppt.is_object()) {
167        process_class(ppt);
168      }
169    }
170  }
171
172  // Returns null if no corresponding class ppt exists
173  private static @Nullable PptTopLevel class_for_object(PptTopLevel object_ppt) {
174    if (object_ppt.parents.size() == 0) {
175      return null;
176    }
177    assert object_ppt.parents.size() == 1 : object_ppt;
178    return object_ppt.parents.get(0).parent;
179  }
180
181  // Process a class, including all its methods.
182  // Takes the object program point as its argument.
183  public static void process_class(PptTopLevel object_ppt) {
184
185    // Get the class program point (if any)
186    PptTopLevel class_ppt = class_for_object(object_ppt);
187
188    String class_samples = "-";
189    if (class_ppt != null) {
190      class_samples = String.format("%d", class_ppt.num_samples());
191    }
192    String ppt_package = object_ppt.ppt_name.getPackageName();
193    if (ppt_package == null) {
194      ppt_package = "";
195    } else {
196      ppt_package = ppt_package.intern();
197    }
198    if (stub_format) {
199      if (ppt_package != last_package) {
200        // This will print the empty string if we switch from a package to the
201        // unnamed package.  That is intentional.
202        System.out.printf("package %s;%n%n", ppt_package);
203        last_package = ppt_package;
204      }
205      System.out.printf(
206          "class %s { // %d/%s obj/class samples%n",
207          object_ppt.ppt_name.getFullClassName(), object_ppt.num_samples(), class_samples);
208    } else {
209      System.out.printf("package %s:%n", ppt_package);
210      System.out.printf(
211          "class %s : // %d/%s obj/class samples%n",
212          object_ppt.ppt_name.getShortClassName(), object_ppt.num_samples(), class_samples);
213    }
214
215    // Process static fields
216    if (class_ppt != null) {
217      process_obj_fields(class_ppt);
218    }
219
220    // Process member (non-static) fields
221    process_obj_fields(object_ppt);
222
223    // Process static methods
224    if (class_ppt != null) {
225      for (PptRelation child_rel : class_ppt.children) {
226        PptTopLevel child = child_rel.child;
227        // Skip enter ppts, all of the info is at the exit.
228        if ((child.type == PptType.ENTER) || (child.type == PptType.OBJECT)) {
229          continue;
230        }
231        // debug.log("processing static method %s, type %s", child, child.type);
232        process_method(child);
233      }
234    } else {
235      String classname = object_ppt.ppt_name.getFullClassName();
236      assert classname != null;
237      @SuppressWarnings("nullness") // map: class_map has entry per classname
238      @NonNull List<PptTopLevel> static_methods = class_map.get(classname);
239      assert static_methods != null : classname;
240      for (PptTopLevel child : static_methods) {
241        process_method(child);
242      }
243    }
244
245    // Process member (non-static) methods
246    for (PptRelation child_rel : object_ppt.children) {
247      PptTopLevel child = child_rel.child;
248      // Skip enter ppts, all of the info is at the exit.
249      if (child.type == PptType.ENTER) {
250        continue;
251      }
252      // debug.log("processing method %s, type %s", child, child.type);
253      process_method(child);
254    }
255
256    if (stub_format) {
257      System.out.printf("}");
258    }
259    System.out.println();
260    System.out.println();
261  }
262
263  /**
264   * Get the annotation for the specified variable. Returns @Nullable if samples were found for this
265   * variable and at least one sample contained a null value. Returns an (interned) empty string if
266   * no annotation is applicable. Otherwise, the return value contains a trailing space.
267   */
268  public static String get_annotation(PptTopLevel ppt, VarInfo vi) {
269
270    if (vi.type.isPrimitive()) {
271      return "";
272    }
273
274    String annotation = (nonnull_annotations ? "NonNull" : "");
275    if ((ppt.num_samples(vi) > 0) && !ppt.is_nonzero(vi)) {
276      annotation = "Nullable";
277    }
278    if (annotation != "") { // interned
279      // if (! stub_format) {
280      //   annotation = "org.checkerframework.checker.nullness.qual." + annotation;
281      // }
282      annotation = "@" + annotation;
283    }
284    return annotation;
285  }
286
287  /** Print out the annotations for the specified method. */
288  public static void process_method(PptTopLevel ppt) {
289
290    assert ppt.type == PptType.EXIT : ppt;
291
292    // Get all of the parameters to the method and the return value
293    List<VarInfo> params = new ArrayList<>();
294    VarInfo retvar = null;
295    for (VarInfo vi : ppt.var_infos) {
296      if (vi.var_kind == VarInfo.VarKind.RETURN) {
297        retvar = vi;
298      } else {
299        if (vi.isParam()
300            && (vi.name() != "this") // interned
301            && !vi.isPrestate()) {
302          params.add(vi);
303        }
304      }
305    }
306
307    // The formatted annotation for the return value with a leading space, or empty string
308    String return_annotation = (retvar == null ? "" : " " + get_annotation(ppt, retvar));
309
310    // Look up the annotation for each parameter.
311    List<String> names = new ArrayList<>();
312    List<String> annos = new ArrayList<>();
313    for (VarInfo param : params) {
314      String annotation = "";
315      names.add(param.name());
316      if (param.file_rep_type.isHashcode()) {
317        annotation = get_annotation(ppt, param);
318      }
319      annos.add(annotation);
320    }
321
322    // Print out the method declaration
323    if (stub_format) {
324      System.out.printf(" %s %s(", return_annotation, ppt.ppt_name.getMethodName());
325      for (int i = 0; i < params.size(); i++) {
326        if (i != 0) {
327          System.out.printf(" ,");
328        }
329        System.out.printf("%s %s %s", annos.get(i), "type-goes-here", names.get(i));
330      }
331      System.out.printf("); // %d samples%n", ppt.num_samples());
332    } else {
333      System.out.printf("  method %s : // %d samples%n", jvm_signature(ppt), ppt.num_samples());
334      System.out.printf("    return:%s%n", return_annotation);
335      for (int i = 0; i < params.size(); i++) {
336        // Print the annotation for this parameter
337        System.out.printf("    parameter #%d : %s // %s%n", i, annos.get(i), names.get(i));
338      }
339    }
340  }
341
342  /** Print out the annotations for each field in the object or class. */
343  public static void process_obj_fields(PptTopLevel ppt) {
344
345    for (VarInfo vi : ppt.var_infos) {
346
347      assert !vi.isPrestate() : vi;
348
349      // Skip anyone with a parent in the hierarchy.  We are only
350      // interested in them at the top (e.g., we don't want to see
351      // object fields in each method).
352      if (!vi.parents.isEmpty()) {
353        continue;
354      }
355
356      // Skip variables that are always non-null.
357      if (vi.aux.isNonNull()) {
358        continue;
359      }
360
361      // Skip any variable that is enclosed by a variable other than 'this'.
362      // These are fields and can only be annotated where they are declared.
363      VarInfo evar = vi.get_enclosing_var();
364      if ((evar != null) && !evar.name().equals("this")) {
365        // System.out.printf("  enclosed %s %s%n", vi.type, vi.name());
366        continue;
367      }
368
369      // print out the entry for this field
370      String annotation = "";
371      if (vi.file_rep_type.isHashcode()) {
372        annotation = get_annotation(ppt, vi);
373      }
374      if (stub_format) {
375        System.out.printf("  field %s %s {} // %s%n", field_name(vi), annotation, vi.type);
376      } else {
377        System.out.printf("  field %s : %s // %s%n", field_name(vi), annotation, vi.type);
378      }
379    }
380  }
381
382  /** Returns a JVM signature for the method. */
383  public static String jvm_signature(PptTopLevel ppt) {
384
385    @SuppressWarnings("nullness") // Java method, so getMethodName() != null
386    @NonNull String method = ppt.ppt_name.getMethodName();
387    @SuppressWarnings("nullness") // Java method, so getSignature() != null
388    @NonNull String java_sig = ppt.ppt_name.getSignature();
389    String java_args = java_sig.replace(method, "");
390    // System.out.printf("m/s/a = %s %s %s%n", method, java_sig, java_args);
391    if (method.equals(ppt.ppt_name.getShortClassName())) {
392      method = "<init>";
393    }
394
395    // Problem:  I need the return type, but Chicory does not output it.
396    // So, I could try to retrieve it from the "return" variable in the
397    // program point (which is, fortunately, always an exit point), or
398    // change Chicory to output it.
399    VarInfo returnVar = ppt.find_var_by_name("return");
400    @SuppressWarnings(
401        "signature" // application invariant: returnVar.type.toString() is a binary name (if
402    // returnVar is non-null), because we are processing a Java program
403    )
404    String returnType =
405        returnVar == null ? "V" : Signatures.binaryNameToFieldDescriptor(returnVar.type.toString());
406
407    return method + Signatures.arglistToJvm(java_args) + returnType;
408  }
409
410  /**
411   * Returns the field name of the specified variable. This is the relative name for instance
412   * fields, but the relative name is not specified for static fields (because there is no enclosing
413   * variable with the full name). The field name is obtained in that case, by removing the
414   * package/class specifier.
415   */
416  public static String field_name(VarInfo vi) {
417
418    if (vi.relative_name != null) {
419      return vi.relative_name;
420    }
421
422    String field_name = vi.name();
423    int pt = field_name.lastIndexOf('.');
424    if (pt == -1) {
425      return field_name;
426    } else {
427      return field_name.substring(pt + 1);
428    }
429  }
430
431  /**
432   * Returns whether or not the method of the specified ppt is static or not. The ppt must be an
433   * exit ppt. Exit ppts that do not have an object as a parent are inferred to be static. This does
434   * not work for enter ppts, because constructors do not have the object as a parent on entry.
435   */
436  @Pure
437  public static boolean is_static_method(PptTopLevel ppt) {
438
439    assert ppt.is_exit() : ppt;
440    for (PptRelation rel : ppt.parents) {
441      if (rel.parent.is_object()) {
442        return false;
443      }
444    }
445
446    return true;
447  }
448}