/* * 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 static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import com.google.template.soy.SoyFileSetParserBuilder; import com.google.template.soy.basetree.SyntaxVersion; import com.google.template.soy.error.FormattingErrorReporter; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for {@link CheckTemplateParamsVisitor}. * */ @RunWith(JUnit4.class) public final class CheckTemplateParamsVisitorTest { @Test public void testMatchingSimple() { // ------ No params ------ String soyDoc = ""; String templateBody = "Hello world!"; assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty(); // ------ Only 'print' statements ------ soyDoc = "@param boo @param foo @param? goo @param moo"; templateBody = "{$boo}{$foo.goo |noAutoescape}{2 * $goo[round($moo)]}"; assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty(); // ------ Simple 'if' statement with nested 'print' statement ------ soyDoc = "@param boo Something scary.\n" + "@param? goo Something slimy.\n"; templateBody = "{if $boo.foo}\n" + " Slimy {$goo}.\n" + "{/if}\n"; assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty(); } @Test public void testMatchingWithAdvancedStmts() { // ------ 'if', 'elseif', 'else', '/if' ------ String soyDoc = "@param boo @param foo"; String templateBody = "{if $boo}\n" + " Scary.\n" + "{elseif $foo.goo}\n" + " Slimy.\n" + "{else}\n" + " {$foo.moo}\n" + "{/if}\n"; assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty(); // ------ 'switch', 'case', 'default', '/switch' ------ soyDoc = "@param boo @param foo @param moo @param too @param zoo"; templateBody = "{switch $boo}\n" + "{case $foo.goo}\n" + " Slimy.\n" + "{case 0, $moo, $too}\n" + " Soggy.\n" + "{default}\n" + " Not {$zoo}.\n" + "{/switch}\n"; assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty(); // ------ 'foreach', 'ifempty', '/foreach' ------ soyDoc = "@param moose @param? meese"; templateBody = "{foreach $moo in $moose}\n" + " Cow says {$moo}.\n" + "{ifempty}\n" + " No {$meese}.\n" + "{/foreach}\n"; assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty(); // ------ 'for', '/for' ------ soyDoc = "@param boo"; templateBody = "{for $i in range(length($boo))}\n" + " {$i + 1}: {$boo[$i]}.\n" + "{/for}\n"; assertThat(soyDocErrorsForTemplate(soyDoc, templateBody)).isEmpty(); } @Test public void testCalls() { String fileContent1 = "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/**\n" + " * @param? goo @param too @param woo @param? zoo\n" + " * @param gee\n" + // no 'mee' is okay because user may not want to list it (chrisn) " * @param maa\n" + // no 'gaa' is okay because it may be optional in 'baa.faa' " * @param transParam \n" + // okay (not required) because it's used in transitive callee " */\n" + "{template .foo}\n" + " {call .fee data=\"$goo.moo\" /}\n" + " {call .fee data=\"$too\"}\n" + " {param gee : $woo.hoo /}\n" + " {param mee}\n" + " {$zoo}\n" + " {/param}\n" + " {/call}\n" + " {call .fee data=\"all\" /}\n" + " {call baa.faa data=\"all\" /}\n" + " {call .transitive1 data=\"all\" /}\n" + "{/template}\n" + "\n" + "/** @param gee @param mee */\n" + "{template .fee}\n" + " {$gee}{$mee}\n" + "{/template}\n" + "\n" + "/** */\n" + "{template .transitive1}\n" + " {call .transitive2 data=\"all\" /}\n" + "{/template}\n" + "\n" + "/** @param transParam */\n" + "{template .transitive2}\n" + " {$transParam}\n" + "{/template}\n"; String fileContent2 = "{namespace baa autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/** @param gaa @param maa */\n" + "{template .faa}\n" + " {$gaa}{$maa}\n" + "{/template}\n"; assertThat(soyDocErrorsFor(fileContent1, fileContent2)).isEmpty(); } @Test public void testCallWithMissingParam() { String fileContent = "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/** @param a */\n" + "{template .caller}\n" + " {call .callee}\n" + " {param x: $a /}\n" + " {/call}\n" + "{/template}\n" + "\n" + "/** @param x @param y */\n" + "{template .callee}\n" + " {$x}{$y}\n" + "{/template}\n"; // This is actually not reported as an error by this visitor CheckTemplateParams doesn't check // calls assertThat(soyDocErrorsFor(fileContent)).containsExactly("Call missing required param 'y'."); } @Test public void testUndeclaredParam() { String soyDoc = "@param foo"; String templateBody = "{$boo.foo}"; ImmutableList<String> errors = soyDocErrorsForTemplate(soyDoc, templateBody); assertThat(errors).hasSize(2); assertThat(errors.get(0)).contains("Unknown data key 'boo'. Did you mean 'foo'?"); assertThat(errors.get(1)).isEqualTo("Param 'foo' unused in template body."); } @Test public void testUnusedParam() { String soyDoc = "@param boo @param? foo"; String templateBody = "{$boo.foo}"; ImmutableList<String> errors = soyDocErrorsForTemplate(soyDoc, templateBody); assertThat(errors).containsExactly("Param 'foo' unused in template body."); } @Test public void testUnusedParamInCallWithAllData() { String fileContent = "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/**\n" + " * @param moo\n" + " * @param zoo\n" + // 'zoo' is not used, even in call to .goo " */\n" + "{template .foo}\n" + " {call .goo data=\"all\" /}\n" + "{/template}\n" + "\n" + "/** @param moo */\n" + "{template .goo}\n" + " {$moo}\n" + "{/template}\n"; ImmutableList<String> errors = soyDocErrorsFor(fileContent); assertThat(errors).containsExactly("Param 'zoo' unused in template body."); } @Test public void testWithExternalCallWithAllData() { String fileContent = "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/**\n" + " * @param zoo\n" + // 'zoo' is okay because it may be used in 'goo.moo' " */\n" + "{template .foo}\n" + " {call goo.moo data=\"all\" /}\n" + "{/template}\n"; assertThat(soyDocErrorsFor(fileContent)).isEmpty(); } @Test public void testUnusedParamWithRecursiveCall() { String soyDoc = "@param boo @param foo"; String templateBody = "{call .foo data=\"all\" /}"; ImmutableList<String> errors = soyDocErrorsForTemplate(soyDoc, templateBody); assertThat(errors) .containsExactly( "Param 'boo' unused in template body.", "Param 'foo' unused in template body.") .inOrder(); } @Test public void testUnusedParamInDelegateTemplate() { String fileContent = "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/**\n" + " * @param zoo\n" + // 'zoo' may be needed in other implementations of the same delegate " */\n" + "{deltemplate MagicButton}\n" + " blah\n" + "{/deltemplate}\n"; assertThat(soyDocErrorsFor(fileContent)).isEmpty(); } @Test public void testDelegateCallVariant() { String fileContent = "" + "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/**\n" + " * @param variant\n" + " */\n" + "{template .foo}\n" + " {delcall MagicButton variant=\"$variant\" /}\n" + "{/template}\n"; assertThat(soyDocErrorsFor(fileContent)).isEmpty(); } @Test public void testOnlyCheckFilesInV2() { String fileContent0 = "{namespace boo0 autoescape=\"deprecated-noncontextual\"}\n" + "\n" + // template is tagged as v1 "{template .foo0 deprecatedV1=\"true\"}\n" + " {$goo0.moo0}\n" + "{/template}\n"; String fileContent1 = "{namespace boo1 autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/** Template 1 */\n" + "{template .foo1 deprecatedV1=\"true\"}\n" + " {$goo1}\n" + " {v1Expression('$goo1.moo1()')}\n" + // file is not all V2 syntax due to this expression "{/template}\n"; String fileContent2 = "{namespace boo2 autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/** Template 2 */\n" + "{template .foo2}\n" + " {$goo2.moo2}\n" + "{/template}\n"; ImmutableList<String> errors = soyDocErrorsFor(fileContent0, fileContent1, fileContent2); assertThat(errors).hasSize(1); assertThat(errors.get(0)).contains("Unknown data key 'goo2'."); } @Test public void testWithHeaderParams() { String fileContent = "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/** */\n" + "{template .foo}\n" + " {@param goo: string}\n" + " {@inject zoo: string}\n" + " {$goo}\n" + " {$zoo}\n" + "{/template}\n"; assertThat(soyDocErrorsFor(fileContent)).isEmpty(); fileContent = "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/** */\n" + "{template .foo}\n" + " {@param goo: string}\n" + " {@inject zoo: string}\n" + "{/template}\n"; ImmutableList<String> errors = soyDocErrorsFor(fileContent); assertThat(errors) .containsExactly( "Param 'goo' unused in template body.", "Param 'zoo' unused in template body.") .inOrder(); } private static ImmutableList<String> soyDocErrorsForTemplate(String soyDoc, String templateBody) { String testFileContent = "{namespace boo autoescape=\"deprecated-noncontextual\"}\n" + "\n" + "/** " + soyDoc + " */\n" + "{template .foo}\n" + templateBody + "\n" + "{/template}\n"; return soyDocErrorsFor(testFileContent); } private static ImmutableList<String> soyDocErrorsFor(String... soyFileContents) { FormattingErrorReporter errorReporter = new FormattingErrorReporter(); SoyFileSetParserBuilder.forFileContents(soyFileContents) .declaredSyntaxVersion(SyntaxVersion.V1_0) .errorReporter(errorReporter) .parse(); return errorReporter.getErrorMessages(); } }