com.google.template.soy.jssrc.internal.JsSrcSubject.java Source code

Java tutorial

Introduction

Here is the source code for com.google.template.soy.jssrc.internal.JsSrcSubject.java

Source

/*
 * 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;
        }
    }
}