| // 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"); |
| } |
| } |
| } |