Skip to content

Commit

Permalink
Add a synthetic print node to flush logging attributes on html nodes …
Browse files Browse the repository at this point in the history
…to support ve log attribute injection.

This will only be needed for server side rendering and as such it is disabled in other backends.  Right now it is configured to be a no-op for jbcsrc.  Future changes will provide a way for loggers to supply a value to be printed.

PiperOrigin-RevId: 705270121
  • Loading branch information
lukesandberg authored and copybara-github committed Dec 16, 2024
1 parent 33c415b commit bec60d6
Show file tree
Hide file tree
Showing 16 changed files with 207 additions and 17 deletions.
24 changes: 17 additions & 7 deletions java/src/com/google/template/soy/SoyFileSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ public SoyTofu compileToTofu() {
public SoyTofu compileToTofu(Map<String, ? extends Supplier<Object>> pluginInstances) {
return entryPoint(
() -> {
ServerCompilationPrimitives primitives = compileForServerRendering();
ServerCompilationPrimitives primitives = compileForServerRendering(/* isTofu= */ true);
throwIfErrorsPresent();
return doCompileToTofu(primitives, pluginInstances);
});
Expand Down Expand Up @@ -974,7 +974,7 @@ public SoySauce compileTemplates() {
public SoySauce compileTemplates(Map<String, ? extends Supplier<Object>> pluginInstances) {
return entryPoint(
() -> {
ServerCompilationPrimitives primitives = compileForServerRendering();
ServerCompilationPrimitives primitives = compileForServerRendering(/* isTofu= */ false);
throwIfErrorsPresent();
return doCompileSoySauce(primitives, PluginInstances.of(pluginInstances));
});
Expand All @@ -994,7 +994,7 @@ public CssRegistry getCssRegistry() {
void compileToJar(ByteSink jarTarget, Optional<ByteSink> srcJarTarget) {
entryPointVoid(
() -> {
ServerCompilationPrimitives primitives = compileForServerRendering();
ServerCompilationPrimitives primitives = compileForServerRendering(/* isTofu= */ false);
try {
BytecodeCompiler.compileToJar(
primitives.soyTree, errorReporter, typeRegistry, jarTarget, primitives.registry);
Expand Down Expand Up @@ -1035,15 +1035,15 @@ private static final class ServerCompilationPrimitives {
}

/** Runs common compiler logic shared by tofu and jbcsrc backends. */
private ServerCompilationPrimitives compileForServerRendering() {
ParseResult result = parse();
private ServerCompilationPrimitives compileForServerRendering(boolean isTofu) {
ParseResult result = parse(passManagerBuilder().addHtmlAttributesForLogging(!isTofu));
throwIfErrorsPresent();

SoyFileSetNode soyTree = result.fileSet();
FileSetMetadata registry = result.registry();
// Clear the SoyDoc strings because they use unnecessary memory, unless we have a cache, in
// which case it is pointless.
if (cache == null) {
if (cache == null && isTofu) {
new ClearSoyDocStringsVisitor().exec(soyTree);
}

Expand Down Expand Up @@ -1078,6 +1078,8 @@ List<String> compileToJsSrcInternal(
passManagerBuilder()
.allowUnknownJsGlobals()
.desugarHtmlNodes(false)
// Because we log by iterating the dom, this is not needed.
.addHtmlAttributesForLogging(false)
.validateJavaMethods(false);
ParseResult result = parse(builder);
throwIfErrorsPresent();
Expand Down Expand Up @@ -1108,6 +1110,8 @@ List<String> compileToIncrementalDomSrcInternal(SoyIncrementalDomSrcOptions jsSr
.desugarHtmlNodes(false)
.allowUnknownJsGlobals()
.desugarIdomFeatures(false)
// Because we log while producing the dom, this is not needed.
.addHtmlAttributesForLogging(false)
.validateJavaMethods(false));
throwIfErrorsPresent();
return new IncrementalDomSrcMain(scopedData.enterable(), typeRegistry)
Expand All @@ -1127,7 +1131,12 @@ List<String> compileToPySrcFiles(SoyPySrcOptions pySrcOptions) {
return entryPoint(
() -> {
try {
ParseResult result = parse(passManagerBuilder().validateJavaMethods(false));
ParseResult result =
parse(
passManagerBuilder()
.validateJavaMethods(false)
// pysrc doesn't support velogging
.addHtmlAttributesForLogging(false));
throwIfErrorsPresent();
return new PySrcMain(scopedData.enterable())
.genPyFiles(result.fileSet(), result.registry(), pySrcOptions, errorReporter);
Expand Down Expand Up @@ -1227,6 +1236,7 @@ private PassManager.Builder passManagerBuilderForAnalysis(AstRewrites astRewrite
.astRewrites(astRewrites)
// skip adding extra attributes
.addHtmlAttributesForDebugging(false)
.addHtmlAttributesForLogging(false)
// skip the autoescaper
.insertEscapingDirectives(false)
.desugarHtmlNodes(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ public abstract LoggingAdvisingAppendable append(CharSequence csq, int start, in
@Nonnull
public abstract LoggingAdvisingAppendable exitLoggableElement();

/** Flushes all pending logging attributes. */
public final LoggingAdvisingAppendable flushPendingLoggingAttributes() {
// TODO(b/383661457): Implement this.
return this;
}

/**
* Flushes all intermediate buffers stored within the appendable.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ static Statement concat(List<Statement> statements) {
private static final MethodRef FLUSH_BUFFERS =
MethodRef.createNonPure(LoggingAdvisingAppendable.class, "flushBuffers", int.class);

private static final MethodRef FLUSH_PENDING_LOGGING_ATTRBIUTES =
MethodRef.createNonPure(LoggingAdvisingAppendable.class, "flushPendingLoggingAttributes");

static AppendableExpression forExpression(Expression delegate) {
return new AppendableExpression(delegate, e -> e, /* supportsSoftLimiting= */ true);
}
Expand Down Expand Up @@ -268,6 +271,10 @@ AppendableExpression setSanitizedContentKindAndDirectionality(SanitizedContentKi
BytecodeUtils.constantSanitizedContentKindAsContentKind(kind)));
}

AppendableExpression flushPendingLoggingAttributes() {
return withNewDelegate(e -> e.invoke(FLUSH_PENDING_LOGGING_ATTRBIUTES));
}

Statement flushBuffers(int depth) {
return delegate.invokeVoid(FLUSH_BUFFERS, constant(depth));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ protected final T visitFunctionNode(FunctionNode node) {
return visitIsTruthyNonEmptyFunction(node);
case NEW_SET:
return visitNewSetFunction(node);
case FLUSH_PENDING_LOGGING_ATTRIBUTES:
return visitFlushPendingLoggingAttributesFunction(node);
case MSG_WITH_ID:
case REMAINDER:
// should have been removed earlier in the compiler
Expand Down Expand Up @@ -231,4 +233,8 @@ T visitIsTruthyNonEmptyFunction(FunctionNode node) {
T visitNewSetFunction(FunctionNode node) {
return visitExprNode(node);
}

T visitFlushPendingLoggingAttributesFunction(FunctionNode node) {
return visitExprNode(node);
}
}
4 changes: 4 additions & 0 deletions java/src/com/google/template/soy/jbcsrc/SoyNodeCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import com.google.template.soy.msgs.internal.MsgUtils;
import com.google.template.soy.msgs.internal.MsgUtils.MsgPartsAndIds;
import com.google.template.soy.passes.IndirectParamsCalculator;
import com.google.template.soy.shared.internal.BuiltinFunction;
import com.google.template.soy.shared.restricted.SoyFunctionSignature;
import com.google.template.soy.shared.restricted.SoyPrintDirective;
import com.google.template.soy.soytree.AbstractReturningSoyNodeVisitor;
Expand Down Expand Up @@ -841,6 +842,9 @@ protected Statement visitPrintNode(PrintNode node) {
if (fn.getSoyFunction() instanceof LoggingFunction) {
return visitLoggingFunction(node, fn, (LoggingFunction) fn.getSoyFunction());
}
if (fn.getSoyFunction() == BuiltinFunction.FLUSH_PENDING_LOGGING_ATTRIBUTES) {
return appendableExpression.flushPendingLoggingAttributes().toStatement();
}
}
// First check our special case where all print directives are streamable and an expression that
// evaluates to a SoyValueProvider. This will allow us to render incrementally.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ protected void visitFunctionNode(FunctionNode node) {
case HAS_CONTENT:
case IS_TRUTHY_NON_EMPTY:
case NEW_SET:
case FLUSH_PENDING_LOGGING_ATTRIBUTES:
// visit children normally
break;
case UNKNOWN_JS_GLOBAL:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ private JsRuntime() {}

public static final Expression SOY_DEBUG_SOY_TEMPLATE_INFO =
SOY.dotAccess("$$getDebugSoyTemplateInfo");

public static final Expression SOY_ARE_YOU_AN_INTERNAL_CALLER =
SOY.dotAccess("$$areYouAnInternalCaller");
public static final Expression SOY_INTERNAL_CALL_MARKER =
Expand All @@ -174,6 +173,9 @@ private JsRuntime() {}
public static final Expression SOY_VISUAL_ELEMENT_DATA =
Expressions.group(SOY_VELOG.googModuleGet().dotAccess("$$VisualElementData"));

public static final Expression SOY_VISUAL_ELEMENT_FLUSH_PENDING_LOGGING_ATTRIBUTES =
SOY_VELOG.googModuleGet().dotAccess("$$flushPendingLoggingAttributes");

public static final Expression WINDOW_CONSOLE_LOG = dottedIdNoRequire("window.console.log");

public static final Expression XID = XID_REQUIRE.reference();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,7 @@ protected Expression visitFunctionNode(FunctionNode node) {
return hasContent(visit(node.getParam(0)));
case NEW_SET:
return visitNewSetFunction(node);
case FLUSH_PENDING_LOGGING_ATTRIBUTES:
case LEGACY_DYNAMIC_TAG:
case REMAINDER:
case MSG_WITH_ID:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2024 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.passes;

import com.google.common.collect.ImmutableList;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.base.internal.IdGenerator;
import com.google.template.soy.base.internal.Identifier;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.shared.internal.BuiltinFunction;
import com.google.template.soy.soytree.HtmlAttributeNode;
import com.google.template.soy.soytree.HtmlContext;
import com.google.template.soy.soytree.HtmlOpenTagNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.SoyTreeUtils;
import com.google.template.soy.soytree.VeLogNode;
import com.google.template.soy.types.SanitizedType.AttributesType;

/**
* Inserts calls to {@code $$flushPendingLoggingAttributes()} at the end of root elements in element
* style templates.
*/
@RunAfter(AutoescaperPass.class)
@RunBefore(DesugarHtmlNodesPass.class)
final class AddFlushPendingLoggingAttributesPass implements CompilerFilePass {

AddFlushPendingLoggingAttributesPass() {}

@Override
public void run(SoyFileNode file, IdGenerator nodeIdGen) {
// we instrument an open tag node if
// 1. it is a direct child of a VeLogNode
for (var velogNode : SoyTreeUtils.getAllNodesOfType(file, VeLogNode.class)) {
var openTag = velogNode.getOpenTagNode();
if (openTag != null) {
instrumentNode(nodeIdGen, openTag);
}
}
// 2. it is the direct child of a template node that is an 'element'
for (var template : file.getTemplates()) {
var metadata = template.getHtmlElementMetadata();
if (metadata == null || !metadata.getIsHtmlElement()) {
continue;
}
// If we are an element then there is either a single root element at the top level, or under
// a velog command, or there is a delegating call either way we are just looking for an
// HtmlOpenTageNode one or two levels deep.
var openTag =
template.getChildren().stream()
.filter(node -> node instanceof HtmlOpenTagNode)
.findFirst();
if (openTag.isPresent()) {
instrumentNode(nodeIdGen, (HtmlOpenTagNode) openTag.get());
}
}
}

/**
* Adds the {@code $$flushPendingLoggingAttributes()} call to the end of the open tag node.
*
* <p>By placing them at the end, we ensure that they will simply be ignored by browesers.
*
* <p>See:
* https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state:parse-error-duplicate-attribute
* which states that duplicate attributes are ignored.
*
* <p>This is not ideal behavior but aligns with the behavior of the javascript implementations
* which will also ignore the duplicate attributes (though they will throw in debug builds).
*
* <p>TODO: b/383661457 - throw an error if duplicates get printed. This will require tracking the
* full set of attributes that are printed for an element that has this call added.
*/
private void instrumentNode(IdGenerator nodeIdGen, HtmlOpenTagNode openTag) {
var functionCall =
FunctionNode.newPositional(
Identifier.create(
BuiltinFunction.FLUSH_PENDING_LOGGING_ATTRIBUTES.name(), SourceLocation.UNKNOWN),
BuiltinFunction.FLUSH_PENDING_LOGGING_ATTRIBUTES,
SourceLocation.UNKNOWN);
functionCall.setType(AttributesType.getInstance());
var printNode =
new PrintNode(
nodeIdGen.genId(),
SourceLocation.UNKNOWN,
/* isImplicit= */ true,
functionCall,
/* attributes= */ ImmutableList.of(),
ErrorReporter.exploding());
printNode.setHtmlContext(HtmlContext.HTML_TAG);
var attributeNode =
new HtmlAttributeNode(
nodeIdGen.genId(),
SourceLocation.UNKNOWN,
/* equalsSignLocation= */ null,
/* isSoyAttr= */ false);
attributeNode.addChild(printNode);
openTag.addChild(attributeNode);
}
}
21 changes: 16 additions & 5 deletions java/src/com/google/template/soy/passes/DesugarHtmlNodesPass.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.google.template.soy.base.internal.IdGenerator;
import com.google.template.soy.base.internal.Identifier;
import com.google.template.soy.basicdirectives.BasicEscapeDirective;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.shared.internal.BuiltinFunction;
import com.google.template.soy.shared.restricted.SoyPrintDirective;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.CallNode;
Expand Down Expand Up @@ -203,6 +205,7 @@ protected void visitHtmlAttributeNode(HtmlAttributeNode node) {

ImmutableList.Builder<StandaloneNode> builder = ImmutableList.builder();
boolean needsDynamicSpace = node.getStaticKey() == null && !node.hasValue();
boolean isFlushPendingLoggingAttribute = false;
if (needsSpaceForAttribute && !needsDynamicSpace) {
// prefix the value with a single whitespace character when the attribute is static. This
// makes it unambiguous with the preceding attribute/tag name.
Expand All @@ -212,16 +215,24 @@ protected void visitHtmlAttributeNode(HtmlAttributeNode node) {
// use a print directive to conditionally add a whitespace for dynamic attributes.
SoyPrintDirective whitespaceDirective =
new BasicEscapeDirective.WhitespaceHtmlAttributesDirective();
if (node.getChild(0) instanceof PrintNode) {
var first = node.getChild(0);
if (first instanceof PrintNode) {
var printNode = (PrintNode) first;
if (printNode.getExpr().getRoot() instanceof FunctionNode
&& ((FunctionNode) printNode.getExpr().getRoot()).getSoyFunction()
== BuiltinFunction.FLUSH_PENDING_LOGGING_ATTRIBUTES) {
isFlushPendingLoggingAttribute = true;
} else {
PrintDirectiveNode whitespaceDirectiveNode =
PrintDirectiveNode.createSyntheticNode(
idGenerator.genId(),
Identifier.create("|whitespaceHtmlAttributes", node.getSourceLocation()),
node.getSourceLocation());
whitespaceDirectiveNode.setPrintDirective(whitespaceDirective);
((PrintNode) node.getChild(0)).addChild(whitespaceDirectiveNode);
} else if (node.getChild(0) instanceof CallNode) {
CallNode typed = (CallNode) node.getChild(0);
printNode.addChild(whitespaceDirectiveNode);
}
} else if (first instanceof CallNode) {
CallNode typed = (CallNode) first;
typed.setEscapingDirectives(
ImmutableList.<SoyPrintDirective>builder()
.addAll(typed.getEscapingDirectives())
Expand All @@ -241,7 +252,7 @@ protected void visitHtmlAttributeNode(HtmlAttributeNode node) {
// normally there would only be 1 child, but rewriting may have split it into multiple
builder.addAll(node.getChildren().subList(1, node.numChildren()));
}
if (!node.hasValue() && node.getStaticKey() == null) {
if (!node.hasValue() && node.getStaticKey() == null && !isFlushPendingLoggingAttribute) {
// Add a space after the last attribute if it is dynamic and the tag is self-closing. If the
// attribute value isn't quoted, a space is needed to disambiguate with the "/" character.
needsSpaceSelfClosingTag = true;
Expand Down
11 changes: 10 additions & 1 deletion java/src/com/google/template/soy/passes/PassManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ public static final class Builder {
private ValidatedConformanceConfig conformanceConfig = ValidatedConformanceConfig.EMPTY;
private boolean insertEscapingDirectives = true;
private boolean addHtmlAttributesForDebugging = true;
private boolean addHtmlAttributesForLogging = true;
private AstRewrites astRewrites = AstRewrites.ALL;
private final Map<Class<? extends CompilerPass>, PassContinuationRule>
passContinuationRegistry = Maps.newHashMap();
Expand Down Expand Up @@ -453,6 +454,12 @@ public Builder addHtmlAttributesForDebugging(boolean addHtmlAttributesForDebuggi
return this;
}

@CanIgnoreReturnValue
public Builder addHtmlAttributesForLogging(boolean addHtmlAttributesForLogging) {
this.addHtmlAttributesForLogging = addHtmlAttributesForLogging;
return this;
}

/** Configures this passmanager to run the conformance pass using the given config object. */
@CanIgnoreReturnValue
public Builder setConformanceConfig(ValidatedConformanceConfig conformanceConfig) {
Expand Down Expand Up @@ -713,7 +720,6 @@ public PassManager build() {
}
} else {
if (astRewrites.combineTextNodes()) {

passes.add(new CombineConsecutiveRawTextNodesPass());
}
passes.add(
Expand All @@ -724,6 +730,9 @@ public PassManager build() {
accumulatedState::registryFull));
passes.add(new IncrementalDomKeysPass(disableAllTypeChecking));
}
if (addHtmlAttributesForLogging) {
passes.add(new AddFlushPendingLoggingAttributesPass());
}
passes.add(new CallAnnotationPass());

// Relies on information from the autoescaper and valid type information
Expand Down
Loading

0 comments on commit bec60d6

Please sign in to comment.