/* * Copyright 2011 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.soytree; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.base.internal.BaseUtils; import com.google.template.soy.basetree.CopyState; import com.google.template.soy.basetree.SyntaxVersionUpperBound; import com.google.template.soy.error.ErrorReporter.Checkpoint; import com.google.template.soy.error.SoyErrorKind; import com.google.template.soy.exprparse.SoyParsingContext; import com.google.template.soy.soytree.CommandTextAttributesParser.Attribute; import com.google.template.soy.soytree.defn.TemplateParam; import java.util.Collection; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** * Node representing a call to a basic template. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * */ public final class CallBasicNode extends CallNode { public static final SoyErrorKind MISSING_CALLEE_NAME = SoyErrorKind.of("Invalid ''call'' command missing callee name: '{'call {0}'}'."); public static final SoyErrorKind BAD_CALLEE_NAME = SoyErrorKind.of("Invalid callee name \"{0}\" for ''call'' command."); /** Helper class used by constructors. Encapsulates all the info derived from the command text. */ @Immutable private static final class CommandTextInfo extends CallNode.CommandTextInfo { /** The callee name string as it appears in the source code. */ private final String srcCalleeName; CommandTextInfo( String commandText, String srcCalleeName, DataAttribute dataAttr, @Nullable String userSuppliedPlaceholderName, @Nullable SyntaxVersionUpperBound syntaxVersionBound) { super(commandText, dataAttr, userSuppliedPlaceholderName, syntaxVersionBound); this.srcCalleeName = srcCalleeName; } } /** Pattern for a callee name not listed as an attribute function="...". */ private static final Pattern NONATTRIBUTE_CALLEE_NAME = Pattern.compile("^\\s* ([.\\w]+) (?= \\s | $)", Pattern.COMMENTS); /** Parser for the command text. */ private static final CommandTextAttributesParser ATTRIBUTES_PARSER = new CommandTextAttributesParser( "call", new Attribute("data", Attribute.ALLOW_ALL_VALUES, null)); /** The callee name string as it appears in the source code. */ private final String sourceCalleeName; /** The full name of the template being called. Briefly null before being set. */ private String calleeName; /** * The list of params that need to be type checked when this node is run. All the params that * could be statically verified will be checked up front (by the {@code * CheckCallingParamTypesVisitor}), this list contains the params that could not be statically * checked. * * <p>NOTE:This list will be a subset of the params of the callee, not a subset of the params * passed from this caller. */ private ImmutableList<TemplateParam> paramsToRuntimeTypeCheck; /** * Private constructor. {@link Builder} is the public API. * * @param id The id for this node. * @param sourceLocation The node's source location. * @param commandTextInfo All the info derived from the command text. * @param escapingDirectiveNames Call-site escaping directives used by strict autoescaping. */ private CallBasicNode( int id, SourceLocation sourceLocation, CommandTextInfo commandTextInfo, ImmutableList<String> escapingDirectiveNames, @Nullable String calleeName) { super(id, sourceLocation, "call", commandTextInfo, escapingDirectiveNames); this.sourceCalleeName = commandTextInfo.srcCalleeName; this.calleeName = calleeName; } /** * Copy constructor. * * @param orig The node to copy. */ private CallBasicNode(CallBasicNode orig, CopyState copyState) { super(orig, copyState); this.sourceCalleeName = orig.sourceCalleeName; this.calleeName = orig.calleeName; this.paramsToRuntimeTypeCheck = orig.paramsToRuntimeTypeCheck; } @Override public Kind getKind() { return Kind.CALL_BASIC_NODE; } /** Returns the callee name string as it appears in the source code. */ public String getSrcCalleeName() { return sourceCalleeName; } /** * Sets the full name of the template being called (must not be a partial name). * * @param calleeName The full name of the template being called. */ public void setCalleeName(String calleeName) { Preconditions.checkState(this.calleeName == null); Preconditions.checkArgument(BaseUtils.isDottedIdentifier(calleeName)); this.calleeName = calleeName; } /** * Sets the names of the parameters that require runtime type checking against the callees formal * types. */ public void setParamsToRuntimeCheck(Collection<TemplateParam> paramNames) { this.paramsToRuntimeTypeCheck = ImmutableList.copyOf(paramNames); } @Override public Collection<TemplateParam> getParamsToRuntimeCheck(TemplateNode callee) { return paramsToRuntimeTypeCheck == null ? callee.getParams() : paramsToRuntimeTypeCheck; } /** Returns the full name of the template being called, or null if not yet set. */ public String getCalleeName() { return calleeName; } @Override public CallBasicNode copy(CopyState copyState) { return new CallBasicNode(this, copyState); } public static final class Builder extends CallNode.Builder { private static CallBasicNode error() { return new Builder(-1, SourceLocation.UNKNOWN) .commandText(".error") .build(SoyParsingContext.exploding()); // guaranteed to be valid } private final int id; private final SourceLocation sourceLocation; private ImmutableList<String> escapingDirectiveNames = ImmutableList.of(); private DataAttribute dataAttr = DataAttribute.none(); @Nullable private String commandText; @Nullable private String userSuppliedPlaceholderName; @Nullable private String calleeName; @Nullable private String sourceCalleeName; @Nullable private SyntaxVersionUpperBound syntaxVersionBound; public Builder(int id, SourceLocation sourceLocation) { this.id = id; this.sourceLocation = sourceLocation; } public Builder calleeName(String calleeName) { this.calleeName = calleeName; return this; } @Override public SourceLocation getSourceLocation() { return sourceLocation; } @Override public Builder commandText(String commandText) { this.commandText = commandText; return this; } public Builder escapingDirectiveNames(ImmutableList<String> escapingDirectiveNames) { this.escapingDirectiveNames = escapingDirectiveNames; return this; } public Builder dataAttribute(DataAttribute dataAttr) { this.dataAttr = dataAttr; return this; } public Builder sourceCalleeName(String sourceCalleeName) { this.sourceCalleeName = sourceCalleeName; return this; } public Builder syntaxVersionBound(SyntaxVersionUpperBound syntaxVersionBound) { this.syntaxVersionBound = syntaxVersionBound; return this; } @Override public Builder userSuppliedPlaceholderName(String userSuppliedPlaceholderName) { this.userSuppliedPlaceholderName = userSuppliedPlaceholderName; return this; } @Override public CallBasicNode build(SoyParsingContext context) { Checkpoint c = context.errorReporter().checkpoint(); CommandTextInfo commandTextInfo = commandText != null ? parseCommandText(context) : buildCommandText(); if (context.errorReporter().errorsSince(c)) { return error(); } CallBasicNode callBasicNode = new CallBasicNode( id, sourceLocation, commandTextInfo, escapingDirectiveNames, calleeName); return callBasicNode; } // TODO(user): eliminate side-channel parsing. This should be a part of the grammar. private CommandTextInfo parseCommandText(SoyParsingContext context) { String cmdText = commandText + ((userSuppliedPlaceholderName != null) ? " phname=\"" + userSuppliedPlaceholderName + "\"" : ""); String cmdTextForParsing = commandText; SyntaxVersionUpperBound syntaxVersionBound = null; Matcher ncnMatcher = NONATTRIBUTE_CALLEE_NAME.matcher(cmdTextForParsing); if (ncnMatcher.find()) { sourceCalleeName = ncnMatcher.group(1); cmdTextForParsing = cmdTextForParsing.substring(ncnMatcher.end()).trim(); if (!(BaseUtils.isIdentifierWithLeadingDot(sourceCalleeName) || BaseUtils.isDottedIdentifier(sourceCalleeName))) { context.report(sourceLocation, BAD_CALLEE_NAME, sourceCalleeName); } } else { context.report(sourceLocation, MISSING_CALLEE_NAME, commandText); } Map<String, String> attributes = ATTRIBUTES_PARSER.parse(cmdTextForParsing, context, sourceLocation); DataAttribute dataAttrInfo = parseDataAttributeHelper(attributes.get("data"), sourceLocation, context); return new CommandTextInfo( cmdText, sourceCalleeName, dataAttrInfo, userSuppliedPlaceholderName, syntaxVersionBound); } // TODO(user): eliminate side-channel parsing. This should be a part of the grammar. private CommandTextInfo buildCommandText() { String commandText = sourceCalleeName; if (dataAttr.isPassingAllData()) { commandText += " data=\"all\""; } else if (dataAttr.isPassingData()) { assert dataAttr.dataExpr() != null; // suppress warnings commandText += " data=\"" + dataAttr.dataExpr().toSourceString() + '"'; } if (userSuppliedPlaceholderName != null) { commandText += " phname=\"" + userSuppliedPlaceholderName + '"'; } return new CommandTextInfo( commandText, sourceCalleeName, dataAttr, userSuppliedPlaceholderName, syntaxVersionBound); } } }