All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.base.format.SimpleFormat Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2011 The Closure Compiler Authors.
 *
 * 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.javascript.jscomp.base.format;

import static java.lang.Math.max;
import static java.lang.Math.min;

import com.google.common.base.Ascii;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Arrays;
import java.util.Date;
import org.jspecify.nullness.Nullable;

/**
 * This is a hacked apart version of the Apache Harmony String.format class
 * with all parts outside of the GWT subset removed.  It will work for
 * simple format commands but does not handle I18N and doesn't handle complex
 * formatting options such as calendar entries and hashcodes.
 *
 * Formats arguments according to a format string (like {@code printf} in C).
 * 

*/ public final class SimpleFormat { final StringBuilder out; // Implementation details. private Object arg; private FormatToken formatToken; /** * Writes a formatted string to the output destination of the {@code Formatter}. * * @param format * a format string. * @param args * the arguments list used in the {@code format()} method. If there are * more arguments than those specified by the format string, then * the additional arguments are ignored. * @return this {@code Formatter}. * @throws IllegalFormatFlagsException * if the format string is illegal or incompatible with the * arguments, or if fewer arguments are sent than those required by * the format string, or any other illegal situation. */ public static String format(String format, Object... args) { SimpleFormat f = new SimpleFormat(); f.doFormat(format, args); return f.out.toString(); } private SimpleFormat() { out = new StringBuilder(); } private void doFormat(String format, Object... args) { FormatSpecifierParser fsp = new FormatSpecifierParser(format); int currentObjectIndex = 0; Object lastArgument = null; boolean hasLastArgumentSet = false; int length = format.length(); int i = 0; while (i < length) { // Find the maximal plain-text sequence... int plainTextStart = i; int nextPercent = format.indexOf('%', i); int plainTextEnd = (nextPercent == -1) ? length : nextPercent; // ...and output it. if (plainTextEnd > plainTextStart) { outputCharSequence(format, plainTextStart, plainTextEnd); } i = plainTextEnd; // Do we have a format specifier? if (i < length) { FormatToken token = fsp.parseFormatToken(i + 1); Object argument = null; if (token.requireArgument()) { int index = token.getArgIndex() == FormatToken.UNSET ? currentObjectIndex++ : token.getArgIndex(); argument = getArgument(args, index, fsp, lastArgument, hasLastArgumentSet); lastArgument = argument; hasLastArgumentSet = true; } CharSequence substitution = transform(token, argument); // The substitution is null if we called Formattable.formatTo. if (substitution != null) { outputCharSequence(substitution, 0, substitution.length()); } i = fsp.i; } } } // Fixes http://code.google.com/p/android/issues/detail?id=1767. private void outputCharSequence(CharSequence cs, int start, int end) { out.append(cs, start, end); } private static @Nullable Object getArgument( Object[] args, int index, FormatSpecifierParser fsp, Object lastArgument, boolean hasLastArgumentSet) { if (index == FormatToken.LAST_ARGUMENT_INDEX && !hasLastArgumentSet) { throw new MissingFormatArgumentException("<"); } if (args == null) { return null; } if (index >= args.length) { throw new MissingFormatArgumentException(fsp.getFormatSpecifierText()); } if (index == FormatToken.LAST_ARGUMENT_INDEX) { return lastArgument; } return args[index]; } /* * Complete details of a single format specifier parsed from a format string. */ private static class FormatToken { static final int LAST_ARGUMENT_INDEX = -2; static final int UNSET = -1; static final int DEFAULT_PRECISION = 6; private int argIndex = UNSET; // These have package access for performance. They used to be represented by an int bitmask // and accessed via methods, but Android's JIT doesn't yet do a good job of such code. // Direct field access, on the other hand, is fast. boolean flagAdd; boolean flagComma; boolean flagMinus; boolean flagParenthesis; boolean flagSharp; boolean flagSpace; boolean flagZero; private char conversionType = (char) UNSET; private char dateSuffix; private int precision = UNSET; private int width = UNSET; private StringBuilder strFlags; // Tests whether there were no flags, no width, and no precision specified. private boolean isDefault() { return !flagAdd && !flagComma && !flagMinus && !flagParenthesis && !flagSharp && !flagSpace && !flagZero && width == UNSET && precision == UNSET; } private boolean isPrecisionSet() { return precision != UNSET; } private int getArgIndex() { return argIndex; } private void setArgIndex(int index) { argIndex = index; } private int getWidth() { return width; } private void setWidth(int width) { this.width = width; } private int getPrecision() { return precision; } private void setPrecision(int precise) { this.precision = precise; } private String getStrFlags() { return (strFlags != null) ? strFlags.toString() : ""; } /* * Sets qualified char as one of the flags. If the char is qualified, * sets it as a flag and returns true. Or else returns false. */ private boolean setFlag(int ch) { boolean dupe = false; switch (ch) { case '+': dupe = flagAdd; flagAdd = true; break; case ',': dupe = flagComma; flagComma = true; break; case '-': dupe = flagMinus; flagMinus = true; break; case '(': dupe = flagParenthesis; flagParenthesis = true; break; case '#': dupe = flagSharp; flagSharp = true; break; case ' ': dupe = flagSpace; flagSpace = true; break; case '0': dupe = flagZero; flagZero = true; break; default: return false; } if (dupe) { throw new RuntimeException(String.valueOf(ch)); } if (strFlags == null) { strFlags = new StringBuilder(7); // There are seven possible flags. } strFlags.append((char) ch); return true; } private char getConversionType() { return conversionType; } private void setConversionType(char c) { conversionType = c; } private void setDateSuffix(char c) { dateSuffix = c; } private boolean requireArgument() { return conversionType != '%' && conversionType != 'n'; } private void checkFlags(Object arg) { // Work out which flags are allowed. boolean allowAdd = false; boolean allowComma = false; boolean allowMinus = true; boolean allowParenthesis = false; boolean allowSharp = false; boolean allowSpace = false; boolean allowZero = false; // Precision and width? boolean allowPrecision = true; boolean allowWidth = true; // Argument? boolean allowArgument = true; switch (conversionType) { // Character and date/time. case 'c': case 'C': case 't': case 'T': // Only '-' is allowed. allowPrecision = false; break; // Floating point. case 'g': case 'G': allowAdd = allowComma = allowParenthesis = allowSpace = allowZero = true; break; case 'f': allowAdd = allowComma = allowParenthesis = allowSharp = allowSpace = true; allowZero = true; break; case 'e': case 'E': allowAdd = allowParenthesis = allowSharp = allowSpace = allowZero = true; break; case 'a': case 'A': allowAdd = allowSharp = allowSpace = allowZero = true; break; // Integral. case 'd': allowAdd = allowComma = allowParenthesis = allowSpace = allowZero = true; allowPrecision = false; break; case 'o': case 'x': case 'X': allowSharp = allowZero = true; if (arg == null) { allowAdd = allowParenthesis = allowSpace = true; } allowPrecision = false; break; // Special. case 'n': // Nothing is allowed. allowMinus = false; allowArgument = allowPrecision = allowWidth = false; break; case '%': // The only flag allowed is '-', and no argument or precision is allowed. allowArgument = false; allowPrecision = false; break; // Strings, booleans and hash codes. case 's': case 'S': case 'b': case 'B': case 'h': case 'H': break; default: throw new RuntimeException("unknownFormatConversionException"); } // Check for disallowed flags. String mismatch = null; if (!allowAdd && flagAdd) { mismatch = "+"; } else if (!allowComma && flagComma) { mismatch = ","; } else if (!allowMinus && flagMinus) { mismatch = "-"; } else if (!allowParenthesis && flagParenthesis) { mismatch = "("; } else if (!allowSharp && flagSharp) { mismatch = "#"; } else if (!allowSpace && flagSpace) { mismatch = " "; } else if (!allowZero && flagZero) { mismatch = "0"; } if (mismatch != null) { if (conversionType == 'n') { // For no good reason, %n is a special case... throw new RuntimeException("IllegalFormatFlagsException:" + mismatch); } else { throw new RuntimeException( "FormatFlagsConversionMismatchException:" + mismatch + "," + conversionType); } } // Check for a missing width with flags that require a width. if ((flagMinus || flagZero) && width == UNSET) { throw new MissingFormatWidthException("-" + conversionType); } // Check that no-argument conversion types don't have an argument. // Note: the RI doesn't enforce this. if (!allowArgument && argIndex != UNSET) { throw new IllegalFormatFlagsException(getStrFlags()); } // Check that we don't have a precision or width where they're not allowed. if (!allowPrecision && precision != UNSET) { throw new IllegalFormatPrecisionException(precision); } if (!allowWidth && width != UNSET) { throw new IllegalFormatWidthException(width); } // Some combinations make no sense... if (flagAdd && flagSpace) { throw new IllegalFormatFlagsException("the '+' and ' ' flags are incompatible"); } if (flagMinus && flagZero) { throw new IllegalFormatFlagsException("the '-' and '0' flags are incompatible"); } } private UnknownFormatConversionException unknownFormatConversionException() { if (conversionType == 't' || conversionType == 'T') { throw new UnknownFormatConversionException(conversionType + String.valueOf(dateSuffix)); } throw new UnknownFormatConversionException(String.valueOf(conversionType)); } } /* * Gets the formatted string according to the format token and the * argument. */ private CharSequence transform(FormatToken token, Object argument) { this.formatToken = token; this.arg = argument; // There are only two format specifiers that matter: "%d" and "%s". // Nothing else is common in the wild. We fast-path these two to // avoid the heavyweight machinery needed to cope with flags, width, // and precision. if (token.isDefault()) { switch (token.getConversionType()) { case 's': if (arg == null) { return "null"; } // fall through case 'd': if (arg instanceof Integer || arg instanceof Long || arg instanceof Short || arg instanceof Byte) { return arg.toString(); } } } formatToken.checkFlags(arg); CharSequence result; switch (token.getConversionType()) { case 'B': case 'b': result = transformFromBoolean(); break; case 'H': case 'h': result = transformFromHashCode(); break; case 'S': case 's': result = transformFromString(); break; case 'C': case 'c': result = transformFromCharacter(); break; case 'd': case 'o': case 'x': case 'X': result = transformFromInteger(); break; case 'A': case 'a': case 'E': case 'e': case 'f': case 'G': case 'g': result = transformFromFloat(); break; case '%': result = transformFromPercent(); break; case 'n': result = transformFromLineSeparator(); break; case 't': case 'T': result = transformFromDateTime(); break; default: throw token.unknownFormatConversionException(); } if (Character.isUpperCase(token.getConversionType()) && result != null) { result = Ascii.toUpperCase(result.toString()); } return result; } private IllegalFormatConversionException badArgumentType() { throw new IllegalFormatConversionException(formatToken.getConversionType(), arg.getClass()); } private CharSequence transformFromBoolean() { String result; if (arg instanceof Boolean) { result = arg.toString(); } else if (arg == null) { result = "false"; } else { result = "true"; } return padding(result, 0); } private CharSequence transformFromHashCode() { String result; if (arg == null) { result = "null"; } else { throw new RuntimeException("Integer.toHexString is outside the GWT subset"); } return padding(result, 0); } private CharSequence transformFromString() { String result = String.valueOf(arg); return padding(result, 0); } private CharSequence transformFromCharacter() { if (arg == null) { return padding("null", 0); } if (arg instanceof Character) { return padding(String.valueOf(arg), 0); } else if (arg instanceof Byte || arg instanceof Short || arg instanceof Integer) { int codePoint = ((Number) arg).intValue(); if (!Character.isValidCodePoint(codePoint)) { throw new IllegalFormatCodePointException(codePoint); } CharSequence result = (codePoint < Character.MIN_SUPPLEMENTARY_CODE_POINT) ? String.valueOf((char) codePoint) : String.valueOf(Character.toChars(codePoint)); return padding(result, 0); } else { throw badArgumentType(); } } private CharSequence transformFromPercent() { return padding("%", 0); } private static String transformFromLineSeparator() { return "\n"; } private CharSequence padding(CharSequence source, int startIndex) { int start = startIndex; int width = formatToken.getWidth(); int precision = formatToken.getPrecision(); int length = source.length(); if (precision >= 0) { length = min(length, precision); if (source instanceof StringBuilder) { ((StringBuilder) source).setLength(length); } else { source = source.subSequence(0, length); } } if (width > 0) { width = max(source.length(), width); } if (length >= width) { return source; } char paddingChar = '\u0020'; // space as padding char. if (formatToken.flagZero) { if (formatToken.getConversionType() == 'd') { paddingChar = '0'; } else { paddingChar = '0'; // No localized digits for bases other than decimal. } } else { // if padding char is space, always pad from the start. start = 0; } char[] paddingChars = new char[width - length]; Arrays.fill(paddingChars, paddingChar); boolean paddingRight = formatToken.flagMinus; StringBuilder result = toStringBuilder(source); if (paddingRight) { result.append(paddingChars); } else { result.insert(start, paddingChars); } return result; } private static StringBuilder toStringBuilder(CharSequence cs) { return cs instanceof StringBuilder ? (StringBuilder) cs : new StringBuilder(cs); } private StringBuilder wrapParentheses(StringBuilder result) { result.setCharAt(0, '('); // Replace the '-'. if (formatToken.flagZero) { formatToken.setWidth(formatToken.getWidth() - 1); result = (StringBuilder) padding(result, 1); result.append(')'); } else { result.append(')'); result = (StringBuilder) padding(result, 0); } return result; } private CharSequence transformFromInteger() { int startIndex = 0; StringBuilder result = new StringBuilder(); char currentConversionType = formatToken.getConversionType(); long value; if (arg instanceof Long) { value = ((Long) arg).longValue(); } else if (arg instanceof Integer) { value = ((Integer) arg).longValue(); } else if (arg instanceof Short) { value = ((Short) arg).longValue(); } else if (arg instanceof Byte) { value = ((Byte) arg).longValue(); } else { throw badArgumentType(); } if (formatToken.flagSharp) { if (currentConversionType == 'o') { result.append("0"); startIndex += 1; } else { result.append("0x"); startIndex += 2; } } if ('d' == currentConversionType) { if (formatToken.flagComma) { // Too bad... we don't care about the comma... result.append(arg); } else { result.append(value); } if (value < 0) { if (formatToken.flagParenthesis) { return wrapParentheses(result); } else if (formatToken.flagZero) { startIndex++; } } else { if (formatToken.flagAdd) { result.insert(0, '+'); startIndex += 1; } else if (formatToken.flagSpace) { result.insert(0, ' '); startIndex += 1; } } } else { // Undo sign-extension, since we'll be using Long.to(Octal|Hex)String. if (arg instanceof Byte) { value &= 0xffL; } else if (arg instanceof Short) { value &= 0xffffL; } else if (arg instanceof Integer) { value &= 0xffffffffL; } if ('o' == currentConversionType) { result.append(Long.toOctalString(value)); } else { result.append(Long.toHexString(value)); } } return padding(result, startIndex); } private @Nullable CharSequence transformFromSpecialNumber() { if (!(arg instanceof Number)) { return null; } Number number = (Number) arg; double d = number.doubleValue(); String source = null; if (Double.isNaN(d)) { source = "NaN"; } else if (d == Double.POSITIVE_INFINITY) { if (formatToken.flagAdd) { source = "+Infinity"; } else if (formatToken.flagSpace) { source = " Infinity"; } else { source = "Infinity"; } } else if (d == Double.NEGATIVE_INFINITY) { if (formatToken.flagParenthesis) { source = "(Infinity)"; } else { source = "-Infinity"; } } else { return null; } formatToken.setPrecision(FormatToken.UNSET); formatToken.flagZero = false; return padding(source, 0); } private CharSequence transformFromNull() { formatToken.flagZero = false; return padding("null", 0); } private CharSequence transformFromDateTime() { if (arg == null) { return transformFromNull(); } // Ignore calendar... // this is a total hack... as we don't care... Date date = null; { if (arg instanceof Long) { date = new Date(((Long) arg).longValue()); } else if (arg instanceof Date) { date = (Date) arg; } else { throw badArgumentType(); } } StringBuilder result = new StringBuilder(); result.append(date); return padding(result, 0); } private CharSequence transformFromFloat() { if (arg == null) { return transformFromNull(); } if (!(arg instanceof Float || arg instanceof Double)) { throw badArgumentType(); } CharSequence specialNumberResult = transformFromSpecialNumber(); if (specialNumberResult != null) { return specialNumberResult; } char conversionType = formatToken.getConversionType(); if (conversionType != 'a' && conversionType != 'A' && !formatToken.isPrecisionSet()) { formatToken.setPrecision(FormatToken.DEFAULT_PRECISION); } StringBuilder result = new StringBuilder(); switch (conversionType) { case 'a': case 'A': transformA(); break; case 'e': case 'E': transformE(result); break; case 'f': transformF(result); break; case 'g': case 'G': transformG(result); break; default: throw formatToken.unknownFormatConversionException(); } formatToken.setPrecision(FormatToken.UNSET); int startIndex = 0; if ('-' == result.charAt(0)) { if (formatToken.flagParenthesis) { return wrapParentheses(result); } } else { if (formatToken.flagSpace) { result.insert(0, ' '); startIndex++; } if (formatToken.flagAdd) { result.insert(0, '+'); startIndex++; } } char firstChar = result.charAt(0); if (formatToken.flagZero && (firstChar == '+' || firstChar == '-')) { startIndex = 1; } if (conversionType == 'a' || conversionType == 'A') { startIndex += 2; } return padding(result, startIndex); } private void transformE(StringBuilder result) { StringBuilder pattern = new StringBuilder(); pattern.append('0'); if (formatToken.getPrecision() > 0) { pattern.append('.'); char[] zeros = new char[formatToken.getPrecision()]; Arrays.fill(zeros, '0'); pattern.append(zeros); } pattern.append("E+00"); String formattedString = arg.toString(); result.append(formattedString.replace('E', 'e')); // if the flag is sharp and decimal separator is always given out. if (formatToken.flagSharp && formatToken.getPrecision() == 0) { int indexOfE = result.indexOf("e"); result.insert(indexOfE, "."); } } private void transformG(StringBuilder result) { int precision = formatToken.getPrecision(); precision = (0 == precision ? 1 : precision); formatToken.setPrecision(precision); double d = ((Number) arg).doubleValue(); if (d == 0.0) { precision--; formatToken.setPrecision(precision); transformF(result); return; } d = Math.abs(d); if (Double.isInfinite(d)) { precision = formatToken.getPrecision(); precision--; formatToken.setPrecision(precision); transformE(result); return; } // Skipped a bunch of scientific notation stuff. transformF(result); } private void transformF(StringBuilder result) { int precision = formatToken.getPrecision(); String s = arg.toString(); if (s.contains(".")) { int i = s.indexOf('.'); if (i + precision < s.length()) { s = s.substring(0, i + precision + 1); } } result.append(s); } private void transformA() { if (arg instanceof Float) { throw new RuntimeException("Float.toHexString is outside the GWT subset"); } else if (arg instanceof Double) { throw new RuntimeException("Double.toHexString is outside the GWT subset"); } else { throw badArgumentType(); } } private static class FormatSpecifierParser { private final String format; private final int length; private int startIndex; private int i; /** * Constructs a new parser for the given format string. */ FormatSpecifierParser(String format) { this.format = format; this.length = format.length(); } /** * Returns a FormatToken representing the format specifier starting at 'offset'. * @param offset the first character after the '%' */ FormatToken parseFormatToken(int offset) { this.startIndex = offset; this.i = offset; return parseArgumentIndexAndFlags(new FormatToken()); } /** * Returns a string corresponding to the last format specifier that was parsed. * Used to construct error messages. */ String getFormatSpecifierText() { return format.substring(startIndex, i); } private int peek() { return (i < length) ? format.charAt(i) : -1; } @CanIgnoreReturnValue private char advance() { if (i >= length) { throw unknownFormatConversionException(); } return format.charAt(i++); } private UnknownFormatConversionException unknownFormatConversionException() { throw new UnknownFormatConversionException(getFormatSpecifierText()); } private static boolean isDigit(int ch) { return ch >= '0' && ch <= '9'; } private FormatToken parseArgumentIndexAndFlags(FormatToken token) { // Parse the argument index, if there is one. int position = i; int ch = peek(); if (isDigit(ch)) { int number = nextInt(); if (peek() == '$') { // The number was an argument index. advance(); // Swallow the '$'. if (number == FormatToken.UNSET) { throw new MissingFormatArgumentException(getFormatSpecifierText()); } // k$ stands for the argument whose index is k-1 except that // 0$ and 1$ both stand for the first element. token.setArgIndex(max(0, number - 1)); } else { if (ch == '0') { // The digit zero is a format flag, so reparse it as such. i = position; } else { // The number was a width. This means there are no flags to parse. return parseWidth(token, number); } } } else if (ch == '<') { token.setArgIndex(FormatToken.LAST_ARGUMENT_INDEX); advance(); } // Parse the flags. while (token.setFlag(peek())) { advance(); } // What comes next? ch = peek(); if (isDigit(ch)) { return parseWidth(token, nextInt()); } else if (ch == '.') { return parsePrecision(token); } else { return parseConversionType(token); } } // We pass the width in because in some cases we've already parsed it. // (Because of the ambiguity between argument indexes and widths.) private FormatToken parseWidth(FormatToken token, int width) { token.setWidth(width); int ch = peek(); if (ch == '.') { return parsePrecision(token); } else { return parseConversionType(token); } } private FormatToken parsePrecision(FormatToken token) { advance(); // Swallow the '.'. int ch = peek(); if (isDigit(ch)) { token.setPrecision(nextInt()); return parseConversionType(token); } else { // The precision is required but not given by the format string. throw unknownFormatConversionException(); } } private FormatToken parseConversionType(FormatToken token) { char conversionType = advance(); // A conversion type is mandatory. token.setConversionType(conversionType); if (conversionType == 't' || conversionType == 'T') { char dateSuffix = advance(); // A date suffix is mandatory for 't' or 'T'. token.setDateSuffix(dateSuffix); } return token; } // Parses an integer (of arbitrary length, but typically just one digit). private int nextInt() { long value = 0; while (i < length && isDigit(format.charAt(i))) { value = 10 * value + (format.charAt(i++) - '0'); if (value > Integer.MAX_VALUE) { return failNextInt(); } } return (int) value; } // Swallow remaining digits to resync our attempted parse, but return failure. private int failNextInt() { while (isDigit(peek())) { advance(); } return FormatToken.UNSET; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy