/*
* Copyright 2017 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.internal;
import static com.google.template.soy.jssrc.dsl.CodeChunk.WithValue.LITERAL_NULL;
import static com.google.template.soy.jssrc.dsl.CodeChunk.ifExpression;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_ARRAY_MAP;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.ForOverride;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import com.google.template.soy.jssrc.dsl.CodeChunk.WithValue;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
/**
* Represents a chain of (possibly null-safe) dot or bracket accesses. Used by {@link
* TranslateExprNodeVisitor#visitNullSafeNode}. TODO(user): remove; simply emit {@link
* CodeChunk#ifStatement conditional statements}. We can't do this yet though, because we can't
* generate non-expression outside of test code.
*/
final class NullSafeAccumulator {
/** The chain's base value. */
private final CodeChunk.WithValue base;
/**
* Represents each "link" in the chain. For example, for the chain {@code a?.b?.c.d},
* there are three links, {@code ?.b}, {@code ?.c}, and {@code .d}.
*/
private final List<ChainAccess> chain;
/**
* A chain of dot accesses can end in a {@link com.google.common.html.types.SafeHtmlProto}
* (SafeStyleProto, etc.). Such a chain needs to be
* {@link com.google.template.soy.data.internalutils.NodeContentKinds#toJsUnpackFunction unpacked}
* to a SanitizedContent object before it can be used in the JS runtime.
*/
@Nullable private CodeChunk.WithValue unpackFunction;
/**
* A chain of dot accesses can end in a reference to a repeated {@link
* com.google.common.html.types.SafeHtmlProto} field (SafeStyleProto field, etc.). The array
* representing the repeated field needs to be unpacked by mapping it through the appropriate
* {@link com.google.template.soy.data.internalutils.NodeContentKinds#toJsUnpackFunction } unpack
* function to produce an array of SanitizedContent objects before it can be used in the JS
* runtime.
*/
private boolean isRepeated = false;
/** Creates a NullSafeAccumulator with the given base chunk. */
NullSafeAccumulator(CodeChunk.WithValue base) {
this.base = base;
this.chain = new ArrayList<>();
}
/**
* Extends the access chain with a dot access to the given value.
* @param nullSafe If true, code will be generated to ensure the chain is non-null before
* dereferencing {@code arg}.
*/
NullSafeAccumulator dotAccess(FieldAccess arg, boolean nullSafe) {
if (arg instanceof CallAndUnpack) {
Preconditions.checkState(
unpackFunction == null, "this chain will already unpack with", unpackFunction);
unpackFunction = ((CallAndUnpack) arg).unpackFunctionName();
isRepeated = ((CallAndUnpack) arg).isRepeated();
}
chain.add(arg.toChainAccess(nullSafe));
return this;
}
/**
* Extends the access chain with a bracket access to the given value.
* @param nullSafe If true, code will be generated to ensure the chain is non-null before
* dereferencing {@code arg}.
*/
NullSafeAccumulator bracketAccess(CodeChunk.WithValue arg, boolean nullSafe) {
chain.add(new Bracket(arg, nullSafe));
return this;
}
/**
* Returns a code chunk representing the entire access chain. Null-safe accesses in the chain
* generate code to make sure the chain is non-null before performing the access.
*/
CodeChunk.WithValue result(CodeChunk.Generator codeGenerator) {
// First generate a list of every partial evaluation of the chain.
ImmutableList<CodeChunk.WithValue> intermediateValues = buildIntermediateValues();
Preconditions.checkState(intermediateValues.size() == chain.size() + 1);
// Walk backwards through the intermediate values. For any null-safe link in the chain,
// test the intermediate value against null before dereferencing it.
// For example, to translate a?.b.c, the rightmost link is not null-safe, so it translates to
// a.b.c. The next link is null-safe, so it translates to a == null ? null : a.b.c.
CodeChunk.WithValue cur = intermediateValues.get(intermediateValues.size() - 1);
for (int i = intermediateValues.size() - 2; i >= 0; --i) {
CodeChunk.WithValue chunk = intermediateValues.get(i);
boolean nullSafe = chain.get(i).nullSafe;
if (nullSafe) {
cur = ifExpression(chunk.doubleEqualsNull(), LITERAL_NULL).else_(cur).build(codeGenerator);
}
}
if (unpackFunction == null) {
return cur;
} else if (!isRepeated) {
// It's okay if the whole chain evals to null. The unpack functions accept null.
return unpackFunction.call(cur);
} else {
return GOOG_ARRAY_MAP.call(cur, unpackFunction);
}
}
/**
* Builds a list of intermediate values representing partial evaluation of the chain.
* For example, the chain {@code a?.b?.c.d} has four intermediate values:
* <ol>
* <li>{@code a}
* <li>{@code a?.b}
* <li>{@code a?.b?.c}
* <li>{@code a?.b?.c.d}
* </ol>
*/
private ImmutableList<CodeChunk.WithValue> buildIntermediateValues() {
ImmutableList.Builder<CodeChunk.WithValue> builder = ImmutableList.builder();
CodeChunk.WithValue prev = base;
builder.add(prev);
for (ChainAccess link : chain) {
prev = link.extend(prev);
builder.add(prev);
}
return builder.build();
}
/**
* Abstract base class for extending the access chain with {@link Dot dot accesses},
* {@link Bracket bracket accesses}, and {@link DotCall dot accesses followed by a function call}.
*/
private abstract static class ChainAccess {
/** How to extend the tip of the chain. */
abstract CodeChunk.WithValue extend(CodeChunk.WithValue prevTip);
final boolean nullSafe;
ChainAccess(boolean nullSafe) {
this.nullSafe = nullSafe;
}
}
/** Extends the chain with a (null-safe or not) bracket access. */
private static final class Bracket extends ChainAccess {
final CodeChunk.WithValue value;
Bracket(CodeChunk.WithValue value, boolean nullSafe) {
super(nullSafe);
this.value = value;
}
@Override
CodeChunk.WithValue extend(CodeChunk.WithValue prevTip) {
return prevTip.bracketAccess(value);
}
}
/** Extends the chain with a (null-safe or not) dot access. */
private static final class Dot extends ChainAccess {
final String id;
Dot(String id, boolean nullSafe) {
super(nullSafe);
this.id = id;
}
@Override
CodeChunk.WithValue extend(CodeChunk.WithValue prevTip) {
return prevTip.dotAccess(id);
}
}
/**
* Extends the chain with a (null-safe or not) dot access followed by a function call.
* See {@link FieldAccess} for rationale.
*/
private static final class DotCall extends ChainAccess {
final String getter;
@Nullable final CodeChunk.WithValue arg;
DotCall(String getter, @Nullable CodeChunk.WithValue arg, boolean nullSafe) {
super(nullSafe);
this.getter = getter;
this.arg = arg;
}
@Override
WithValue extend(WithValue prevTip) {
return arg == null
? prevTip.dotAccess(getter).call()
: prevTip.dotAccess(getter).call(arg);
}
}
/**
* {@link NullSafeAccumulator} works by extending the tip of a chain of accesses.
* In some situations (e.g. {@link TranslateExprNodeVisitor#genCodeForProtoAccess}),
* the tip is "extended" by a dot access followed by a function call. Because dot accesses
* have higher precedence in JS than function calls, the extension cannot be represented by a
* single {@link CodeChunk.WithValue} that is attached to the previous tip; that would generate
* an incorrect pair of parens around the function call, e.g. {@code proto.(getFoo())} instead of
* {@code proto.getFoo()}. This tuple is a workaround for that precedence issue.
*/
abstract static class FieldAccess {
@ForOverride
abstract ChainAccess toChainAccess(boolean nullSafe);
static FieldAccess id(String fieldName) {
return new AutoValue_NullSafeAccumulator_Id(fieldName);
}
static FieldAccess call(String getter, CodeChunk.WithValue arg) {
return new AutoValue_NullSafeAccumulator_Call(getter, arg);
}
static FieldAccess call(String getter) {
return new AutoValue_NullSafeAccumulator_Call(getter, null /* arg */);
}
static CallAndUnpack.Builder callAndUnpack() {
return new AutoValue_NullSafeAccumulator_CallAndUnpack.Builder();
}
}
@AutoValue
abstract static class Id extends FieldAccess {
abstract String fieldName();
@Override
ChainAccess toChainAccess(boolean nullSafe) {
return new Dot(fieldName(), nullSafe);
}
}
@AutoValue
abstract static class Call extends FieldAccess {
abstract String getter();
@Nullable abstract CodeChunk.WithValue arg();
@Override
ChainAccess toChainAccess(boolean nullSafe) {
return new DotCall(getter(), arg(), nullSafe);
}
}
@AutoValue
abstract static class CallAndUnpack extends FieldAccess {
abstract String getter();
@Nullable abstract CodeChunk.WithValue arg();
abstract CodeChunk.WithValue unpackFunctionName();
abstract boolean isRepeated();
@Override
ChainAccess toChainAccess(boolean nullSafe) {
return new DotCall(getter(), arg(), nullSafe);
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder getter(String getter);
abstract Builder arg(CodeChunk.WithValue arg);
abstract Builder unpackFunctionName(CodeChunk.WithValue unpackFunctionName);
abstract Builder isRepeated(boolean isRepeated);
abstract CallAndUnpack build();
}
}
}