| // 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.base.Strings; | |
| import java.io.IOException; | |
| import java.util.Arrays; | |
| import java.util.IllegalFormatException; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.MissingFormatWidthException; | |
| import java.util.UnknownFormatConversionException; | |
| import javax.annotation.Nullable; | |
| /** A printer of Starlark values. */ | |
| // TODO(adonovan): merge BasePrinter into Printer and simplify. | |
| public abstract class Printer { | |
| /** Append a char to the printer's buffer */ | |
| public abstract Printer append(char c); | |
| /** Append a char sequence to the printer's buffer */ | |
| public abstract Printer append(CharSequence s); | |
| /** | |
| * Prints a list to the printer's buffer. All list items are rendered with {@code repr}. | |
| * | |
| * @param list the list | |
| * @param isTuple if true, uses parentheses, otherwise, uses square brackets. Also one-element | |
| * tuples are rendered with a comma after the element. | |
| * @return Printer | |
| */ | |
| public abstract Printer printList(Iterable<?> list, boolean isTuple); | |
| /** | |
| * Prints a list to the printer's buffer. All list items are rendered with {@code repr}. | |
| * | |
| * @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 | |
| * @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") | |
| * @return Printer | |
| */ | |
| public abstract Printer printList( | |
| Iterable<?> list, | |
| String before, | |
| String separator, | |
| String after, | |
| @Nullable String singletonTerminator); | |
| /** Renders an object with {@code repr} and append to the printer's buffer. */ | |
| public abstract Printer repr(Object o); | |
| /** Renders an object with {@code str} and append to the printer's buffer. */ | |
| public abstract Printer str(Object o); | |
| /** Renders an object in the style of {@code print} and append to the printer's buffer. */ | |
| public abstract Printer debugPrint(Object o); | |
| /** | |
| * Performs Python-style string formatting, as per {@code pattern % tuple}. Limitations: only | |
| * {@code %d %s %r %%} are supported. | |
| * | |
| * @param pattern a format string | |
| * @param arguments an array containing positional arguments | |
| * @return Printer | |
| */ | |
| public abstract Printer format(String pattern, Object... arguments); | |
| /** | |
| * Performs Python-style string formatting, as per {@code pattern % tuple}. Limitations: only | |
| * {@code %d %s %r %%} are supported. | |
| * | |
| * @param pattern a format string | |
| * @param arguments a list containing positional arguments | |
| * @return Printer | |
| */ | |
| public abstract Printer formatWithList(String pattern, List<?> arguments); | |
| /** | |
| * Creates an instance of {@link BasePrinter} that wraps an existing buffer. | |
| * | |
| * @param buffer an {@link Appendable} | |
| * @return new {@link BasePrinter} | |
| */ | |
| static BasePrinter getPrinter(Appendable buffer) { | |
| return new BasePrinter(buffer); | |
| } | |
| /** | |
| * Creates an instance of {@link BasePrinter} with an empty buffer. | |
| * | |
| * @return new {@link BasePrinter} | |
| */ | |
| public static BasePrinter getPrinter() { | |
| return new BasePrinter(new StringBuilder()); | |
| } | |
| /** | |
| * Creates an instance of {@link PrettyPrinter} with an empty buffer. | |
| * | |
| * @return new {@link PrettyPrinter} | |
| */ | |
| public static PrettyPrinter getPrettyPrinter() { | |
| return new PrettyPrinter(new StringBuilder()); | |
| } | |
| /** | |
| * Creates an instance of {@link BasePrinter} with an empty buffer and whose format strings allow | |
| * only %s and %%. | |
| */ | |
| public static BasePrinter getSimplifiedPrinter() { | |
| return new BasePrinter(new StringBuilder(), /*simplifiedFormatStrings=*/ true); | |
| } | |
| private Printer() {} | |
| /** | |
| * Append a char to a buffer. In case of {@link IOException} throw an {@link AssertionError} | |
| * instead | |
| * | |
| * @return buffer | |
| */ | |
| private static Appendable append(Appendable buffer, char c) { | |
| try { | |
| return buffer.append(c); | |
| } catch (IOException e) { | |
| throw new AssertionError(e); | |
| } | |
| } | |
| /** | |
| * Append a char sequence to a buffer. In case of {@link IOException} throw an {@link | |
| * AssertionError} instead | |
| * | |
| * @return buffer | |
| */ | |
| private static Appendable append(Appendable buffer, CharSequence s) { | |
| try { | |
| return buffer.append(s); | |
| } catch (IOException e) { | |
| throw new AssertionError(e); | |
| } | |
| } | |
| /** | |
| * Append a char sequence range to a buffer. In case of {@link IOException} throw an | |
| * {@link AssertionError} instead | |
| * @return buffer | |
| */ | |
| 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); | |
| } | |
| } | |
| /** Actual class that implements Printer API */ | |
| public static class BasePrinter extends Printer { | |
| // Methods of this class should not recurse through static methods of Printer | |
| protected final Appendable buffer; | |
| /** | |
| * If true, the only percent sequences allowed in format strings are %s substitutions and %% | |
| * escapes. | |
| */ | |
| protected final boolean simplifiedFormatStrings; | |
| /** | |
| * Creates a printer. | |
| * | |
| * @param buffer the {@link Appendable} that will be written to | |
| * @param simplifiedFormatStrings if true, format strings will allow only %s and %% | |
| */ | |
| protected BasePrinter(Appendable buffer, boolean simplifiedFormatStrings) { | |
| this.buffer = buffer; | |
| this.simplifiedFormatStrings = simplifiedFormatStrings; | |
| } | |
| /** | |
| * Creates a printer that writes to the given buffer and that does not use simplified format | |
| * strings. | |
| */ | |
| protected BasePrinter(Appendable buffer) { | |
| this(buffer, /*simplifiedFormatStrings=*/ false); | |
| } | |
| /** | |
| * Creates a printer that uses a fresh buffer and that does not use simplified format strings. | |
| */ | |
| protected BasePrinter() { | |
| this(new StringBuilder()); | |
| } | |
| @Override | |
| public String toString() { | |
| return buffer.toString(); | |
| } | |
| /** | |
| * Print an informal debug-only representation of object x. | |
| * | |
| * @param o the object | |
| * @return the buffer, in fluent style | |
| */ | |
| public BasePrinter debugPrint(Object o) { | |
| if (o instanceof StarlarkValue) { | |
| ((StarlarkValue) o).debugPrint(this); | |
| return this; | |
| } | |
| return this.str(o); | |
| } | |
| /** | |
| * Prints the informal representation of value {@code o}. 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 BasePrinter str(Object o) { | |
| if (o instanceof StarlarkValue) { | |
| ((StarlarkValue) o).str(this); | |
| return this; | |
| } | |
| if (o instanceof String) { | |
| return this.append((String) o); | |
| } | |
| return this.repr(o); | |
| } | |
| /** | |
| * Prints the quoted representation of Starlark value {@code o}. The quoted form is often a | |
| * Starlark expression that evaluates to {@code o}. | |
| * | |
| * <p>Implementations of StarlarkValue may define their own behavior of {@code repr}. | |
| * | |
| * <p>In addition to Starlark values, {@code repr} also prints instances of classes Map, List, | |
| * Map.Entry, Class, Node, or Location. To avoid nondeterminism, all other values are printed | |
| * opaquely. | |
| */ | |
| @Override | |
| public BasePrinter repr(Object o) { | |
| if (o == null) { | |
| // Java null is not a valid Starlark value, but sometimes printers are used on non-Skylark | |
| // values such as Locations or ASTs. | |
| this.append("null"); | |
| } else if (o instanceof StarlarkValue) { | |
| ((StarlarkValue) o).repr(this); | |
| } else if (o instanceof String) { | |
| writeString((String) o); | |
| } else if (o instanceof Integer || o instanceof Double) { | |
| this.append(o.toString()); | |
| } else if (Boolean.TRUE.equals(o)) { | |
| this.append("True"); | |
| } else if (Boolean.FALSE.equals(o)) { | |
| this.append("False"); | |
| // -- non-Starlark values -- | |
| } else if (o instanceof Map<?, ?>) { | |
| Map<?, ?> dict = (Map<?, ?>) o; | |
| this.printList(dict.entrySet(), "{", ", ", "}", null); | |
| } else if (o instanceof List<?>) { | |
| List<?> seq = (List<?>) o; | |
| this.printList(seq, false); | |
| } else if (o instanceof Map.Entry<?, ?>) { | |
| Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o; | |
| this.repr(entry.getKey()); | |
| this.append(": "); | |
| this.repr(entry.getValue()); | |
| } else if (o instanceof Class<?>) { | |
| this.append(EvalUtils.getDataTypeNameFromClass((Class<?>) o)); | |
| } else if (o instanceof Node || o instanceof Location) { | |
| // AST node objects and locations are printed in tracebacks and error messages, | |
| // it's safe to print their toString representations | |
| this.append(o.toString()); | |
| } else { | |
| // For now, we print all unknown values opaquely. | |
| // Historically this was a defense against accidental nondeterminism, | |
| // but Starlark code cannot access values of o that would reach here, | |
| // and native code is already trusted to be deterministic. | |
| // TODO(adonovan): replace this with a default behavior of this.append(o), | |
| // once we require that all @Skylark-annotated classes implement StarlarkValue. | |
| // (After all, Java code can call String.format, which also calls toString.) | |
| this.append("<unknown object " + o.getClass().getName() + ">"); | |
| } | |
| return this; | |
| } | |
| /** | |
| * Write a properly escaped Starlark representation of a string to a buffer. | |
| * | |
| * @param s the string a representation of which to repr. | |
| * @return this printer. | |
| */ | |
| protected BasePrinter writeString(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 BasePrinter backslashChar(char c) { | |
| return this.append('\\').append(c); | |
| } | |
| private BasePrinter 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 | |
| } // endswitch | |
| } | |
| /** | |
| * Print a list of object representations | |
| * | |
| * @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 | |
| * @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"). | |
| * @return this printer. | |
| */ | |
| @Override | |
| public BasePrinter printList( | |
| Iterable<?> list, | |
| String before, | |
| String separator, | |
| String after, | |
| @Nullable String singletonTerminator) { | |
| this.append(before); | |
| int len = appendListElements(list, separator); | |
| if (singletonTerminator != null && len == 1) { | |
| this.append(singletonTerminator); | |
| } | |
| return this.append(after); | |
| } | |
| /** | |
| * Appends the given elements to the specified {@link Appendable} and returns the number of | |
| * elements. | |
| */ | |
| private int appendListElements(Iterable<?> list, String separator) { | |
| boolean printSeparator = false; // don't print the separator before the first element | |
| int len = 0; | |
| for (Object o : list) { | |
| if (printSeparator) { | |
| this.append(separator); | |
| } | |
| this.repr(o); | |
| printSeparator = true; | |
| len++; | |
| } | |
| return len; | |
| } | |
| /** | |
| * Print a Starlark list or tuple of object representations | |
| * | |
| * @param list the contents of the list or tuple | |
| * @param isTuple if true the list will be formatted with parentheses and with a trailing comma | |
| * in case of one-element tuples. 'Soft' means that this limit may be exceeded because of | |
| * formatting. | |
| * @return this printer. | |
| */ | |
| @Override | |
| public BasePrinter printList(Iterable<?> list, boolean isTuple) { | |
| if (isTuple) { | |
| return this.printList(list, "(", ", ", ")", ","); | |
| } else { | |
| return this.printList(list, "[", ", ", "]", null); | |
| } | |
| } | |
| /** | |
| * Perform Python-style string formatting, similar to the {@code pattern % tuple} syntax. | |
| * | |
| * <p>The only supported placeholder patterns are | |
| * <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. | |
| * | |
| * <p>If this printer has {@code simplifiedFormatStrings} set, only {@code %s} and {@code %%} | |
| * are permitted. | |
| * | |
| * @param pattern a format string that may contain placeholders | |
| * @param arguments an array containing arguments to substitute into the placeholders in order | |
| * @return the formatted string | |
| * @throws IllegalFormatException if {@code pattern} is not a valid format string, or if | |
| * {@code arguments} mismatches the number or type of placeholders in {@code pattern} | |
| */ | |
| @Override | |
| public BasePrinter format(String pattern, Object... arguments) { | |
| return this.formatWithList(pattern, Arrays.asList(arguments)); | |
| } | |
| /** | |
| * Perform Python-style string formatting, similar to the {@code pattern % tuple} syntax. | |
| * | |
| * <p>Same as {@link #format(String, Object...)}, but with a list instead of variadic args. | |
| */ | |
| @Override | |
| public BasePrinter formatWithList(String pattern, List<?> arguments) { | |
| // TODO(bazel-team): support formatting arguments, and more complex Python patterns. | |
| // 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) { | |
| Printer.append(buffer, pattern, i, length); | |
| break; | |
| } | |
| if (p > i) { | |
| Printer.append(buffer, pattern, i, p); | |
| } | |
| if (p == length - 1) { | |
| throw new MissingFormatWidthException( | |
| "incomplete format pattern ends with %: " + this.repr(pattern)); | |
| } | |
| char directive = pattern.charAt(p + 1); | |
| i = p + 2; | |
| switch (directive) { | |
| case '%': | |
| this.append('%'); | |
| continue; | |
| case 'd': | |
| case 'r': | |
| case 's': | |
| if (simplifiedFormatStrings && (directive != 's')) { | |
| throw new UnknownFormatConversionException( | |
| "cannot use %" + directive + " substitution placeholder when " | |
| + "simplifiedFormatStrings is set"); | |
| } | |
| if (a >= argLength) { | |
| throw new MissingFormatWidthException( | |
| "not enough arguments for format pattern " | |
| + Starlark.repr(pattern) | |
| + ": " | |
| + Starlark.repr(Tuple.copyOf(arguments))); | |
| } | |
| Object argument = arguments.get(a++); | |
| switch (directive) { | |
| case 'd': | |
| if (argument instanceof Integer) { | |
| this.append(argument.toString()); | |
| continue; | |
| } else { | |
| throw new MissingFormatWidthException( | |
| "invalid argument " + Starlark.repr(argument) + " for format pattern %d"); | |
| } | |
| case 'r': | |
| this.repr(argument); | |
| continue; | |
| case 's': | |
| this.str(argument); | |
| continue; | |
| } | |
| // fall through | |
| default: | |
| throw new MissingFormatWidthException( | |
| // The call to Starlark.repr doesn't cause an infinite recursion because it's | |
| // only used to format a string properly | |
| String.format( | |
| "unsupported format character \"%s\" at index %s in %s", | |
| String.valueOf(directive), p + 1, Starlark.repr(pattern))); | |
| } | |
| } | |
| if (a < argLength) { | |
| throw new MissingFormatWidthException( | |
| "not all arguments converted during string formatting"); | |
| } | |
| return this; | |
| } | |
| @Override | |
| public BasePrinter append(char c) { | |
| Printer.append(buffer, c); | |
| return this; | |
| } | |
| @Override | |
| public BasePrinter append(CharSequence s) { | |
| Printer.append(buffer, s); | |
| return this; | |
| } | |
| BasePrinter append(CharSequence sequence, int start, int end) { | |
| return this.append(sequence.subSequence(start, end)); | |
| } | |
| } | |
| /** A printer that breaks lines between the entries of lists, with proper indenting. */ | |
| public static class PrettyPrinter extends BasePrinter { | |
| static final int BASE_INDENT = 4; | |
| private int indent; | |
| protected PrettyPrinter(Appendable buffer) { | |
| super(buffer); | |
| indent = 0; | |
| } | |
| @Override | |
| public BasePrinter printList( | |
| Iterable<?> list, | |
| String before, | |
| String untrimmedSeparator, | |
| String after, | |
| @Nullable String singletonTerminator) { | |
| // If the list is empty, do not split the presentation over | |
| // several lines. | |
| if (!list.iterator().hasNext()) { | |
| this.append(before + after); | |
| return this; | |
| } | |
| String separator = untrimmedSeparator.trim(); | |
| this.append(before + "\n"); | |
| indent += BASE_INDENT; | |
| boolean printSeparator = false; // don't print the separator before the first element | |
| int len = 0; | |
| for (Object o : list) { | |
| if (printSeparator) { | |
| this.append(separator + "\n"); | |
| } | |
| this.append(Strings.repeat(" ", indent)); | |
| this.repr(o); | |
| printSeparator = true; | |
| len++; | |
| } | |
| if (singletonTerminator != null && len == 1) { | |
| this.append(singletonTerminator); | |
| } | |
| this.append("\n"); | |
| indent -= BASE_INDENT; | |
| this.append(Strings.repeat(" ", indent) + after); | |
| return this; | |
| } | |
| } | |
| } |