blob: 88abe13756de72a85176b3c22fc6d78b91c93a26 [file] [log] [blame]
// Copyright 2015 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
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.skylarkinterface.SkylarkPrintableValue;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.Formattable;
import java.util.Formatter;
import java.util.IllegalFormatException;
import java.util.List;
import java.util.Map;
import java.util.MissingFormatWidthException;
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
*/
public final class Printer {
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() {
}
/**
* Get an informal representation of object x.
* Currently only differs from repr in the behavior for strings and labels at top-level,
* that are returned as is rather than quoted.
* @param quotationMark The quotation mark to be used (' or ")
* @return the representation.
*/
public static String str(Object x, char quotationMark) {
return print(new StringBuilder(), x, quotationMark).toString();
}
public static String str(Object x) {
return str(x, SKYLARK_QUOTATION_MARK);
}
/**
* Get an official representation of object x.
* For regular data structures, the value should be parsable back into an equal data structure.
* @param quotationMark The quotation mark to be used (' or ")
* @return the representation.
*/
public static String repr(Object x, char quotationMark) {
return write(new StringBuilder(), x, quotationMark).toString();
}
public static String repr(Object x) {
return repr(x, SKYLARK_QUOTATION_MARK);
}
// In absence of a Python naming tradition, the write() vs print() function names
// follow the Lisp tradition: print() displays the informal representation (as in Python str)
// whereas write() displays a readable representation (as in Python repr).
/**
* Print an informal representation of object x.
* Currently only differs from repr in the behavior for strings and labels at top-level,
* that are returned as is rather than quoted.
* @param buffer the Appendable to which to print the representation
* @param o the object
* @param quotationMark The quotation mark to be used (' or ")
* @return the buffer, in fluent style
*/
private static Appendable print(Appendable buffer, Object o, char quotationMark) {
if (o instanceof SkylarkPrintableValue) {
((SkylarkPrintableValue) o).print(buffer, quotationMark);
return buffer;
}
if (o instanceof String) {
return append(buffer, (String) o);
}
return write(buffer, o, quotationMark);
}
private static Appendable print(Appendable buffer, Object o) {
return print(buffer, o, SKYLARK_QUOTATION_MARK);
}
/**
* Print an official representation of object x.
* For regular data structures, the value should be parsable back into an equal data structure.
* @param buffer the Appendable to write to.
* @param o the string a representation of which to write.
* @param quotationMark The quotation mark to be used (' or ")
* @return the Appendable, in fluent style.
*/
public static Appendable write(Appendable buffer, Object o, char quotationMark) {
if (o == null) {
throw new NullPointerException(); // Java null is not a build language value.
} else if (o instanceof SkylarkValue) {
((SkylarkValue) o).write(buffer, quotationMark);
} else if (o instanceof String) {
writeString(buffer, (String) o, quotationMark);
} else if (o instanceof Integer || o instanceof Double) {
append(buffer, o.toString());
} else if (o == Boolean.TRUE) {
append(buffer, "True");
} else if (o == Boolean.FALSE) {
append(buffer, "False");
} else if (o instanceof List<?>) {
List<?> seq = (List<?>) o;
printList(buffer, seq, false, quotationMark);
} else if (o instanceof Map<?, ?>) {
Map<?, ?> dict = (Map<?, ?>) o;
printList(buffer, getSortedEntrySet(dict), "{", ", ", "}", null, quotationMark);
} else if (o instanceof Map.Entry<?, ?>) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
write(buffer, entry.getKey(), quotationMark);
append(buffer, ": ");
write(buffer, entry.getValue(), quotationMark);
} else if (o instanceof PathFragment) {
append(buffer, ((PathFragment) o).getPathString());
} else if (o instanceof Class<?>) {
append(buffer, EvalUtils.getDataTypeNameFromClass((Class<?>) o));
} else {
append(buffer, o.toString());
}
return buffer;
}
/**
* Returns the sorted entry set of the given map
*/
private static <K, V> Set<Map.Entry<K, V>> getSortedEntrySet(Map<K, V> dict) {
if (!(dict instanceof SortedMap<?, ?>)) {
Map<K, V> tmp = new TreeMap<>(EvalUtils.SKYLARK_COMPARATOR);
tmp.putAll(dict);
dict = tmp;
}
return dict.entrySet();
}
public static Appendable write(Appendable buffer, Object o) {
return write(buffer, o, SKYLARK_QUOTATION_MARK);
}
// Throughout this file, we transform IOException into AssertionError.
// During normal operations, we only use in-memory Appendable-s that
// cannot cause an IOException.
public static Appendable append(Appendable buffer, char c) {
try {
return buffer.append(c);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static Appendable append(Appendable buffer, CharSequence s) {
try {
return buffer.append(s);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static Appendable append(Appendable buffer, CharSequence s, int start, int end) {
try {
return buffer.append(s, start, end);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static Appendable backslashChar(Appendable buffer, char c) {
return append(append(buffer, '\\'), c);
}
private static Appendable escapeCharacter(Appendable buffer, char c, char quote) {
if (c == quote) {
return backslashChar(buffer, c);
}
switch (c) {
case '\\':
return backslashChar(buffer, '\\');
case '\r':
return backslashChar(buffer, 'r');
case '\n':
return backslashChar(buffer, 'n');
case '\t':
return backslashChar(buffer, 't');
default:
if (c < 32) {
return append(buffer, String.format("\\x%02x", (int) c));
}
return append(buffer, c); // no need to support UTF-8
} // endswitch
}
/**
* Write a properly escaped Skylark representation of a string to a buffer.
*
* @param buffer the Appendable to write to.
* @param s the string a representation of which to write.
* @param quote the quote character to use, '"' or '\''.
* @return the Appendable, in fluent style.
*/
private static Appendable writeString(Appendable buffer, String s, char quote) {
append(buffer, quote);
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
escapeCharacter(buffer, c, quote);
}
return append(buffer, quote);
}
/**
* Print a list of object representations
* @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 ")
* @return the Appendable, in fluent style.
*/
public static Appendable printList(
Appendable buffer,
Iterable<?> list,
String before,
String separator,
String after,
String singletonTerminator,
char quotationMark) {
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);
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);
}
return append(buffer, after);
}
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);
}
/**
* Print a Skylark list or tuple of object representations
* @param buffer an appendable buffer onto which to write the list.
* @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) {
return printList(buffer, list, isTuple, quotationMark, -1, -1);
}
/**
* Print a list of object representations
* @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 ")
* @return a String, the representation.
*/
public static String listString(Iterable<?> list, String before, String separator, String after,
String singletonTerminator, char quotationMark) {
return printList(new StringBuilder(), list, before, separator, after, singletonTerminator,
quotationMark).toString();
}
public static String listString(
Iterable<?> list, String before, String separator, String after, String singletonTerminator) {
return listString(list, before, separator, after, singletonTerminator, SKYLARK_QUOTATION_MARK);
}
/**
* Perform Python-style string formatting, lazily.
*
* @param pattern a format string.
* @param arguments positional arguments.
* @return the formatted string.
*/
public static Formattable formattable(final String pattern, Object... arguments)
throws IllegalFormatException {
final ImmutableList<Object> args = ImmutableList.copyOf(arguments);
return new Formattable() {
@Override
public String toString() {
return formatToString(pattern, args);
}
@Override
public void formatTo(Formatter formatter, int flags, int width, int precision) {
Printer.formatTo(formatter.out(), pattern, args);
}
};
}
/**
* Perform Python-style string formatting.
*
* @param pattern a format string.
* @param arguments a tuple containing positional arguments.
* @return the formatted string.
*/
public static String format(String pattern, Object... arguments)
throws IllegalFormatException {
return formatToString(pattern, ImmutableList.copyOf(arguments));
}
/**
* Perform Python-style string formatting.
*
* @param pattern a format string.
* @param arguments a tuple containing positional arguments.
* @return the formatted string.
*/
public static String formatToString(String pattern, List<?> arguments)
throws IllegalFormatException {
return formatTo(new StringBuilder(), pattern, arguments).toString();
}
/**
* Perform Python-style string formatting, as per pattern % tuple
* Limitations: only %d %s %r %% are supported.
*
* @param buffer an Appendable to output to.
* @param pattern a format string.
* @param arguments a list containing positional arguments.
* @return the buffer, in fluent style.
*/
// TODO(bazel-team): support formatting arguments, and more complex Python patterns.
public static Appendable formatTo(Appendable buffer, String pattern, List<?> arguments)
throws IllegalFormatException {
// N.B. MissingFormatWidthException is the only kind of IllegalFormatException
// whose constructor can take and display arbitrary error message, hence its use below.
int length = pattern.length();
int argLength = arguments.size();
int i = 0; // index of next character in pattern
int a = 0; // index of next argument in arguments
while (i < length) {
int p = pattern.indexOf('%', i);
if (p == -1) {
append(buffer, pattern, i, length);
break;
}
if (p > i) {
append(buffer, pattern, i, p);
}
if (p == length - 1) {
throw new MissingFormatWidthException(
"incomplete format pattern ends with %: " + repr(pattern));
}
char directive = pattern.charAt(p + 1);
i = p + 2;
switch (directive) {
case '%':
append(buffer, '%');
continue;
case 'd':
case 'r':
case 's':
if (a >= argLength) {
throw new MissingFormatWidthException("not enough arguments for format pattern "
+ repr(pattern) + ": "
+ repr(Tuple.copyOf(arguments)));
}
Object argument = arguments.get(a++);
switch (directive) {
case 'd':
if (argument instanceof Integer) {
append(buffer, argument.toString());
continue;
} else {
throw new MissingFormatWidthException(
"invalid argument " + repr(argument) + " for format pattern %d");
}
case 'r':
write(buffer, argument);
continue;
case 's':
print(buffer, argument);
continue;
}
default:
throw new MissingFormatWidthException(
"unsupported format character " + repr(String.valueOf(directive))
+ " at index " + (p + 1) + " in " + repr(pattern));
}
}
if (a < argLength) {
throw new MissingFormatWidthException(
"not all arguments converted during string formatting");
}
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();
}
}
}