blob: 1215f0ef3d5b80f9fcd0806f55a3c36038ba492c [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 net.starlark.java.eval;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Arrays;
import java.util.IllegalFormatException;
import java.util.List;
import java.util.Map;
import java.util.MissingFormatWidthException;
/**
* A printer of Starlark values.
*
* <p>Subclasses may override methods such as {@link #repr} and {@link #printList} to alter the
* formatting behavior.
*/
// TODO(adonovan): disallow printing of objects that are not Starlark values.
public class Printer {
private final StringBuilder buffer;
// Stack of values in the middle of being printed.
// Each renders as "..." if recursively encountered,
// indicating a cycle.
private Object[] stack;
private int depth;
/** Creates a printer that writes to the given buffer. */
public Printer(StringBuilder buffer) {
this.buffer = buffer;
}
/** Creates a printer that uses a fresh buffer. */
public Printer() {
this(new StringBuilder());
}
/** Appends a char to the printer's buffer */
@CanIgnoreReturnValue
public final Printer append(char c) {
buffer.append(c);
return this;
}
/** Appends a char sequence to the printer's buffer */
@CanIgnoreReturnValue
public final Printer append(CharSequence s) {
buffer.append(s);
return this;
}
/** Appends a char subsequence to the printer's buffer */
@CanIgnoreReturnValue
public final Printer append(CharSequence s, int start, int end) {
buffer.append(s, start, end);
return this;
}
/** Appends an integer to the printer's buffer */
@CanIgnoreReturnValue
public final Printer append(int i) {
buffer.append(i);
return this;
}
/** Appends a long integer to the printer's buffer */
@CanIgnoreReturnValue
public final Printer append(long l) {
buffer.append(l);
return this;
}
/**
* Appends a list to the printer's buffer. List elements are rendered with {@code repr}.
*
* <p>May be overridden by subclasses.
*
* @param list the list of objects to repr (each as with repr)
* @param before a string to print before the list items, e.g. an opening bracket
* @param separator a separator to print between items
* @param after a string to print after the list items, e.g. a closing bracket
*/
public Printer printList(Iterable<?> list, String before, String separator, String after) {
this.append(before);
String sep = "";
for (Object elem : list) {
this.append(sep);
sep = separator;
this.repr(elem);
}
return this.append(after);
}
@Override
public final String toString() {
return buffer.toString();
}
/**
* Appends the {@code StarlarkValue.debugPrint} representation of a value (as used by the Starlark
* {@code print} statement) to the printer's buffer.
*
* <p>Implementations of StarlarkValue may define their own behavior of {@code debugPrint}.
*/
public Printer debugPrint(Object o, StarlarkSemantics semantics) {
if (o instanceof StarlarkValue) {
((StarlarkValue) o).debugPrint(this, semantics);
return this;
}
return this.str(o, semantics);
}
/**
* Appends the {@code StarlarkValue.str} representation of a value to the printer's buffer. Unlike
* {@code repr(x)}, it does not quote strings at top level, though strings and other values
* appearing as elements of other structures are quoted as if by {@code repr}.
*
* <p>Implementations of StarlarkValue may define their own behavior of {@code str}.
*/
public Printer str(Object o, StarlarkSemantics semantics) {
if (o instanceof String) {
return this.append((String) o);
} else if (o instanceof StarlarkValue) {
((StarlarkValue) o).str(this, semantics);
return this;
} else {
return this.repr(o);
}
}
/**
* Appends the {@code StarlarkValue.repr} (quoted) representation of a value to the printer's
* buffer. The quoted form is often a Starlark expression that evaluates to the value.
*
* <p>Implementations of StarlarkValue may define their own behavior of {@code repr}.
*
* <p>Cyclic values are rendered as {@code ...} if they are recursively encountered by the same
* printer. (Implementations of {@link StarlarkValue#repr} should avoid calling {@code
* Starlark#repr}, as it creates another printer, which hide cycles from the cycle detector.)
*
* <p>In addition to Starlark values, {@code repr} also prints instances of classes Map, List,
* Map.Entry, or Class. All other values are formatted using their {@code toString} method.
* TODO(adonovan): disallow that.
*/
public Printer repr(Object o) {
// atomic values (leaves of the object graph)
if (o == null) {
// Java null is not a valid Starlark value, but sometimes printers are used on non-Starlark
// values such as Locations or Nodes.
return this.append("null");
} else if (o instanceof String) {
appendQuoted((String) o);
return this;
} else if (o instanceof StarlarkInt) {
((StarlarkInt) o).repr(this);
return this;
} else if (o instanceof Boolean) {
this.append(((boolean) o) ? "True" : "False");
return this;
} else if (o instanceof Integer) { // a non-Starlark value
this.buffer.append((int) o);
return this;
} else if (o instanceof Class) { // a non-Starlark value
this.append(Starlark.classType((Class<?>) o));
return this;
}
// compound values (may form cycles in the object graph)
if (!push(o)) {
return this.append("..."); // elided cycle
}
try {
if (o instanceof StarlarkValue) {
((StarlarkValue) o).repr(this);
// -- non-Starlark values --
} else if (o instanceof Map) {
Map<?, ?> dict = (Map<?, ?>) o;
this.printList(dict.entrySet(), "{", ", ", "}");
} else if (o instanceof List) {
this.printList((List) o, "[", ", ", "]");
} else if (o instanceof Map.Entry) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
this.repr(entry.getKey());
this.append(": ");
this.repr(entry.getValue());
} else {
// All other non-Starlark Java values (e.g. Node, Location).
// Starlark code cannot access values of o that would reach here,
// and native code is already trusted to be deterministic.
this.append(o.toString());
}
} finally {
pop();
}
return this;
}
private Printer appendQuoted(String s) {
this.append('"');
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
escapeCharacter(c);
}
return this.append('"');
}
private Printer backslashChar(char c) {
return this.append('\\').append(c);
}
private Printer escapeCharacter(char c) {
if (c == '"') {
return backslashChar(c);
}
switch (c) {
case '\\':
return backslashChar('\\');
case '\r':
return backslashChar('r');
case '\n':
return backslashChar('n');
case '\t':
return backslashChar('t');
default:
if (c < 32) {
// TODO(bazel-team): support \x escapes
return this.append(String.format("\\x%02x", (int) c));
}
return this.append(c); // no need to support UTF-8
}
}
// Reports whether x is already present on the visitation stack, pushing it if not.
private boolean push(Object x) {
// cyclic?
for (int i = 0; i < depth; i++) {
if (x == stack[i]) {
return false;
}
}
if (stack == null) {
this.stack = new Object[4];
} else if (depth == stack.length) {
this.stack = Arrays.copyOf(stack, 2 * stack.length);
}
this.stack[depth++] = x;
return true;
}
private void pop() {
this.stack[--depth] = null;
}
/**
* Appends a string, formatted as if by Starlark's {@code str % tuple} operator, to the printer's
* buffer.
*
* <p>Supported conversions:
*
* <ul>
* <li>{@code %s} (convert as if by {@code str()})
* <li>{@code %r} (convert as if by {@code repr()})
* <li>{@code %d} (convert an integer to its decimal representation)
* </ul>
*
* To encode a literal percent character, escape it as {@code %%}. It is an error to have a
* non-escaped {@code %} at the end of the string or followed by any character not listed above.
*
* @param format the format string
* @param arguments an array containing arguments to substitute into the format operators in order
* @throws IllegalFormatException if the format string is invalid or the arguments do not match it
*/
public static void format(
Printer printer, StarlarkSemantics semantics, String format, Object... arguments) {
formatWithList(printer, semantics, format, Arrays.asList(arguments));
}
/** Same as {@link #format}, but with a list instead of variadic args. */
@SuppressWarnings("FormatString") // see b/178189609
public static void formatWithList(
Printer printer, StarlarkSemantics semantics, String pattern, List<?> arguments) {
// N.B. MissingFormatWidthException is the only kind of IllegalFormatException
// whose constructor can take and display arbitrary error message, hence its use below.
// TODO(adonovan): this suggests we're using the wrong exception. Throw IAE?
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) {
printer.append(pattern, i, length);
break;
}
if (p > i) {
printer.append(pattern, i, p);
}
if (p == length - 1) {
throw new MissingFormatWidthException(
"incomplete format pattern ends with %: " + Starlark.repr(pattern));
}
char conv = pattern.charAt(p + 1);
i = p + 2;
// %%: literal %
if (conv == '%') {
printer.append('%');
continue;
}
// get argument
if (a >= argLength) {
throw new MissingFormatWidthException(
"not enough arguments for format pattern "
+ Starlark.repr(pattern)
+ ": "
+ Starlark.repr(Tuple.copyOf(arguments)));
}
Object arg = arguments.get(a++);
switch (conv) {
case 'd':
case 'o':
case 'x':
case 'X':
{
Number n;
if (arg instanceof StarlarkInt) {
n = ((StarlarkInt) arg).toNumber();
} else if (arg instanceof Integer) {
n = (Number) arg;
} else if (arg instanceof StarlarkFloat) {
double d = ((StarlarkFloat) arg).toDouble();
try {
n = StarlarkInt.ofFiniteDouble(d).toNumber();
} catch (IllegalArgumentException unused) {
throw new MissingFormatWidthException("got " + arg + ", want a finite number");
}
} else {
throw new MissingFormatWidthException(
String.format(
"got %s for '%%%c' format, want int or float", Starlark.type(arg), conv));
}
printer.append(
String.format(
conv == 'd' ? "%d" : conv == 'o' ? "%o" : conv == 'x' ? "%x" : "%X", n));
continue;
}
case 'e':
case 'f':
case 'g':
case 'E':
case 'F':
case 'G':
double v;
if (arg instanceof Integer) {
v = (double) (Integer) arg;
} else if (arg instanceof StarlarkInt) {
v = ((StarlarkInt) arg).toDouble();
} else if (arg instanceof StarlarkFloat) {
v = ((StarlarkFloat) arg).toDouble();
} else {
throw new MissingFormatWidthException(
String.format(
"got %s for '%%%c' format, want int or float", Starlark.type(arg), conv));
}
printer.append(StarlarkFloat.format(v, conv));
continue;
case 'r':
printer.repr(arg);
continue;
case 's':
printer.str(arg, semantics);
continue;
default:
// The call to Starlark.repr doesn't cause an infinite recursion
// because it's only used to format a string properly.
throw new MissingFormatWidthException(
String.format(
"unsupported format character \"%s\" at index %s in %s",
String.valueOf(conv), p + 1, Starlark.repr(pattern)));
}
}
if (a < argLength) {
throw new MissingFormatWidthException("not all arguments converted during string formatting");
}
}
}