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