Optionally limits the length of the output of Printer.printList() (default = no).

--
MOS_MIGRATED_REVID=104655508
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Printer.java b/src/main/java/com/google/devtools/build/lib/syntax/Printer.java
index 0fa2964..26e6fda 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Printer.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Printer.java
@@ -14,6 +14,8 @@
 package com.google.devtools.build.lib.syntax;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
 import com.google.devtools.build.lib.vfs.PathFragment;
 
@@ -27,6 +29,8 @@
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * (Pretty) Printing of Skylark values
@@ -35,6 +39,19 @@
 
   private static final char SKYLARK_QUOTATION_MARK = '"';
 
+  /*
+   * Suggested maximum number of list elements that should be printed via printList().
+   * By default, this setting is not considered and no limitation takes place.
+   */
+  static final int SUGGESTED_CRITICAL_LIST_ELEMENTS_COUNT = 4;
+
+  /*
+   * Suggested limit for printList() to shorten the values of list elements when their combined
+   * string length reaches this value.
+   * By default, this setting is not considered and no limitation takes place.
+   */
+  static final int SUGGESTED_CRITICAL_LIST_ELEMENTS_STRING_LENGTH = 32;
+
   private Printer() {
   }
 
@@ -257,16 +274,41 @@
       String after,
       String singletonTerminator,
       char quotationMark) {
-    boolean printSeparator = false; // don't print the separator before the first element
-    int len = 0;
+    return printList(
+        buffer, list, before, separator, after, singletonTerminator, quotationMark, -1, -1);
+  }
+
+  /**
+   * Print a list of object representations.
+   *
+   * <p>The length of the output will be limited when both {@code maxItemsToPrint} and {@code
+   * criticalItemsStringLength} have values greater than zero.
+   *
+   * @param buffer an appendable buffer onto which to write the list.
+   * @param list the list of objects to write (each as with repr)
+   * @param before a string to print before the list
+   * @param separator a separator to print between each object
+   * @param after a string to print after the list
+   * @param singletonTerminator null or a string to print after the list if it is a singleton
+   * The singleton case is notably relied upon in python syntax to distinguish
+   *    a tuple of size one such as ("foo",) from a merely parenthesized object such as ("foo").
+   * @param quotationMark The quotation mark to be used (' or ")
+   * @param maxItemsToPrint the maximum number of elements to be printed.
+   * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
+   *    'Soft' means that this limit may be exceeded because of formatting.
+   * @return the Appendable, in fluent style.
+   */
+  public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
+      String separator, String after, String singletonTerminator, char quotationMark,
+      int maxItemsToPrint, int criticalItemsStringLength) {
     append(buffer, before);
-    for (Object o : list) {
-      if (printSeparator) {
-        append(buffer, separator);
-      }
-      write(buffer, o, quotationMark);
-      printSeparator = true;
-      len++;
+    int len = 0;
+    // Limits the total length of the string representation of the elements, if specified.
+    if (maxItemsToPrint > 0 && criticalItemsStringLength > 0) {
+      len = appendListElements(LengthLimitedAppendable.create(buffer, criticalItemsStringLength),
+          list, separator, quotationMark, maxItemsToPrint);
+    } else {
+      len = appendListElements(buffer, list, separator, quotationMark);
     }
     if (singletonTerminator != null && len == 1) {
       append(buffer, singletonTerminator);
@@ -275,6 +317,68 @@
   }
 
   public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
+      String separator, String after, String singletonTerminator, int maxItemsToPrint,
+      int criticalItemsStringLength) {
+    return printList(buffer, list, before, separator, after, singletonTerminator,
+        SKYLARK_QUOTATION_MARK, maxItemsToPrint, criticalItemsStringLength);
+  }
+
+  /**
+   * Appends the given elements to the specified {@link Appendable} and returns the number of
+   * elements.
+   */
+  private static int appendListElements(
+      Appendable appendable, Iterable<?> list, String separator, char quotationMark) {
+    boolean printSeparator = false; // don't print the separator before the first element
+    int len = 0;
+    for (Object o : list) {
+      if (printSeparator) {
+        append(appendable, separator);
+      }
+      write(appendable, o, quotationMark);
+      printSeparator = true;
+      len++;
+    }
+    return len;
+  }
+
+  /**
+   * Tries to append the given elements to the specified {@link Appendable} until specific limits
+   * are reached.
+   * @return the number of appended elements.
+   */
+  private static int appendListElements(LengthLimitedAppendable appendable, Iterable<?> list,
+      String separator, char quotationMark, int maxItemsToPrint) {
+    boolean printSeparator = false; // don't print the separator before the first element
+    boolean skipArgs = false;
+    int items = Iterables.size(list);
+    int len = 0;
+    // We don't want to print "1 more arguments", hence we don't skip arguments if there is only one
+    // above the limit.
+    int itemsToPrint = (items - maxItemsToPrint == 1) ? items : maxItemsToPrint;
+    appendable.enforceLimit();
+    for (Object o : list) {
+      // We don't want to print "1 more arguments", even if we hit the string limit.
+      if (len == itemsToPrint || (appendable.hasHitLimit() && len < items - 1)) {
+        skipArgs = true;
+        break;
+      }
+      if (printSeparator) {
+        append(appendable, separator);
+      }
+      write(appendable, o, quotationMark);
+      printSeparator = true;
+      len++;
+    }
+    appendable.ignoreLimit();
+    if (skipArgs) {
+      append(appendable, separator);
+      append(appendable, String.format("<%d more arguments>", items - len));
+    }
+    return len;
+  }
+
+  public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
       String separator, String after, String singletonTerminator) {
     return printList(
         buffer, list, before, separator, after, singletonTerminator, SKYLARK_QUOTATION_MARK);
@@ -286,15 +390,25 @@
    * @param list the contents of the list or tuple
    * @param isTuple is it a tuple or a list?
    * @param quotationMark The quotation mark to be used (' or ")
+   * @param maxItemsToPrint the maximum number of elements to be printed.
+   * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
+   * 'Soft' means that this limit may be exceeded because of formatting.
    * @return the Appendable, in fluent style.
    */
+  public static Appendable printList(Appendable buffer, Iterable<?> list, boolean isTuple,
+      char quotationMark, int maxItemsToPrint, int criticalItemsStringLength) {
+    if (isTuple) {
+      return printList(buffer, list, "(", ", ", ")", ",", quotationMark, maxItemsToPrint,
+          criticalItemsStringLength);
+    } else {
+      return printList(buffer, list, "[", ", ", "]", null, quotationMark, maxItemsToPrint,
+          criticalItemsStringLength);
+    }
+  }
+
   public static Appendable printList(
       Appendable buffer, Iterable<?> list, boolean isTuple, char quotationMark) {
-    if (isTuple) {
-      return printList(buffer, list, "(", ", ", ")", ",", quotationMark);
-    } else {
-      return printList(buffer, list, "[", ", ", "]", null, quotationMark);
-    }
+    return printList(buffer, list, isTuple, quotationMark, -1, -1);
   }
 
   /**
@@ -443,4 +557,134 @@
     }
     return buffer;
   }
+
+  /**
+   * Helper class for {@code Appendable}s that want to limit the length of their input.
+   *
+   * <p>Instances of this class act as a proxy for one {@code Appendable} object and decide whether
+   * the given input (or parts of it) can be written to the underlying {@code Appendable}, depending
+   * on whether the specified maximum length has been met or not.
+   */
+  private static final class LengthLimitedAppendable implements Appendable {
+
+    private static final ImmutableSet<Character> SPECIAL_CHARS =
+        ImmutableSet.of(',', ' ', '"', '\'', ':', '(', ')', '[', ']', '{', '}');
+
+    private static final Pattern ARGS_PATTERN = Pattern.compile("<\\d+ more arguments>");
+
+    private final Appendable original;
+    private int limit;
+    private boolean ignoreLimit;
+    private boolean previouslyShortened;
+    
+    private LengthLimitedAppendable(Appendable original, int limit) {
+      this.original = original;
+      this.limit = limit;
+    }
+
+    public static LengthLimitedAppendable create(Appendable original, int limit) {
+      // We don't want to overwrite the limit if original is already an instance of this class.
+      return (original instanceof LengthLimitedAppendable)
+          ? (LengthLimitedAppendable) original : new LengthLimitedAppendable(original, limit);
+    }
+
+    @Override
+    public Appendable append(CharSequence csq) throws IOException {
+      if (ignoreLimit || hasOnlySpecialChars(csq)) {
+        // Don't update limit.
+        original.append(csq);
+        previouslyShortened = false;
+      } else {
+        int length = csq.length();
+        if (length <= limit) {
+          limit -= length;
+          original.append(csq);
+        } else {
+          original.append(csq, 0, limit);
+          // We don't want to append multiple ellipses.
+          if (!previouslyShortened) {
+            original.append("...");
+          }
+          appendTrailingSpecialChars(csq, limit);
+          previouslyShortened = true;
+          limit = 0;
+        }
+      }
+      return this;
+    }
+
+    /**
+     * Appends any trailing "special characters" (e.g. brackets, quotation marks) in the given
+     * sequence to the output buffer, regardless of the limit.
+     *
+     * <p>For example, let's look at foo(['too long']). Without this method, the shortened result
+     * would be foo(['too...) instead of the prettier foo(['too...']).
+     *
+     * <p>If the input string was already shortened and contains "<x more arguments>", this part
+     * will also be appended.
+     */
+    private void appendTrailingSpecialChars(CharSequence csq, int limit) throws IOException {
+      int length = csq.length();
+      Matcher matcher = ARGS_PATTERN.matcher(csq);
+      // We assume that everything following the "x more arguments" part has to be copied, too.
+      int start = matcher.find() ? matcher.start() : length;
+      // Find the left-most non-arg char that has to be copied.
+      for (int i = start - 1; i > limit; --i) {
+        if (isSpecialChar(csq.charAt(i))) {
+          start = i;
+        } else {
+          break;
+        }
+      }
+      if (start < length) {
+        original.append(csq, start, csq.length());
+      }
+    }
+
+    /**
+     * Returns whether the given sequence denotes characters that are not part of the value of an
+     * argument.
+     *
+     * <p>Examples are brackets, braces and quotation marks.
+     */
+    private boolean hasOnlySpecialChars(CharSequence csq) {
+      for (int i = 0; i < csq.length(); ++i) {
+        if (!isSpecialChar(csq.charAt(i))) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    private boolean isSpecialChar(char c)    {
+      return SPECIAL_CHARS.contains(c);
+    }
+
+    @Override
+    public Appendable append(CharSequence csq, int start, int end) throws IOException {
+      return append(csq.subSequence(start, end));
+    }
+
+    @Override
+    public Appendable append(char c) throws IOException {
+      return append(String.valueOf(c));
+    }
+    
+    public boolean hasHitLimit()  {
+      return limit <= 0;
+    }
+
+    public void enforceLimit()  {
+      ignoreLimit = false;
+    }
+    
+    public void ignoreLimit() {
+      ignoreLimit = true;
+    }
+
+    @Override
+    public String toString() {
+      return original.toString();
+    }
+  }
 }