/*
* Copyright 2015 Google Inc.
*
* 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.template.soy.pysrc.restricted;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.internalutils.NodeContentKinds;
import com.google.template.soy.exprtree.Operator;
import com.google.template.soy.exprtree.Operator.Associativity;
import com.google.template.soy.exprtree.Operator.Operand;
import com.google.template.soy.exprtree.Operator.Spacer;
import com.google.template.soy.exprtree.Operator.SyntaxElement;
import com.google.template.soy.exprtree.Operator.Token;
import com.google.template.soy.internal.targetexpr.TargetExpr;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Common utilities for dealing with Python expressions.
*
* <p>Important: This class may only be used in implementing plugins (e.g. functions, directives).
*
*/
public final class PyExprUtils {
/** The variable name used to reference the current translator instance. */
public static final String TRANSLATOR_NAME = "translator_impl";
/** Expression constant for empty string. */
private static final PyExpr EMPTY_STRING = new PyStringExpr("''");
/**
* Map used to provide operator precedences in Python.
*
* @see <a href="https://docs.python.org/2/reference/expressions.html#operator-precedence">Python
* operator precedence.</a>
*/
private static final ImmutableMap<Operator, Integer> PYTHON_PRECEDENCES =
new ImmutableMap.Builder<Operator, Integer>()
.put(Operator.NEGATIVE, 8)
.put(Operator.TIMES, 7)
.put(Operator.DIVIDE_BY, 7)
.put(Operator.MOD, 7)
.put(Operator.PLUS, 6)
.put(Operator.MINUS, 6)
.put(Operator.LESS_THAN, 5)
.put(Operator.GREATER_THAN, 5)
.put(Operator.LESS_THAN_OR_EQUAL, 5)
.put(Operator.GREATER_THAN_OR_EQUAL, 5)
.put(Operator.EQUAL, 5)
.put(Operator.NOT_EQUAL, 5)
.put(Operator.NOT, 4)
.put(Operator.AND, 3)
.put(Operator.OR, 2)
.put(Operator.NULL_COALESCING, 1)
.put(Operator.CONDITIONAL, 1)
.build();
private PyExprUtils() {}
/**
* Builds one Python expression that computes the concatenation of the given Python expressions.
*
* <p>Python doesn't allow arbitrary concatentation between types, so to ensure type safety and
* consistent behavior, coerce all expressions to Strings before joining them. Python's array
* joining mechanism is used in place of traditional concatenation to improve performance.
*
* @param pyExprs The Python expressions to concatenate.
* @return One Python expression that computes the concatenation of the given Python expressions.
*/
public static PyExpr concatPyExprs(List<? extends PyExpr> pyExprs) {
if (pyExprs.isEmpty()) {
return EMPTY_STRING;
}
if (pyExprs.size() == 1) {
// If there's only one element, simply return the expression as a String.
return pyExprs.get(0).toPyString();
}
StringBuilder resultSb = new StringBuilder();
// Use Python's list joining mechanism to speed up concatenation.
resultSb.append("[");
boolean isFirst = true;
for (PyExpr pyExpr : pyExprs) {
if (isFirst) {
isFirst = false;
} else {
resultSb.append(',');
}
resultSb.append(pyExpr.toPyString().getText());
}
resultSb.append("]");
return new PyListExpr(resultSb.toString(), Integer.MAX_VALUE);
}
/** Generates a Python not null (None) check expression for the given {@link PyExpr}. */
public static PyExpr genPyNotNullCheck(PyExpr pyExpr) {
ImmutableList<PyExpr> exprs = ImmutableList.of(pyExpr, new PyExpr("None", Integer.MAX_VALUE));
// Note: is/is not is Python's identity comparison. It's used for None checks for performance.
String conditionalExpr = genExprWithNewToken(Operator.NOT_EQUAL, exprs, "is not");
return new PyExpr(conditionalExpr, PyExprUtils.pyPrecedenceForOperator(Operator.NOT_EQUAL));
}
/** Generates a Python null (None) check expression for the given {@link PyExpr}. */
public static PyExpr genPyNullCheck(PyExpr expr) {
ImmutableList<PyExpr> exprs = ImmutableList.of(expr, new PyExpr("None", Integer.MAX_VALUE));
// Note: is/is not is Python's identity comparison. It's used for None checks for performance.
String conditionalExpr = genExprWithNewToken(Operator.EQUAL, exprs, "is");
return new PyExpr(conditionalExpr, PyExprUtils.pyPrecedenceForOperator(Operator.EQUAL));
}
/**
* Wraps an expression with parenthesis if it's not above the minimum safe precedence.
*
* <p>NOTE: For the sake of brevity, this implementation loses typing information in the
* expressions.
*
* @param expr The expression to wrap.
* @param minSafePrecedence The minimum safe precedence (not inclusive).
* @return The PyExpr potentially wrapped in parenthesis.
*/
public static PyExpr maybeProtect(PyExpr expr, int minSafePrecedence) {
if (expr.getPrecedence() > minSafePrecedence) {
return expr;
} else {
return new PyExpr("(" + expr.getText() + ")", Integer.MAX_VALUE);
}
}
/**
* Wraps an expression with the proper SanitizedContent constructor.
*
* <p>NOTE: The pyExpr provided must be properly escaped for the given ContentKind. Please talk to
* ISE (ise@) for any questions or concerns.
*
* @param contentKind The kind of sanitized content.
* @param pyExpr The expression to wrap.
*/
public static PyExpr wrapAsSanitizedContent(ContentKind contentKind, PyExpr pyExpr) {
String sanitizer = NodeContentKinds.toPySanitizedContentOrdainer(contentKind);
String approval =
"sanitize.IActuallyUnderstandSoyTypeSafetyAndHaveSecurityApproval("
+ "'Internally created Sanitization.')";
return new PyExpr(
sanitizer + "(" + pyExpr.getText() + ", approval=" + approval + ")", Integer.MAX_VALUE);
}
/**
* Provide the Python operator precedence for a given operator.
*
* @param op The operator.
* @return THe python precedence as an integer.
*/
public static int pyPrecedenceForOperator(Operator op) {
return PYTHON_PRECEDENCES.get(op);
}
/**
* Convert a java Iterable object to valid PyExpr as array.
*
* @param iterable Iterable of Objects to be converted to PyExpr, it must be Number, PyExpr or
* String.
*/
public static PyExpr convertIterableToPyListExpr(Iterable<?> iterable) {
return convertIterableToPyExpr(iterable, true);
}
/**
* Convert a java Iterable object to valid PyExpr as tuple.
*
* @param iterable Iterable of Objects to be converted to PyExpr, it must be Number, PyExpr or
* String.
*/
public static PyExpr convertIterableToPyTupleExpr(Iterable<?> iterable) {
return convertIterableToPyExpr(iterable, false);
}
/**
* Convert a java Map to valid PyExpr as dict.
*
* @param dict A Map to be converted to PyExpr as a dictionary, both key and value should be
* PyExpr.
*/
public static PyExpr convertMapToOrderedDict(Map<PyExpr, PyExpr> dict) {
List<String> values = new ArrayList<>();
for (Map.Entry<PyExpr, PyExpr> entry : dict.entrySet()) {
values.add("(" + entry.getKey().getText() + ", " + entry.getValue().getText() + ")");
}
Joiner joiner = Joiner.on(", ");
return new PyExpr("collections.OrderedDict([" + joiner.join(values) + "])", Integer.MAX_VALUE);
}
/**
* Convert a java Map to valid PyExpr as dict.
*
* @param dict A Map to be converted to PyExpr as a dictionary, both key and value should be
* PyExpr.
*/
public static PyExpr convertMapToPyExpr(Map<PyExpr, PyExpr> dict) {
List<String> values = new ArrayList<>();
for (Map.Entry<PyExpr, PyExpr> entry : dict.entrySet()) {
values.add(entry.getKey().getText() + ": " + entry.getValue().getText());
}
Joiner joiner = Joiner.on(", ");
return new PyExpr("{" + joiner.join(values) + "}", Integer.MAX_VALUE);
}
private static PyExpr convertIterableToPyExpr(Iterable<?> iterable, boolean asArray) {
List<String> values = new ArrayList<>();
String leftDelimiter = "[";
String rightDelimiter = "]";
if (!asArray) {
leftDelimiter = "(";
rightDelimiter = ")";
}
for (Object elem : iterable) {
if (!(elem instanceof Number || elem instanceof String || elem instanceof PyExpr)) {
throw new UnsupportedOperationException("Only Number, String and PyExpr is allowed");
} else if (elem instanceof Number) {
values.add(String.valueOf(elem));
} else if (elem instanceof PyExpr) {
values.add(((PyExpr) elem).getText());
} else if (elem instanceof String) {
values.add("'" + elem + "'");
}
}
String contents = Joiner.on(", ").join(values);
// Tuples of one element require an extra comma otherwise the parens just set precedence.
if (values.size() == 1 && !asArray) {
contents += ",";
}
return new PyListExpr(leftDelimiter + contents + rightDelimiter, Integer.MAX_VALUE);
}
/**
* Generates an expression for the given operator and operands assuming that the expression for
* the operator uses the same syntax format as the Soy operator, with the exception that the of a
* different token. Associativity, spacing, and precedence are maintained from the original
* operator.
*
* <p>Examples:
*
* <pre>
* NOT, ["$a"], "!" -> "! $a"
* AND, ["$a", "$b"], "&&" -> "$a && $b"
* NOT, ["$a * $b"], "!"; -> "! ($a * $b)"
* </pre>
*
* @param op The operator.
* @param operandExprs The operands.
* @param newToken The language specific token equivalent to the operator's original token.
* @return The generated expression with a new token.
*/
public static String genExprWithNewToken(
Operator op, List<? extends TargetExpr> operandExprs, String newToken) {
int opPrec = op.getPrecedence();
boolean isLeftAssociative = op.getAssociativity() == Associativity.LEFT;
StringBuilder exprSb = new StringBuilder();
// Iterate through the operator's syntax elements.
List<SyntaxElement> syntax = op.getSyntax();
for (int i = 0, n = syntax.size(); i < n; i++) {
SyntaxElement syntaxEl = syntax.get(i);
if (syntaxEl instanceof Operand) {
// Retrieve the operand's subexpression.
int operandIndex = ((Operand) syntaxEl).getIndex();
TargetExpr operandExpr = operandExprs.get(operandIndex);
// If left (right) associative, first (last) operand doesn't need protection if it's an
// operator of equal precedence to this one.
boolean needsProtection;
if (i == (isLeftAssociative ? 0 : n - 1)) {
needsProtection = operandExpr.getPrecedence() < opPrec;
} else {
needsProtection = operandExpr.getPrecedence() <= opPrec;
}
// Append the operand's subexpression to the expression we're building (if necessary,
// protected using parentheses).
String subexpr =
needsProtection ? "(" + operandExpr.getText() + ")" : operandExpr.getText();
exprSb.append(subexpr);
} else if (syntaxEl instanceof Token) {
// If a newToken is supplied, then use it, else use the token defined by Soy syntax.
if (newToken != null) {
exprSb.append(newToken);
} else {
exprSb.append(((Token) syntaxEl).getValue());
}
} else if (syntaxEl instanceof Spacer) {
// Spacer is just one space.
exprSb.append(' ');
} else {
throw new AssertionError();
}
}
return exprSb.toString();
}
}