/* * 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.passes; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.template.soy.base.internal.IdGenerator; import com.google.template.soy.basetree.SyntaxVersion; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.ErrorReporter.Checkpoint; import com.google.template.soy.error.SoyErrorKind; import com.google.template.soy.shared.SoyGeneralOptions; import com.google.template.soy.shared.restricted.SoyFunction; import com.google.template.soy.soytree.AliasDeclaration; import com.google.template.soy.soytree.SoyFileNode; import com.google.template.soy.soytree.SoyFileSetNode; import com.google.template.soy.soytree.SoyTreeUtils; import com.google.template.soy.soytree.TemplateRegistry; import com.google.template.soy.types.SoyTypeRegistry; import java.util.Objects; /** * Configures all the parsing passes. * * <p>The parsing passes are a collection of operations that mutate/rewrite parts of the parse tree * in trivial/obvious ways. These passes are logically part of parsing the literal text of the soy * file and each one could theoretically be done as part of the parser, but for maintainability it * is easier to pull them out into separate passes. It is expected that each of these passes will * mutate the AST in critical ways. */ public final class PassManager { private final ImmutableList<CompilerFilePass> singleFilePasses; private final ImmutableList<CompilerFileSetPass> fileSetPasses; private final SoyTypeRegistry registry; private final ImmutableMap<String, ? extends SoyFunction> soyFunctionMap; private final ErrorReporter errorReporter; private final SyntaxVersion declaredSyntaxVersion; private final SoyGeneralOptions options; private final boolean allowUnknownGlobals; private PassManager(Builder builder) { this.registry = checkNotNull(builder.registry); this.soyFunctionMap = checkNotNull(builder.soyFunctionMap); this.errorReporter = checkNotNull(builder.errorReporter); this.declaredSyntaxVersion = checkNotNull(builder.declaredSyntaxVersion); this.options = checkNotNull(builder.opts); this.allowUnknownGlobals = builder.allowUnknownGlobals; boolean enabledStrictHtml = options.getExperimentalFeatures().contains("stricthtml"); ImmutableList.Builder<CompilerFilePass> singleFilePassesBuilder = ImmutableList.<CompilerFilePass>builder() .add(new RewriteGendersPass()) .add(new RewriteRemaindersPass()) .add(new HtmlRewritePass(options.getExperimentalFeatures(), errorReporter)) .add(new StrictHtmlValidationPass(options.getExperimentalFeatures(), errorReporter)) .add(new RewriteGlobalsPass(registry, options.getCompileTimeGlobals(), errorReporter)) .add(new RewriteFunctionsPass(registry)) .add(new SetFullCalleeNamesPass()) .add(new ResolveNamesPass()) .add(new ResolveFunctionsPass()) .add(new ResolveExpressionTypesPass()) .add(new ResolvePackageRelativeCssNamesPass()) .add(new VerifyPhnameAttrOnlyOnPlaceholdersPass()); if (!allowUnknownGlobals) { // Must come after RewriteGlobalsPass since that is when values are substituted. // We should also run after the ResolveNamesPass which checks for global/param ambiguity and // may issue better error messages. singleFilePassesBuilder.add(new CheckGlobalsPass(errorReporter)); } singleFilePassesBuilder .add(new CheckInvalidParamsPass()) .add(new ValidateAliasesPass()) .add(new CheckSyntaxVersionPass()) // Must run after ResolveExpressionTypesPass, which adds the SoyProtoType info. .add(new CheckProtoInitCallsPass(errorReporter)) .add( new CheckFunctionCallsPass( builder.allowUnknownFunctions, declaredSyntaxVersion, errorReporter)); // If requiring strict autoescaping, check and enforce it. if (options.isStrictAutoescapingRequired()) { singleFilePassesBuilder.add(new EnforceStrictAutoescapingPass()); } this.singleFilePasses = singleFilePassesBuilder.build(); // Fileset passes run on the whole tree and should be reserved for checks that need transitive // call information (or full delegate sets). // Notably, the results of these passes cannot be cached in the AST cache. So minimize their // use. ImmutableList.Builder<CompilerFileSetPass> fileSetPassBuilder = ImmutableList.<CompilerFileSetPass>builder() .add(new CheckTemplateParamsPass()) .add(new CheckTemplateCallsPass(enabledStrictHtml, errorReporter)) .add(new CheckVisibilityPass()) .add(new CheckDelegatesPass()); // If disallowing external calls, perform the check. if (Objects.equals(options.allowExternalCalls(), Boolean.FALSE)) { fileSetPassBuilder.add(new StrictDepsPass()); } // TODO(lukes): move this to run after autoescaping. fileSetPassBuilder.add(new DesugarHtmlNodesPass()); this.fileSetPasses = fileSetPassBuilder.build(); } public void runSingleFilePasses(SoyFileNode file, IdGenerator nodeIdGen) { for (CompilerFilePass pass : singleFilePasses) { pass.run(file, nodeIdGen); } } // TODO(lukes): consider changing this to create the registry here and then return some tuple // object that contains the registry, the file set and ijparams info. This would make it easier // to move ContextualAutoescaping into this file (alternatively, eliminate deprecated-contextual // autoescaping, which would make it so the autoescaper no longer modifies calls and adds // templates. public void runWholeFilesetPasses(TemplateRegistry registry, SoyFileSetNode soyTree) { for (CompilerFileSetPass pass : fileSetPasses) { pass.run(soyTree, registry); } } public static final class Builder { private SoyTypeRegistry registry; private ImmutableMap<String, ? extends SoyFunction> soyFunctionMap; private ErrorReporter errorReporter; private SyntaxVersion declaredSyntaxVersion; private SoyGeneralOptions opts; private boolean allowUnknownGlobals; private boolean allowUnknownFunctions; public Builder setErrorReporter(ErrorReporter errorReporter) { this.errorReporter = checkNotNull(errorReporter); return this; } public Builder setSoyFunctionMap(ImmutableMap<String, ? extends SoyFunction> functionMap) { this.soyFunctionMap = checkNotNull(functionMap); return this; } public Builder setTypeRegistry(SoyTypeRegistry registry) { this.registry = checkNotNull(registry); return this; } public Builder setDeclaredSyntaxVersion(SyntaxVersion declaredSyntaxVersion) { this.declaredSyntaxVersion = checkNotNull(declaredSyntaxVersion); return this; } public Builder setGeneralOptions(SoyGeneralOptions opts) { this.opts = opts; return this; } /** * Allows unknown global references. * * <p>This option is only available for backwards compatibility with legacy js only templates * and for parseinfo generation. */ public Builder allowUnknownGlobals() { this.allowUnknownGlobals = true; return this; } /** * Allows unknown functions. * * <p>This option is only available for the parseinfo generator which historically has not had * proper build dependencies and thus often references unknown functions. */ public Builder allowUnknownFunctions() { this.allowUnknownFunctions = true; return this; } public PassManager build() { return new PassManager(this); } } private final class CheckSyntaxVersionPass extends CompilerFilePass { final ReportSyntaxVersionErrors reportDeclaredVersionErrors = new ReportSyntaxVersionErrors(declaredSyntaxVersion, true, errorReporter); @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { Checkpoint checkpoint = errorReporter.checkpoint(); reportDeclaredVersionErrors.report(file); // If there were no errors against the declared syntax version, check for errors against // the inferred syntax version too. (If there were errors against the declared syntax version, // skip the inferred error checking, because it could produce duplicate errors and in any case // it's confusing for the user to have to deal with both declared and inferred errors.) if (!errorReporter.errorsSince(checkpoint)) { SyntaxVersion inferredSyntaxVersion = InferRequiredSyntaxVersion.infer(file); if (inferredSyntaxVersion.num > declaredSyntaxVersion.num) { new ReportSyntaxVersionErrors(inferredSyntaxVersion, false, errorReporter).report(file); } } } } private final class RewriteGendersPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { new RewriteGenderMsgsVisitor(nodeIdGen, errorReporter).exec(file); } } private final class RewriteRemaindersPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { new RewriteRemaindersVisitor(errorReporter).exec(file); } } private final class SetFullCalleeNamesPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { new SetFullCalleeNamesVisitor(errorReporter).exec(file); } } private final class ResolveNamesPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { new ResolveNamesVisitor(errorReporter).exec(file); } } private final class ResolveFunctionsPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { SoyTreeUtils.execOnAllV2Exprs(file, new ResolveFunctionsVisitor(soyFunctionMap)); } } private final class ResolveExpressionTypesPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { new ResolveExpressionTypesVisitor(registry, declaredSyntaxVersion, errorReporter).exec(file); } } private final class ResolvePackageRelativeCssNamesPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { new ResolvePackageRelativeCssNamesVisitor(errorReporter).exec(file); } } private final class VerifyPhnameAttrOnlyOnPlaceholdersPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { new VerifyPhnameAttrOnlyOnPlaceholdersVisitor(errorReporter).exec(file); } } private final class CheckInvalidParamsPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { new CheckInvalidParamsVisitor(errorReporter).exec(file); } } private final class EnforceStrictAutoescapingPass extends CompilerFilePass { final AssertStrictAutoescapingVisitor visitor = new AssertStrictAutoescapingVisitor(errorReporter); @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { visitor.exec(file); } } private final class CheckTemplateParamsPass extends CompilerFileSetPass { @Override public void run(SoyFileSetNode fileSet, TemplateRegistry registry) { new CheckTemplateParamsVisitor(registry, declaredSyntaxVersion, errorReporter).exec(fileSet); } } private final class CheckDelegatesPass extends CompilerFileSetPass { private final boolean enabledStrictHtml = options.getExperimentalFeatures().contains("stricthtml"); @Override public void run(SoyFileSetNode fileSet, TemplateRegistry registry) { new CheckDelegatesVisitor(registry, enabledStrictHtml, errorReporter).exec(fileSet); } } private final class CheckVisibilityPass extends CompilerFileSetPass { @Override public void run(SoyFileSetNode fileSet, TemplateRegistry registry) { // TODO(lukes): make this part of CheckCallsPass? new CheckTemplateVisibility(registry, errorReporter).exec(fileSet); } } private final class StrictDepsPass extends CompilerFileSetPass { @Override public void run(SoyFileSetNode fileSet, TemplateRegistry registry) { new StrictDepsVisitor(registry, errorReporter).exec(fileSet); } } private static final SoyErrorKind ALIAS_CONFLICTS_WITH_GLOBAL = SoyErrorKind.of("Alias ''{0}'' conflicts with a global of the same name."); private static final SoyErrorKind ALIAS_CONFLICTS_WITH_GLOBAL_PREFIX = SoyErrorKind.of("Alias ''{0}'' conflicts with namespace for global ''{1}''."); private final class ValidateAliasesPass extends CompilerFilePass { @Override public void run(SoyFileNode file, IdGenerator nodeIdGen) { for (AliasDeclaration alias : file.getAliasDeclarations()) { if (options.getCompileTimeGlobals().containsKey(alias.getAlias())) { errorReporter.report(alias.getLocation(), ALIAS_CONFLICTS_WITH_GLOBAL, alias.getAlias()); } for (String global : options.getCompileTimeGlobals().keySet()) { if (global.startsWith(alias.getAlias() + ".")) { errorReporter.report( alias.getLocation(), ALIAS_CONFLICTS_WITH_GLOBAL_PREFIX, alias.getAlias(), global); } } } } } }