/*
* Copyright 2016 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.jssrc.dsl;
import static com.google.template.soy.jssrc.dsl.OutputContext.STATEMENT;
import static com.google.template.soy.jssrc.dsl.OutputContext.TRAILING_EXPRESSION;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.ForOverride;
import com.google.errorprone.annotations.Immutable;
import com.google.template.soy.base.internal.BaseUtils;
import com.google.template.soy.base.internal.UniqueNameGenerator;
import com.google.template.soy.exprtree.IntegerNode;
import com.google.template.soy.exprtree.Operator;
import com.google.template.soy.exprtree.Operator.Associativity;
import com.google.template.soy.jssrc.restricted.JsExpr;
import java.util.Arrays;
import java.util.List;
/**
* DSL for constructing sequences of JavaScript code. Unlike {@link JsExpr}, it can handle code that
* cannot be represented as single expressions.
*
* <p>Sample usage: <code>
* CodeChunk.WithValue fraction = cg.declare(
* number(3)
* .divideBy(number(4)));
* cg
* .newChunk(fraction)
* .if_(
* fraction.doubleEqualsNull(),
* id("someFunction").call())
* .endif()
* .assign(fraction.times(number(5)))
* .build()
* .getCode();
* </code> produces <code>
* var $$tmp0 = 3 / 4;
* if ($$tmp0 == null) {
* someFunction();
* }
* $$tmp0 = $$tmp0 * 5;
* </code> TODO(user): do all JS code generation with this DSL (that is, remove {@link
* com.google.template.soy.jssrc.internal.JsCodeBuilder}).
*/
@Immutable
public abstract class CodeChunk {
/** Returns a chunk representing the concatenation of this chunk with the other. */
public final CodeChunk concat(CodeChunk other) {
return StatementList.of(ImmutableList.of(this, other));
}
/** Starts a conditional statement beginning with the given predicate and consequent chunks. */
public static ConditionalBuilder ifStatement(
CodeChunk.WithValue predicate, CodeChunk consequent) {
return new ConditionalBuilder(predicate, consequent);
}
/** Starts a conditional expression beginning with the given predicate and consequent chunks. */
public static ConditionalExpressionBuilder ifExpression(
CodeChunk.WithValue predicate, CodeChunk.WithValue consequent) {
return new ConditionalExpressionBuilder(predicate, consequent);
}
/**
* Creates a new code chunk from the given expression. The expression's precedence is preserved.
*/
public static WithValue fromExpr(JsExpr expr, Iterable<GoogRequire> requires) {
return Leaf.create(expr, requires);
}
/**
* Creates a code chunk representing a JavaScript identifier.
*
* @throws IllegalArgumentException if {@code id} is not a valid JavaScript identifier.
*/
public static WithValue id(String id) {
CodeChunkUtils.checkId(id);
return Leaf.create(id);
}
/**
* Creates a code chunk representing a JavaScript identifier.
*
* @throws IllegalArgumentException if {@code id} is not a valid JavaScript identifier.
*/
static WithValue id(String id, Iterable<GoogRequire> requires) {
CodeChunkUtils.checkId(id);
return Leaf.create(id, requires);
}
/**
* Creates a code chunk representing a JavaScript "dotted identifier" which needs no {@code
* goog.require} statements to be added.
*
* <p>"Dotted identifiers" are really just sequences of dot-access operations off some base
* identifier, so this method is just a convenience for <code>id(...).dotAccess(...)...</code>.
* It's provided because working with raw dot-separated strings is common.
*
* <p>Most dotted identifiers should be accessed via the {@link GoogRequire} api.
*/
public static WithValue dottedIdNoRequire(String dotSeparatedIdentifiers) {
return dottedIdWithRequires(dotSeparatedIdentifiers, ImmutableSet.<GoogRequire>of());
}
static WithValue dottedIdWithRequires(
String dotSeparatedIdentifiers, Iterable<GoogRequire> requires) {
List<String> ids = Splitter.on('.').splitToList(dotSeparatedIdentifiers);
Preconditions.checkState(
!ids.isEmpty(),
"not a dot-separated sequence of JavaScript identifiers: %s",
dotSeparatedIdentifiers);
// Associate the requires with the base id for convenience. It is arguable that they should
// be instead associated with the last dot. Or perhaps with the 'whole' expression somehow.
// This is a minor philosophical concern but it should be fine in practice because nothing would
// ever split apart a code chunk into sub-chunks. So the requires could really go anywhere.
CodeChunk.WithValue tip = id(ids.get(0), requires);
for (int i = 1; i < ids.size(); ++i) {
tip = tip.dotAccess(ids.get(i));
}
return tip;
}
/**
* Creates a code chunk representing a JavaScript string literal.
*
* @param contents The contents of the string literal. The contents will be escaped appropriately
* and embedded inside single quotes.
*/
public static WithValue stringLiteral(String contents) {
// Escape non-ASCII characters since browsers are inconsistent in how they interpret utf-8 in
// JS source files.
String escaped = BaseUtils.escapeToSoyString(contents, true /* shouldEscapeToAscii */);
// </script in a JavaScript string will end the current script tag in most browsers. Escape the
// forward slash in the string to get around this issue.
escaped = escaped.replace("</script", "<\\/script");
return Leaf.create(escaped);
}
/** Creates a code chunk representing a JavaScript number literal. */
public static WithValue number(long value) {
Preconditions.checkArgument(
IntegerNode.isInRange(value), "Number is outside JS safe integer range: %s", value);
return Leaf.create(Long.toString(value));
}
/** Creates a code chunk representing a JavaScript number literal. */
public static WithValue number(double value) {
return Leaf.create(Double.toString(value));
}
/** Creates a code chunk that assigns value to a preexisting variable with the given name. */
public static CodeChunk assign(String varName, CodeChunk.WithValue rhs) {
return Assignment.create(varName, rhs);
}
/** Creates a code chunk that declares a new variable and assigns a value to it. */
public static Declaration declare(String varName, CodeChunk.WithValue rhs) {
return Declaration.create(varName, rhs);
}
public static Declaration declare(
String varName, CodeChunk.WithValue value, String typeExpr, Iterable<GoogRequire> requires) {
return Declaration.create(varName, value, typeExpr, requires);
}
/** Creates a code chunk representing the logical negation {@code !} of the given chunk. */
public static WithValue not(CodeChunk.WithValue arg) {
return PrefixUnaryOperation.create(Operator.NOT, arg);
}
/** Starts a {@code switch} statement dispatching on the given chunk. */
public static SwitchBuilder switch_(CodeChunk.WithValue switchOn) {
return new SwitchBuilder(switchOn);
}
/**
* Creates a code chunk representing the {@code new} operator applied to the given constructor. If
* you need to call the constructor with arguments, call {@link WithValue#call} on the returned
* chunk.
*/
public static WithValue new_(WithValue ctor) {
return New.create(ctor);
}
/**
* Creates a code chunk representing the given Soy operator applied to the given operands.
*
* <p>Cannot be used for {@link Operator#AND}, {@link Operator#OR}, or {@link
* Operator#CONDITIONAL}, as they require access to a {@link CodeChunk.Generator} to generate
* temporary variables for short-circuiting. Use {@link CodeChunk.WithValue#and}, {@link
* CodeChunk.WithValue#or}, and {@link CodeChunk.Generator#conditionalExpression} instead.
*/
public static WithValue operation(Operator op, List<WithValue> operands) {
Preconditions.checkArgument(operands.size() == op.getNumOperands());
Preconditions.checkArgument(
op != Operator.AND && op != Operator.OR && op != Operator.CONDITIONAL);
switch (op.getNumOperands()) {
case 1:
return PrefixUnaryOperation.create(op, operands.get(0));
case 2:
return BinaryOperation.create(op, operands.get(0), operands.get(1));
default:
throw new AssertionError();
}
}
/** Creates a code chunk representing a javascript array literal. */
public static WithValue arrayLiteral(Iterable<? extends WithValue> elements) {
return ArrayLiteral.create(ImmutableList.copyOf(elements));
}
/** Creates a code chunk representing a javascript map literal. */
public static WithValue mapLiteral(
Iterable<? extends WithValue> keys, Iterable<? extends WithValue> values) {
return MapLiteral.create(ImmutableList.copyOf(keys), ImmutableList.copyOf(values));
}
/** Creates a code chunk representing a for loop. */
public static CodeChunk forLoop(
String localVar,
CodeChunk.WithValue initial,
CodeChunk.WithValue limit,
CodeChunk.WithValue increment,
CodeChunk body) {
return For.create(localVar, initial, limit, increment, body);
}
/** Creates a code chunk representing a for loop, with default values for initial & increment. */
public static CodeChunk forLoop(String localVar, CodeChunk.WithValue limit, CodeChunk body) {
return For.create(localVar, number(0), limit, number(1), body);
}
/** Creates a code chunk that represents a return statement returning the given value. */
public static CodeChunk return_(CodeChunk.WithValue returnValue) {
return Return.create(returnValue);
}
/**
* Wraps a {@link JsExpr} that could have incorrect precedence in parens.
*
* <p>The JsExpr constructor is inherently error-prone. It allows callers to pass a precedence
* unrelated to the topmost operator in the text string. While JsExprs created in the Soy codebase
* can be audited, JsExprs are also returned by {@link SoyJsSrcFunction functions} and {@link
* SoyJsSrcPrintDirective print directives} owned by others. This method should be used to wrap
* the results of those plugins.
*/
public static WithValue dontTrustPrecedenceOf(
JsExpr couldHaveWrongPrecedence, Iterable<GoogRequire> requires) {
return Group.create(fromExpr(couldHaveWrongPrecedence, requires));
}
/**
* Creates a code chunk from the given text, treating it as a series of statements rather than an
* expression. For use only by {@link
* com.google.template.soy.jssrc.internal.GenJsCodeVisitor#visitReturningCodeChunk}.
*
* <p>TODO(user): remove.
*/
public static CodeChunk treatRawStringAsStatementLegacyOnly(
String rawString, Iterable<GoogRequire> requires) {
return LeafStatement.create(rawString.trim(), requires);
}
/**
* Marker class for a chunk of code that represents a value.
*
* <p>Expressions represent values. Sequences of statements can represent a value (for example, if
* the first statement declares a variable and subsequent statements update the variable's state),
* but they are not required to.
*
* <p>Chunks representing values are required in certain contexts (for example, the right-hand
* side of an {@link CodeChunk.WithValue#assign assignment}).
*/
@Immutable
public abstract static class WithValue extends CodeChunk {
public static final WithValue LITERAL_TRUE = id("true");
public static final WithValue LITERAL_FALSE = id("false");
public static final WithValue LITERAL_NULL = id("null");
public static final WithValue LITERAL_EMPTY_STRING = Leaf.create("''");
public static final WithValue EMPTY_OBJECT_LITERAL = Leaf.create("{}");
WithValue() { /* no subclasses outside this package */ }
public final CodeChunk.WithValue plus(CodeChunk.WithValue rhs) {
return BinaryOperation.create(Operator.PLUS, this, rhs);
}
public final CodeChunk.WithValue minus(CodeChunk.WithValue rhs) {
return BinaryOperation.create(Operator.MINUS, this, rhs);
}
public final CodeChunk.WithValue plusEquals(CodeChunk.WithValue rhs) {
return BinaryOperation.create(
"+=",
0, // the precedence of JS assignments (including +=) is lower than any Soy operator
Associativity.RIGHT,
this,
rhs);
}
public final CodeChunk.WithValue doubleEquals(CodeChunk.WithValue rhs) {
return BinaryOperation.create(Operator.EQUAL, this, rhs);
}
public final CodeChunk.WithValue doubleNotEquals(CodeChunk.WithValue rhs) {
return BinaryOperation.create(Operator.NOT_EQUAL, this, rhs);
}
public final CodeChunk.WithValue tripleEquals(CodeChunk.WithValue rhs) {
return BinaryOperation.create(
"===",
Operator.EQUAL.getPrecedence(),
Operator.EQUAL.getAssociativity(),
this,
rhs);
}
public final CodeChunk.WithValue doubleEqualsNull() {
return doubleEquals(LITERAL_NULL);
}
public final CodeChunk.WithValue times(CodeChunk.WithValue rhs) {
return BinaryOperation.create(Operator.TIMES, this, rhs);
}
public final CodeChunk.WithValue divideBy(CodeChunk.WithValue rhs) {
return BinaryOperation.create(Operator.DIVIDE_BY, this, rhs);
}
/**
* Returns a code chunk representing the logical and ({@code &&}) of this chunk with the given
* chunk.
*
* @param codeGenerator Required in case temporary variables need to be allocated for
* short-circuiting behavior ({@code rhs} should be evaluated only if the current chunk
* evaluates as true).
*/
public final CodeChunk.WithValue and(
CodeChunk.WithValue rhs, CodeChunk.Generator codeGenerator) {
return BinaryOperation.and(this, rhs, codeGenerator);
}
/**
* Returns a code chunk representing the logical or ({@code ||}) of this chunk with the given
* chunk.
*
* @param codeGenerator Required in case temporary variables need to be allocated for
* short-circuiting behavior ({@code rhs} should be evaluated only if the current chunk
* evaluates as false).
*/
public final CodeChunk.WithValue or(
CodeChunk.WithValue rhs, CodeChunk.Generator codeGenerator) {
return BinaryOperation.or(this, rhs, codeGenerator);
}
public final CodeChunk.WithValue op(Operator op, CodeChunk.WithValue rhs) {
return BinaryOperation.operation(op, ImmutableList.of(this, rhs));
}
/** Takes in a String identifier for convenience, since that's what most use cases need. */
public final CodeChunk.WithValue dotAccess(String identifier) {
return Dot.create(this, id(identifier));
}
public final CodeChunk.WithValue bracketAccess(CodeChunk.WithValue arg) {
return Bracket.create(this, arg);
}
public final CodeChunk.WithValue call(CodeChunk.WithValue... args) {
return call(Arrays.asList(args));
}
public final CodeChunk.WithValue call(Iterable<? extends CodeChunk.WithValue> args) {
return Call.create(this, ImmutableList.copyOf(args));
}
public final CodeChunk.WithValue instanceof_(CodeChunk.WithValue identifier) {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
// instanceof has the same precedence as LESS_THAN
return BinaryOperation.create(
"instanceof", Operator.LESS_THAN.getPrecedence(), Associativity.LEFT, this, identifier);
}
public final CodeChunk.WithValue assign(CodeChunk.WithValue rhs) {
return BinaryOperation.create(
"=",
0, // the precedence of JS assignments is lower than any Soy operator
Associativity.RIGHT,
this,
rhs);
}
/**
* Returns a chunk whose output expression is the same as this chunk's, but which includes the
* given initial statements.
*
* <p>This method is designed for interoperability with parts of the JS codegen system that do
* not understand code chunks. For example, when applying plugin functions, {@link
* com.google.template.soy.jssrc.internal.TranslateExprNodeVisitor#visitFunctionNode} needs to
* downgrade the plugin arguments from CodeChunk.WithValues to {@link JsExpr}s for the plugin
* API to process. The result (a JsExpr) needs to be upgraded back to a CodeChunk.WithValue that
* includes the initial statements from the original arguments.
*/
public final CodeChunk.WithValue withInitialStatements(
Iterable<? extends CodeChunk> initialStatements) {
// If there are no new initial statements, return the current chunk.
if (Iterables.isEmpty(initialStatements)) {
return this;
}
// Otherwise, return a code chunk that includes all of the dependent code.
return Composite.create(ImmutableList.copyOf(initialStatements), this);
}
/** Convenience method for {@code withInitialStatements(ImmutableList.of(statement))}. */
public final CodeChunk.WithValue withInitialStatement(CodeChunk initialStatement) {
return withInitialStatements(ImmutableList.of(initialStatement));
}
/**
* Returns true if this chunk can be represented as a single expression. This method should be
* rarely used, but is needed when interoperating with parts of the codegen system that do not
* yet understand CodeChunks (e.g. {@link SoyJsSrcFunction}).
*/
final boolean isRepresentableAsSingleExpression() {
return Iterables.isEmpty(initialStatements());
}
/**
* If this chunk can be represented as a single expression, returns that expression. If this
* chunk cannot be represented as a single expression, returns an expression containing
* references to a variable defined by the corresponding {@link #doFormatInitialStatements
* initial statements}.
*
* <p>This method should rarely be used, but is needed when interoperating with parts of the
* codegen system that do not yet understand CodeChunks (e.g. {@link SoyJsSrcFunction}).
*/
public abstract JsExpr singleExprOrName();
/**
* If this chunk can be represented as a single expression, writes that single expression to the
* buffer. If the chunk cannot be represented as a single expression, writes an expression to
* the buffer containing references to a variable defined by the corresponding {@link
* #doFormatInitialStatements initial statements}.
*
* <p>Must only be called by {@link FormattingContext#appendOutputExpression}.
*/
abstract void doFormatOutputExpr(FormattingContext ctx);
/**
* Returns the initial statements associated with this value. The statements must be serialized
* before this value (for example, they could contain declarations of variables referenced in
* this value).
*
* <p>These are direct dependencies only, not transitive.
*/
public abstract ImmutableSet<CodeChunk> initialStatements();
}
/**
* A trivial interface for {@link #collectRequires(RequiresCollector)} that can be used to collect
* all required namespaces from a code chunk.
*/
public interface RequiresCollector {
/** Drops all requires. */
final RequiresCollector NULL =
new RequiresCollector() {
@Override
public void add(GoogRequire require) {}
};
/** Collects requires into an ImmutableSet that can be accessed via {@link #get} */
final class IntoImmutableSet implements RequiresCollector {
private final ImmutableSet.Builder<GoogRequire> builder = ImmutableSet.builder();
@Override
public void add(GoogRequire require) {
builder.add(require);
}
public ImmutableSet<GoogRequire> get() {
return builder.build();
}
}
void add(GoogRequire require);
}
/** Adds all the 'goog.require' identifiers needed by this CodeChunk to the given collection. */
public abstract void collectRequires(RequiresCollector collector);
/**
* Returns a sequence of JavaScript statements. In the special case that this chunk is
* representable as a single expression, returns that expression followed by a semicolon.
*
* <p>This method is intended to be used at the end of codegen to emit the entire gencode. It
* should not be used within the codegen system for intermediate representations.
*
* <p>Because the returned code is intended to be used at the end of codegen, it does not end
* in a newline.
*/
public final String getCode() {
return getCode(0, OutputContext.STATEMENT);
}
/**
* Returns a sequence of JavaScript statements suitable for inserting into JS code
* that is not managed by the CodeChunk DSL. The string is guaranteed to end in a newline.
*
* <p>Callers should use {@link #getCode()} when the CodeChunk DSL is managing the entire
* code generation. getCode may drop variable declarations if there is no other code referencing
* those variables.
*
* <p>By contrast, this method is provided for incremental migration to the CodeChunk DSL.
* Variable declarations will not be dropped, since there may be gencode not managed by the
* CodeChunk DSL that references them.
*
* TODO(user): remove.
*
* @param startingIndent The indent level of the foreign code into which this code
* will be inserted. This doesn't affect the correctness of the composed code,
* only its readability.
*
*/
public final String getStatementsForInsertingIntoForeignCodeAtIndent(int startingIndent) {
String code = getCode(startingIndent, STATEMENT);
return code.endsWith("\n") ? code : code + "\n";
}
/**
* Returns a sequence of JavaScript statements. In the special case that this chunk is
* representable as a single expression, returns that expression
* <em>without</em> a trailing semicolon. (By contrast, {@link #getCode()} does send
* the trailing semicolon in such cases.)
*
* <p>This method is generally not safe, since concatenating statements that do not end
* in semicolons can cause arbitrary lexical errors. It's intended for use by unit tests
* whose assertions are currently written without trailing semicolons.
* TODO: migrate the unit tests and delete this method.
*/
@VisibleForTesting
public final String getExpressionTestOnly() {
return getCode(0, TRAILING_EXPRESSION);
}
/**
* Temporary method to ease migration to the CodeChunk DSL.
*
* <p>Because of the recursive nature of the JS codegen system, it is generally not possible
* to convert one codegen method at a time to use the CodeChunk DSL.
* However, the business logic inside those methods can be migrated incrementally.
* Methods that do not yet use the CodeChunk DSL can "unwrap" inputs using this method
* and "wrap" results using {@link CodeChunk#fromExpr(JsExpr)}. This is safe as long as
* each CodeChunk generated for production code is
* {@link CodeChunk.WithValue#isRepresentableAsSingleExpression}.
*
* TODO(user): remove.
*/
public final JsExpr assertExpr() {
RequiresCollector.IntoImmutableSet collector = new RequiresCollector.IntoImmutableSet();
JsExpr expr = assertExprAndCollectRequires(collector);
ImmutableSet<GoogRequire> requires = collector.get();
if (!requires.isEmpty()) {
throw new IllegalStateException("calling assertExpr() would drop requires!: " + requires);
}
return expr;
}
/**
* Temporary method to ease migration to the CodeChunk DSL.
*
* <p>Because of the recursive nature of the JS codegen system, it is generally not possible to
* convert one codegen method at a time to use the CodeChunk DSL. However, the business logic
* inside those methods can be migrated incrementally. Methods that do not yet use the CodeChunk
* DSL can "unwrap" inputs using this method and "wrap" results using {@link
* CodeChunk#fromExpr(JsExpr)}. This is safe as long as each CodeChunk generated for production
* code is {@link CodeChunk.WithValue#isRepresentableAsSingleExpression}.
*
* <p>TODO(user): remove.
*/
public final JsExpr assertExprAndCollectRequires(RequiresCollector collector) {
WithValue withValue = (WithValue) this;
if (!withValue.isRepresentableAsSingleExpression()) {
throw new IllegalStateException(String.format("Not an expr:\n%s", this.getCode()));
}
collectRequires(collector);
return withValue.singleExprOrName();
}
/**
* {@link #doFormatInitialStatements} and {@link CodeChunk.WithValue#doFormatOutputExpr} are the
* main methods subclasses should override to control their formatting. Subclasses should only
* override this method in the special case that a code chunk needs to control its formatting when
* it is the only chunk being serialized. TODO(brndn): only one override, can probably be declared
* final.
*
* @param startingIndent The indent level of the foreign code into which this code will be
* inserted. This doesn't affect the correctness of the composed code, only its readability.
* @param outputContext The grammatical context into which the output expression generated by this
* chunk (if any) will be inserted.
* <ul>
* <li>{@link OutputContext#STATEMENT}: the output expression will appear as its own
* statement. Include a trailing semicolon and newline.
* <li>{@link OutputContext#EXPRESSION}: the output expression is being inserted into
* another expression. Omit the trailing semicolon and newline.
* <li>{@link OutputContext#TRAILING_EXPRESSION}: the output expression is being inserted
* into another expression, but it is the last component of the entire unit of code that
* is being {@link CodeChunk#getCode() formatted}. There is therefore no need to
* serialize the name of variable that holds this expression's value, if any (since
* there is no following code that could reference it).
* </ul>
*/
@ForOverride
String getCode(int startingIndent, OutputContext outputContext) {
FormattingContext initialStatements = new FormattingContext(startingIndent);
initialStatements.appendInitialStatements(this);
FormattingContext outputExprs = new FormattingContext(startingIndent);
if (this instanceof WithValue) {
outputExprs.appendOutputExpression((WithValue) this);
if (outputContext == STATEMENT) {
outputExprs.append(';').endLine();
}
}
return initialStatements.concat(outputExprs).toString();
}
/**
* If this chunk can be represented as a single expression, does nothing. If this chunk cannot be
* represented as a single expression, writes everything except the final expression to the
* buffer. Must only be called by {@link FormattingContext#appendInitialStatements}.
*/
abstract void doFormatInitialStatements(FormattingContext ctx);
CodeChunk() {}
/**
* Code chunks in a single Soy template emit code into a shared JavaScript lexical scope, so they
* must use distinct variable names. This class enforces that.
*/
public static final class Generator {
private final UniqueNameGenerator nameGenerator;
private Generator(UniqueNameGenerator nameGenerator) {
this.nameGenerator = nameGenerator;
}
/** Returns an object that can be used to build code chunks. */
public static Generator create(UniqueNameGenerator nameGenerator) {
return new Generator(nameGenerator);
}
private String newVarName() {
return nameGenerator.generateName("$tmp");
}
/**
* Creates a code chunk declaring an automatically-named variable initialized to the given
* value.
*/
public Declaration declare(CodeChunk.WithValue rhs) {
return CodeChunk.declare(newVarName(), rhs);
}
/**
* Returns a code chunk representing an if-then-else condition.
*
* <p>If all the parameters are {@link WithValue#isRepresentableAsSingleExpression representable
* as single expressions}, the returned chunk will use the JavaScript ternary syntax ({@code
* predicate ? consequent : alternate}). Otherwise, the returned chunk will use JavaScript
* conditional statement syntax: <code>
* var $tmp = null;
* if (predicate) {
* $tmp = consequent;
* } else {
* $tmp = alternate;
* }
* </code>
*/
public CodeChunk.WithValue conditionalExpression(
CodeChunk.WithValue predicate,
CodeChunk.WithValue consequent,
CodeChunk.WithValue alternate) {
if (predicate.initialStatements().containsAll(consequent.initialStatements())
&& predicate.initialStatements().containsAll(alternate.initialStatements())) {
return Ternary.create(predicate, consequent, alternate);
}
return ifExpression(predicate, consequent).else_(alternate).build(this);
}
}
}