Java tutorial
/* * Copyright 2016 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.jssrc.internal; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.truth.FailureStrategy; import com.google.common.truth.IterableSubject; import com.google.common.truth.StringSubject; import com.google.common.truth.Subject; import com.google.common.truth.SubjectFactory; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.ForOverride; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.template.soy.SoyFileSetParser.ParseResult; import com.google.template.soy.SoyFileSetParserBuilder; import com.google.template.soy.SoyModule; import com.google.template.soy.base.internal.UniqueNameGenerator; import com.google.template.soy.basetree.SyntaxVersion; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.ExplodingErrorReporter; import com.google.template.soy.error.FormattingErrorReporter; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.Operator; import com.google.template.soy.jssrc.SoyJsSrcOptions; import com.google.template.soy.jssrc.dsl.CodeChunk; import com.google.template.soy.jssrc.dsl.CodeChunk.RequiresCollector; import com.google.template.soy.jssrc.dsl.CodeChunk.WithValue; import com.google.template.soy.shared.SharedTestUtils; import com.google.template.soy.shared.SoyGeneralOptions; import com.google.template.soy.shared.internal.GuiceSimpleScope; import com.google.template.soy.shared.restricted.SoyFunction; 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.TemplateDelegateNode; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.types.SoyTypeRegistry; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; /** Custom Truth subject to aid testing Soy->JS codegen. */ @CheckReturnValue abstract class JsSrcSubject<T extends Subject<T, String>> extends Subject<T, String> { private static final Joiner JOINER = Joiner.on('\n'); private static final SubjectFactory<ForFile, String> TEMPLATE_FACTORY = new SubjectFactory<ForFile, String>() { @Override public ForFile getSubject(FailureStrategy fs, String that) { return new ForFile(fs, that); } }; private static final SubjectFactory<ForExprs, String> EXPR_FACTORY = new SubjectFactory<ForExprs, String>() { @Override public ForExprs getSubject(FailureStrategy fs, String that) { return new ForExprs(fs, that); } }; private SoyGeneralOptions generalOptions = new SoyGeneralOptions(); SoyJsSrcOptions jsSrcOptions = new SoyJsSrcOptions(); private SoyTypeRegistry typeRegistry = new SoyTypeRegistry(); ErrorReporter errorReporter = ExplodingErrorReporter.get(); private final List<SoyFunction> soyFunctions = new ArrayList<>(); private SyntaxVersion syntaxVersion = SyntaxVersion.V2_0; private JsSrcSubject(FailureStrategy failureStrategy, @Nullable String s) { super(failureStrategy, s); } static ForFile assertThatSoyFile(String... lines) { return assertAbout(TEMPLATE_FACTORY).that(JOINER.join(lines)); } static ForFile assertThatTemplateBody(String... lines) { String templateBody = JOINER.join(lines); return assertAbout(TEMPLATE_FACTORY).that("{namespace ns autoescape=\"deprecated-noncontextual\"}\n" + "{template .aaa}\n" + templateBody + "{/template}\n"); } /** * Allows callers to pass in an expression without param declarations, when the caller doesn't * care about the param types. (Each variable reference generates an untyped param declaration.) */ static ForExprs assertThatSoyExpr(String... lines) { return assertThatSoyExpr(expr(lines)); } static ForExprs assertThatSoyExpr(TestExpr build) { return assertAbout(EXPR_FACTORY).that(build.buildTemplateThatContainsOneExpression()); } static TestExpr expr(String... lines) { return new TestExpr(JOINER.join(lines)); } /** A utility for building an strongly typed expression. */ static final class TestExpr { private final String exprText; private final StringBuilder paramDecls = new StringBuilder(); private TestExpr(String exprText) { this.exprText = exprText; } TestExpr withParam(String param) { paramDecls.append(param).append('\n'); return this; } TestExpr withParam(String name, String type) { paramDecls.append("{@param ").append(name).append(": ").append(type).append("}\n"); return this; } private String buildTemplateThatContainsOneExpression() { String templateBody; if (paramDecls.length() == 0) { templateBody = SharedTestUtils.untypedTemplateBodyForExpression(exprText); } else { templateBody = paramDecls.toString() + "{" + exprText + "}"; } return "{namespace ns autoescape=\"deprecated-noncontextual\"}\n" + "{template .aaa}\n" + templateBody + "\n{/template}"; } } T withJsSrcOptions(SoyJsSrcOptions options) { this.jsSrcOptions = options; return typedThis(); } T addSoyFunction(SoyFunction function) { soyFunctions.add(checkNotNull(function)); return typedThis(); } T withGeneralOptions(SoyGeneralOptions generalOptions) { this.generalOptions = generalOptions; return typedThis(); } T withTypeRegistry(SoyTypeRegistry typeRegistry) { this.typeRegistry = typeRegistry; return typedThis(); } @CheckReturnValue T withDeclaredSyntaxVersion(SyntaxVersion version) { this.syntaxVersion = version; return typedThis(); } @ForOverride abstract T typedThis(); @ForOverride abstract void generateCode(); private ParseResult parse() { SoyFileSetParserBuilder builder = SoyFileSetParserBuilder.forFileContents(actual()) .allowUnboundGlobals(true).declaredSyntaxVersion(syntaxVersion).typeRegistry(typeRegistry) .options(generalOptions); for (SoyFunction soyFunction : soyFunctions) { builder.addSoyFunction(soyFunction); } ParseResult parse = builder.parse(); // genjscodevisitor depends on this having been run new ExtractMsgVariablesVisitor().exec(parse.fileSet()); return parse; } void causesErrors(String... expectedErrorMsgSubstrings) { FormattingErrorReporter formattingErrorReporter = new FormattingErrorReporter(); this.errorReporter = formattingErrorReporter; generateCode(); ImmutableList<String> errorMessages = formattingErrorReporter.getErrorMessages(); assertThat(errorMessages).hasSize(expectedErrorMsgSubstrings.length); for (int i = 0; i < expectedErrorMsgSubstrings.length; ++i) { assertThat(errorMessages.get(i)).contains(expectedErrorMsgSubstrings[i]); } } /** Asserts on the contents of a generated soy file. */ static final class ForFile extends JsSrcSubject<ForFile> { private static final Injector INJECTOR = Guice.createInjector(new SoyModule()); private String file; private SoyFileNode fileNode; private ForFile(FailureStrategy fs, String expr) { super(fs, expr); } @Override void generateCode() { ParseResult parseResult = super.parse(); try (GuiceSimpleScope.InScope inScope = JsSrcTestUtils.simulateNewApiCall(INJECTOR, jsSrcOptions)) { this.fileNode = parseResult.fileSet().getChild(0); this.file = INJECTOR.getInstance(GenJsCodeVisitor.class) .gen(parseResult.fileSet(), parseResult.registry(), errorReporter).get(0); } } StringSubject generatesCodeThat() { generateCode(); return new StringSubject(badCodeStrategy(failureStrategy, "soy file"), file); } StringSubject generatesTemplateThat() { generateCode(); if (fileNode.numChildren() != 1) { fail("expected to only have 1 template: " + fileNode.getChildren()); } TemplateNode template = fileNode.getChild(0); // we know that 'file' contains exactly one template. so find it. int functionIndex = file.indexOf("function("); int startOfFunction = file.substring(0, functionIndex).lastIndexOf('\n') + 1; int endOfFunction = file.lastIndexOf("}\n") + 2; //+2 to capture the \n // if it is a delegate function we want to include the registration code which is a single // statement after the end of the template if (template instanceof TemplateDelegateNode) { endOfFunction = file.indexOf(";\n", endOfFunction) + 2; } // if we are generating jsdoc we want to capture that too String templateBody; if (jsSrcOptions.shouldGenerateJsdoc()) { int startOfJsDoc = file.substring(0, startOfFunction).lastIndexOf("/**"); templateBody = file.substring(startOfJsDoc, endOfFunction); } else { templateBody = file.substring(startOfFunction, endOfFunction); } return new StringSubject(badCodeStrategy(failureStrategy, "template body"), templateBody); } IterableSubject generatesRequiresThat() { generateCode(); Pattern googRequire = Pattern.compile("goog.require\\('(.*)'\\);"); ImmutableSet.Builder<String> requires = ImmutableSet.builder(); Matcher matcher = googRequire.matcher(file); while (matcher.find()) { requires.add(matcher.group(1)); } // have to make an anonymous subclass to construct it since the constructor is protected, // lame. return new IterableSubject(badCodeStrategy(failureStrategy, "goog requires"), requires.build()) { }; } private FailureStrategy badCodeStrategy(final FailureStrategy delegate, final String type) { return new FailureStrategy() { private String prependMessage(String message) { return "Unexpected " + type + " generated for " + actual() + ":" + (message.isEmpty() ? "" : " " + message); } @Override public void fail(String message) { delegate.fail(prependMessage(message)); } @Override public void fail(String message, Throwable cause) { delegate.fail(prependMessage(message), cause); } @Override public void failComparing(String message, CharSequence expected, CharSequence actual) { delegate.failComparing(prependMessage(message), expected, actual); } }; } @Override ForFile typedThis() { return this; } } /** For asserting on the contents of a single soy expression. */ static final class ForExprs extends JsSrcSubject<ForExprs> { private CodeChunk.WithValue chunk; private ImmutableMap<String, WithValue> initialLocalVarTranslations = ImmutableMap.of(); private ForExprs(FailureStrategy fs, String templateThatContainsOneExpression) { super(fs, templateThatContainsOneExpression); } @Override void generateCode() { ParseResult parseResult = super.parse(); List<PrintNode> printNodes = SoyTreeUtils.getAllNodesOfType(parseResult.fileSet().getChild(0), PrintNode.class); assertThat(printNodes).hasSize(1); ExprNode exprNode = printNodes.get(0).getExpr(); UniqueNameGenerator nameGenerator = JsSrcNameGenerators.forLocalVariables(); this.chunk = new TranslateExprNodeVisitor(jsSrcOptions, TranslationContext.of(SoyToJsVariableMappings.startingWith(initialLocalVarTranslations), CodeChunk.Generator.create(nameGenerator), nameGenerator), errorReporter).exec(exprNode); } JsSrcSubject.ForExprs withInitialLocalVarTranslations( ImmutableMap<String, CodeChunk.WithValue> initialLocalVarTranslations) { this.initialLocalVarTranslations = initialLocalVarTranslations; return this; } @CanIgnoreReturnValue JsSrcSubject.ForExprs withPrecedence(Operator operator) { Preconditions.checkNotNull(this.chunk, "Call generatesCode() first."); assertThat(this.chunk.assertExprAndCollectRequires(RequiresCollector.NULL).getPrecedence()) .isEqualTo(operator.getPrecedence()); return this; } @CanIgnoreReturnValue ForExprs generatesCode(String... expectedLines) { generateCode(); String expected = Joiner.on('\n').join(expectedLines); assertThat(chunk.getExpressionTestOnly()).isEqualTo(expected); return this; } @Override ForExprs typedThis() { return this; } } }