/* * Copyright 2008 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.ArrayListMultimap; import com.google.common.collect.ListMultimap; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.basetree.Node; import com.google.template.soy.basetree.NodeVisitor; import com.google.template.soy.basetree.SyntaxVersion; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.SoyErrorKind; import com.google.template.soy.error.SoyErrors; import com.google.template.soy.exprtree.VarRefNode; import com.google.template.soy.passes.FindIndirectParamsVisitor.IndirectParamsInfo; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.SoyFileNode; import com.google.template.soy.soytree.SoyFileSetNode; import com.google.template.soy.soytree.SoyNode; import com.google.template.soy.soytree.SoyNode.ParentSoyNode; import com.google.template.soy.soytree.SoyTreeUtils; import com.google.template.soy.soytree.TemplateBasicNode; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.TemplateRegistry; import com.google.template.soy.soytree.defn.TemplateParam; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * Visitor for checking that in each template, the parameters declared in the SoyDoc match the data * keys referenced in the template. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * * <p>Precondition: All template and callee names should be full names (i.e. you must execute {@code * SetFullCalleeNamesVisitor} before executing this visitor). * * <p>Note this visitor only works for code in Soy V2 syntax. * * <p>{@link #exec} should be called on a full parse tree. There is no return value. However, errors * are reported to the {@code ErrorReporter} if the parameters declared in some template's SoyDoc do * not match the data keys referenced in that template. * */ final class CheckTemplateParamsVisitor extends AbstractSoyNodeVisitor<Void> { private static final SoyErrorKind UNDECLARED_DATA_KEY = SoyErrorKind.of("Unknown data key ''{0}''.{1}"); private static final SoyErrorKind UNUSED_PARAM = SoyErrorKind.of("Param ''{0}'' unused in template body."); /** User-declared syntax version. */ private final SyntaxVersion declaredSyntaxVersion; private final ErrorReporter errorReporter; /** Registry of all templates in the Soy tree. */ private final TemplateRegistry templateRegistry; /** @param declaredSyntaxVersion User-declared syntax version, */ CheckTemplateParamsVisitor( TemplateRegistry templateRegistry, SyntaxVersion declaredSyntaxVersion, ErrorReporter errorReporter) { this.templateRegistry = templateRegistry; this.errorReporter = errorReporter; this.declaredSyntaxVersion = declaredSyntaxVersion; } // ----------------------------------------------------------------------------------------------- // Implementations for specific nodes. @Override protected void visitSoyFileSetNode(SoyFileSetNode node) { // Run pass only on the Soy files that are all in V2 syntax. for (SoyFileNode soyFile : node.getChildren()) { // Run pass on Soy file if it is all in V2 syntax. if (declaredSyntaxVersion.num >= SyntaxVersion.V2_0.num || allNodesInferredAboveV2(soyFile)) { visit(soyFile); } } } private boolean allNodesInferredAboveV2(SoyNode node) { final AtomicBoolean allV2 = new AtomicBoolean(true); SoyTreeUtils.visitAllNodes( node, new NodeVisitor<Node, Boolean>() { @Override public Boolean exec(Node node) { if (!node.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)) { allV2.set(false); return false; } return true; } }); return allV2.get(); } @Override protected void visitTemplateNode(TemplateNode node) { ListMultimap<String, SourceLocation> dataKeys = ArrayListMultimap.create(); for (VarRefNode varRefNode : SoyTreeUtils.getAllNodesOfType(node, VarRefNode.class)) { if (varRefNode.isPossibleParam()) { dataKeys.put(varRefNode.getName(), varRefNode.getSourceLocation()); } } IndirectParamsInfo ipi = new FindIndirectParamsVisitor(templateRegistry).exec(node); Set<String> allParamNames = new HashSet<>(); List<String> unusedParams = new ArrayList<>(); for (TemplateParam param : node.getAllParams()) { allParamNames.add(param.name()); if (dataKeys.containsKey(param.name())) { // Good: Declared and referenced in template. We remove these from dataKeys so // that at the end of the for-loop, dataKeys will only contain the keys that are referenced // but not declared in SoyDoc. dataKeys.removeAll(param.name()); } else if (ipi.paramKeyToCalleesMultimap.containsKey(param.name()) || ipi.mayHaveIndirectParamsInExternalCalls || ipi.mayHaveIndirectParamsInExternalDelCalls) { // Good: Declared in SoyDoc and either (a) used in a call that passes all data or (b) used // in an external call or delcall that passes all data, which may need the param (we can't // verify). } else { // Bad: Declared in SoyDoc but not referenced in template. unusedParams.add(param.name()); } } // At this point, the only keys left in dataKeys are undeclared. for (Entry<String, SourceLocation> undeclared : dataKeys.entries()) { String extraErrorMessage = SoyErrors.getDidYouMeanMessage(allParamNames, undeclared.getKey()); errorReporter.report( undeclared.getValue(), UNDECLARED_DATA_KEY, undeclared.getKey(), extraErrorMessage); } // Delegate templates can declare unused params because other implementations // of the same delegate may need to use those params. if (node instanceof TemplateBasicNode) { for (String unusedParam : unusedParams) { errorReporter.report(node.getSourceLocation(), UNUSED_PARAM, unusedParam); } } } // ----------------------------------------------------------------------------------------------- // Fallback implementation. @Override protected void visitSoyNode(SoyNode node) { if (node instanceof ParentSoyNode<?>) { visitChildren((ParentSoyNode<?>) node); } } }