/* * Copyright 2006 The Closure Compiler Authors. * * 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.javascript.jscomp; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static com.google.javascript.jscomp.testing.JSErrorSubject.assertError; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.ForOverride; import com.google.javascript.jscomp.AbstractCompiler.MostRecentTypechecker; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; import com.google.javascript.jscomp.testing.BlackHoleErrorManager; import com.google.javascript.jscomp.type.ReverseAbstractInterpreter; import com.google.javascript.jscomp.type.SemanticReverseAbstractInterpreter; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.testing.BaseJSTypeTestCase; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import junit.framework.TestCase; /** * <p>Base class for testing JS compiler classes that change * the node tree of a compiled JS input.</p> * * <p>Pulls in shared functionality from different test cases. Also supports * node tree comparison for input and output (instead of string comparison), * which makes it easier to write tests b/c you don't have to get the syntax * exactly correct to the spacing.</p> * */ public abstract class CompilerTestCase extends TestCase { protected static final Joiner LINE_JOINER = Joiner.on('\n'); /** Externs for the test */ protected final List<SourceFile> externsInputs; /** Whether to compare input and output as trees instead of strings */ private boolean compareAsTree; /** Whether to parse type info from JSDoc comments */ protected boolean parseTypeInfo; /** Whether to take JSDoc into account when comparing ASTs. */ protected boolean compareJsDoc; /** Whether we check warnings without source information. */ private boolean allowSourcelessWarnings = false; /** True iff closure pass runs before pass being tested. */ private boolean closurePassEnabled = false; /** Whether the closure pass is run on the expected JS. */ private boolean closurePassEnabledForExpected = false; /** Whether to rewrite Closure code before the test is run. */ private boolean rewriteClosureCode = false; /** * If true, run type checking together with the pass being tested. A separate * flag controls whether type checking runs before or after the pass. */ private boolean typeCheckEnabled = false; /** * If true, run NTI together with the pass being tested. A separate * flag controls whether NTI runs before or after the pass. */ private boolean newTypeInferenceEnabled = false; /** Whether to test the compiler pass before the type check. */ protected boolean runTypeCheckAfterProcessing = false; /** Whether to test the compiler pass before NTI. */ protected boolean runNTIAfterProcessing = false; /** Whether to scan externs for property names. */ private boolean gatherExternPropertiesEnabled = false; /** * Whether the Normalize pass runs before pass being tested and * whether the expected JS strings should be normalized. */ private boolean normalizeEnabled = false; private boolean polymerPass = false; /** Whether the tranpilation passes runs before pass being tested. */ private boolean transpileEnabled = false; /** Whether the expected JS strings should be transpiled. */ private boolean transpileExpected = false; /** Whether we run InferConsts before checking. */ private boolean enableInferConsts = false; /** Whether we run CheckAccessControls after the pass being tested. */ private boolean checkAccessControls = false; /** Whether to check that all line number information is preserved. */ private boolean checkLineNumbers = true; /** Whether to check that changed scopes are marked as changed */ private boolean checkAstChangeMarking = true; /** Whether we expect parse warnings in the current test. */ private boolean expectParseWarningsThisTest = false; /** * An expected symbol table error. Only useful for testing the * symbol table error-handling. */ private DiagnosticType expectedSymbolTableError = null; /** * Whether the MarkNoSideEffectsCalls pass runs before the pass being tested */ private boolean markNoSideEffects = false; /** * Whether the PureFunctionIdentifier pass runs before the pass being tested */ private boolean computeSideEffects = false; /** The most recently used Compiler instance. */ private Compiler lastCompiler; /** * Whether to accept ES6, ES5 or ES3 source. */ private LanguageMode acceptedLanguage = LanguageMode.ECMASCRIPT5; private LanguageMode languageOut = LanguageMode.ECMASCRIPT5; private boolean emitUseStrict = false; /** * Whether externs changes should be allowed for this pass. */ private boolean allowExternsChanges = false; /** * Whether the AST should be validated. */ private boolean astValidationEnabled = true; private String filename = "testcode"; private final Set<DiagnosticType> ignoredWarnings = new HashSet<>(); static final String ACTIVE_X_OBJECT_DEF = LINE_JOINER.join( "/**", " * @param {string} progId", " * @param {string=} opt_location", " * @constructor", " * @see http://msdn.microsoft.com/en-us/library/7sw4ddf8.aspx", " */", "function ActiveXObject(progId, opt_location) {}"); /** A minimal set of externs, consisting of only those needed for NTI not to blow up. */ static final String MINIMAL_EXTERNS = LINE_JOINER.join( "/**", " * @constructor", " * @param {*=} opt_value", " * @return {!Object}", " */", "function Object(opt_value) {}", "/**", " * @constructor", " * @param {...*} var_args", " */", "function Function(var_args) {}", "/**", " * @constructor", " * @param {*=} arg", " * @return {string}", " */", "function String(arg) {}", "/**", " * @interface", " * @template VALUE", " */", "function Iterable() {}", "/**", " * @interface", " * @template KEY1, VALUE1", " */", "function IObject() {};", "/**", " * @record", " * @extends IObject<number, VALUE2>", " * @template VALUE2", " */", "function IArrayLike() {};", "/**", " * @template T", " * @constructor ", " * @implements {IArrayLike<T>} ", " * @implements {Iterable<T>}", " * @param {...*} var_args", " * @return {!Array.<?>}", " */", "function Array(var_args) {}"); /** A default set of externs for testing. */ public static final String DEFAULT_EXTERNS = LINE_JOINER.join( MINIMAL_EXTERNS, "/**", " * @type{number}", " */", "IArrayLike.prototype.length;", "/** @return {string} */", "Object.prototype.toString = function() {};", "/**", " * @param {*} propertyName", " * @return {boolean}", " */", "Object.prototype.hasOwnProperty = function(propertyName) {};", "/** @type {?Function} */ Object.prototype.constructor;", "Object.defineProperties = function(obj, descriptors) {};", "/** @type {!Function} */ Function.prototype.apply;", "/** @type {!Function} */ Function.prototype.bind;", "/** @type {!Function} */ Function.prototype.call;", "/** @type {number} */", "Function.prototype.length;", "/** @type {string} */", "Function.prototype.name;", "/** @param {number} sliceArg */", "String.prototype.slice = function(sliceArg) {};", "/**", " * @this {?String|string}", " * @param {?} regex", " * @param {?} str", " * @param {string=} opt_flags", " * @return {string}", " */", "String.prototype.replace = function(regex, str, opt_flags) {};", "/** @type {number} */ String.prototype.length;", "/**", " * @constructor", " * @param {*=} arg", " * @return {number}", " */", "function Number(arg) {}", "/**", " * @constructor", " * @param {*=} arg", " * @return {boolean}", " */", "function Boolean(arg) {}", "/** @type {number} */ Array.prototype.length;", "/**", " * @param {...T} var_args", " * @return {number} The new length of the array.", " * @this {{length: number}|!Array.<T>}", " * @template T", " * @modifies {this}", " */", "Array.prototype.push = function(var_args) {};", "/**", " * @this {IArrayLike<T>}", " * @return {T}", " * @template T", " */", "Array.prototype.shift = function() {};", "/**", " * @param {?function(this:S, T, number, !Array<T>): ?} callback", " * @param {S=} opt_thisobj", " * @this {?IArrayLike<T>|string}", " * @template T,S", " * @return {undefined}", " */", "Array.prototype.forEach = function(callback, opt_thisobj) {};", "/**", " * @param {?function(this:S, T, number, !Array<T>): ?} callback", " * @param {S=} opt_thisobj", " * @return {!Array<T>}", " * @this {?IArrayLike<T>|string}", " * @template T,S", " */", "Array.prototype.filter = function(callback, opt_thisobj) {};", "/**", " * @constructor", " * @template T", " * @implements {IArrayLike<T>}", " */", "function Arguments() {}", "/** @type {number} */", "Arguments.prototype.length;", "/**", " * @constructor", " * @param {*=} opt_pattern", " * @param {*=} opt_flags", " * @return {!RegExp}", " * @nosideeffects", " */", "function RegExp(opt_pattern, opt_flags) {}", "/**", " * @param {*} str The string to search.", " * @return {?Array<string>}", " */", "RegExp.prototype.exec = function(str) {};", "/**", " * @constructor", " */", // TODO(bradfordcsmith): Copy fields for this from es5.js this when we have test cases // that depend on them. "function ObjectPropertyDescriptor() {}", "/**", " * @param {!Object} obj", " * @param {string} prop", " * @return {!ObjectPropertyDescriptor|undefined}", " * @nosideeffects", " */", "Object.getOwnPropertyDescriptor = function(obj, prop) {};", "/**", " * @param {!Object} obj", " * @param {string} prop", " * @param {!Object} descriptor", " * @return {!Object}", " */", "Object.defineProperty = function(obj, prop, descriptor) {};", "/** @type {?} */ var unknown;", // For producing unknowns in tests. "/** @typedef {?} */ var symbol;", // TODO(sdh): remove once primitive 'symbol' supported "/** @constructor */ function Symbol() {}", "/** @const {!symbol} */ Symbol.iterator;", ACTIVE_X_OBJECT_DEF); /** * Constructs a test. * * @param externs Externs JS as a string * @param compareAsTree True to compare output & expected as a node tree. * 99% of the time you want to compare as a tree. There are a few * special cases where you don't, like if you want to test the code * printing of "unnatural" syntax trees. For example, * * <pre> * IF * IF * STATEMENT * ELSE * STATEMENT * </pre> */ protected CompilerTestCase(String externs, boolean compareAsTree) { this.externsInputs = ImmutableList.of(SourceFile.fromCode("externs", externs)); this.compareAsTree = compareAsTree; this.parseTypeInfo = false; this.compareJsDoc = true; } /** * Constructs a test. Uses AST comparison. * @param externs Externs JS as a string */ protected CompilerTestCase(String externs) { this(externs, true); } /** * Constructs a test. Uses AST comparison and no externs. */ protected CompilerTestCase() { this("", true); } @Override protected void tearDown() throws Exception { super.tearDown(); expectParseWarningsThisTest = false; } /** * Gets the compiler pass instance to use for a test. * * @param compiler The compiler * @return The pass to test */ protected abstract CompilerPass getProcessor(Compiler compiler); /** * Gets the compiler options to use for this test. Use getProcessor to * determine what passes should be run. */ protected CompilerOptions getOptions() { return getOptions(new CompilerOptions()); } /** * Gets the compiler options to use for this test. Use getProcessor to * determine what passes should be run. */ protected CompilerOptions getOptions(CompilerOptions options) { options.setLanguageIn(acceptedLanguage); options.setEmitUseStrict(emitUseStrict); options.setLanguageOut(languageOut); // This doesn't affect whether checkSymbols is run--it just affects // whether variable warnings are filtered. options.setCheckSymbols(true); options.setWarningLevel(DiagnosticGroups.MISSING_PROPERTIES, CheckLevel.WARNING); options.setWarningLevel(DiagnosticGroups.INVALID_CASTS, CheckLevel.WARNING); if (!ignoredWarnings.isEmpty()) { options.setWarningLevel( new DiagnosticGroup(ignoredWarnings.toArray(new DiagnosticType[0])), CheckLevel.OFF); } options.setCodingConvention(getCodingConvention()); options.setPolymerVersion(1); return options; } @ForOverride protected CodingConvention getCodingConvention() { return new GoogleCodingConvention(); } public void setFilename(String filename) { this.filename = filename; } public String getFilename() { return filename; } /** * Returns the number of times the pass should be run before results are * verified. */ @ForOverride protected int getNumRepetitions() { // Since most compiler passes should be idempotent, we run each pass twice // by default. return 2; } /** Adds the given DiagnosticTypes to the set of warnings to ignore. */ protected final void ignoreWarnings(DiagnosticType... warnings) { ignoredWarnings.addAll(Arrays.asList(warnings)); } /** Adds the given DiagnosticGroups to the set of warnings to ignore. */ protected final void ignoreWarnings(DiagnosticGroup... warnings) { for (DiagnosticGroup group : warnings) { ignoredWarnings.addAll(group.getTypes()); } } /** Expect warnings without source information. */ void allowSourcelessWarnings() { allowSourcelessWarnings = true; } /** The most recently used JSComp instance. */ Compiler getLastCompiler() { return lastCompiler; } /** * What language to allow in source parsing. Also sets the output language. */ protected void setAcceptedLanguage(LanguageMode lang) { setLanguage(lang, lang); } /** * Sets the input and output language modes.. */ protected void setLanguage(LanguageMode langIn, LanguageMode langOut) { this.acceptedLanguage = langIn; setLanguageOut(langOut); } protected void setLanguageOut(LanguageMode acceptedLanguage) { this.languageOut = acceptedLanguage; } protected void setEmitUseStrict(boolean emitUseStrict) { this.emitUseStrict = emitUseStrict; } /** * Whether to run InferConsts before passes */ protected void enableInferConsts(boolean enable) { this.enableInferConsts = enable; } /** * Whether to run CheckAccessControls after the pass being tested (and checking types). */ protected void enableCheckAccessControls(boolean enable) { this.checkAccessControls = enable; } /** * Whether to allow externs changes. */ protected void allowExternsChanges(boolean allowExternsChanges) { this.allowExternsChanges = allowExternsChanges; } public void enablePolymerPass() { polymerPass = true; } /** * Perform type checking before running the test pass. This will check * for type errors and annotate nodes with type information. * * @see TypeCheck */ public void enableTypeCheck() { typeCheckEnabled = true; } // Run the new type inference after the test pass. Useful for testing passes // that rewrite the AST prior to typechecking, eg, AngularPass or PolymerPass. public void enableNewTypeInference() { this.newTypeInferenceEnabled = true; } /** * Check to make sure that line numbers were preserved. */ public void enableLineNumberCheck(boolean newVal) { checkLineNumbers = newVal; } /** * @param newVal Whether to validate AST change marking. */ public void validateAstChangeMarking(boolean newVal) { checkAstChangeMarking = newVal; } /** * Do not run type checking before running the test pass. * * @see TypeCheck */ public void disableTypeCheck() { typeCheckEnabled = false; } public void disableNewTypeInference() { this.newTypeInferenceEnabled = false; } /** * Process closure library primitives. */ protected void enableClosurePass() { closurePassEnabled = true; } protected void enableClosurePassForExpected() { closurePassEnabledForExpected = true; } /** * Rewrite Closure code before the test is run. */ void enableRewriteClosureCode() { rewriteClosureCode = true; } /** * Don't rewrite Closure code before the test is run. */ void disableRewriteClosureCode() { rewriteClosureCode = false; } /** * Perform AST normalization before running the test pass, and anti-normalize * after running it. * * @see Normalize */ protected void enableNormalize() { this.normalizeEnabled = true; } /** * Perform AST transpilation before running the test pass. */ protected void enableTranspile() { enableTranspile(true); } /** * Perform AST transpilation before running the test pass. * * @param transpileExpected Whether to perform transpilation on the * expected JS result. */ protected void enableTranspile(boolean transpileExpected) { transpileEnabled = true; this.transpileExpected = transpileExpected; } /** * Don't perform AST normalization before running the test pass. * @see Normalize */ protected void disableNormalize() { normalizeEnabled = false; } /** * Run the MarkSideEffectCalls pass before running the test pass. * * @see MarkNoSideEffectCalls */ void enableMarkNoSideEffects() { markNoSideEffects = true; } /** * Run the PureFunctionIdentifier pass before running the test pass. * * @see MarkNoSideEffectCalls */ void enableComputeSideEffects() { computeSideEffects = true; } /** * Scan externs for properties that should not be renamed. */ void enableGatherExternProperties() { gatherExternPropertiesEnabled = true; } /** * Whether to allow Validate the AST after each run of the pass. */ protected void enableAstValidation(boolean validate) { astValidationEnabled = validate; } /** * Whether to compare the expected output as a tree or string. */ protected void enableCompareAsTree(boolean compareAsTree) { this.compareAsTree = compareAsTree; } /** Whether we should ignore parse warnings for the current test method. */ protected void setExpectParseWarningsThisTest() { expectParseWarningsThisTest = true; } /** Returns a newly created TypeCheck. */ private static TypeCheck createTypeCheck(Compiler compiler) { ReverseAbstractInterpreter rai = new SemanticReverseAbstractInterpreter(compiler.getTypeRegistry()); compiler.setMostRecentTypechecker(MostRecentTypechecker.OTI); return new TypeCheck(compiler, rai, compiler.getTypeRegistry()); } static void runNewTypeInference(Compiler compiler, Node externs, Node js) { GlobalTypeInfo gti = compiler.getSymbolTable(); gti.process(externs, js); NewTypeInference nti = new NewTypeInference(compiler); nti.process(externs, js); } /** * Verifies that the compiler pass's JS output matches the expected output. * * @param js Input * @param expected Expected JS output */ public void test(String js, String expected) { test(js, expected, null, null); } /** * Verifies that the compiler generates the given error for the given input. * * @param js Input * @param error Expected error */ public void testError(String js, DiagnosticType error) { assertNotNull(error); test(js, null, error, null); } /** * Verifies that the compiler generates the given error and description for the given input. */ public void testError(String js, DiagnosticType error, String description) { assertNotNull(error); test(js, null, error, null, description); } /** * Verifies that the compiler generates the given error for the given input. * * @param js Input * @param error Expected error */ public void testError(String[] js, DiagnosticType error) { assertNotNull(error); test(js, null, error, null); } /** * Verifies that the compiler generates the given warning for the given input. * * @param js Input * @param warning Expected warning */ public void testWarning(String js, DiagnosticType warning) { assertNotNull(warning); test(js, null, null, warning); } /** * Verifies that the compiler generates the given warning for the given input. */ public void testWarning(String[] js, DiagnosticType warning) { assertNotNull(warning); test(js, null, null, warning); } /** * Verifies that the compiler generates the given warning and description for the given input. * * @param js Input * @param warning Expected warning */ public void testWarning(String js, DiagnosticType warning, String description) { assertNotNull(warning); test(js, null, null, warning, description); } /** * Verifies that the compiler generates the given warning for the given input. */ public void testWarning(String externs, String js, DiagnosticType warning, String description) { assertNotNull(warning); test(externs, js, null, null, warning, description); } /** * Verifies that the compiler generates no warnings for the given input. * * @param js Input */ public void testNoWarning(String js) { test(js, null, null, null); } /** * Verifies that the compiler generates no warnings for the given input. */ public void testNoWarning(String[] js) { test(js, null, null, null); } /** * Verifies that the compiler pass's JS output matches the expected output, * or that an expected error is encountered. * * @param js Input * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected * @param description The content of the error expected */ public void test( String js, String expected, DiagnosticType error, DiagnosticType warning, String description) { test(externsInputs, js, expected, error, warning, description); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param js Input * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected */ public void test(String js, String expected, DiagnosticType error, DiagnosticType warning) { test(externsInputs, js, expected, error, warning, null); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param externs Externs input * @param js Input * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected */ public void test( String externs, String js, String expected, DiagnosticType error, DiagnosticType warning) { test(externs, js, expected, error, warning, null); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param externs Externs input * @param js Input * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should not be examined */ public void test( String externs, String js, String expected, DiagnosticType error, DiagnosticType warning, String description) { SourceFile externsFile = SourceFile.fromCode("externs", externs); externsFile.setIsExtern(true); List<SourceFile> externsInputs = ImmutableList.of(externsFile); test(externsInputs, js, expected, error, warning, description); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param externs Externs inputs * @param js Input * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should not be examined */ public void test( List<SourceFile> externs, String js, String expected, DiagnosticType error, DiagnosticType warning, String description) { test( externs, ImmutableList.of(SourceFile.fromCode(filename, js)), expected, error, warning, description); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param externs Externs inputs * @param js Inputs * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should not be examined */ private void test( List<SourceFile> externs, List<SourceFile> js, String expected, DiagnosticType error, DiagnosticType warning, String description) { Compiler compiler = createCompiler(); lastCompiler = compiler; CompilerOptions options = getOptions(); options.setCheckTypes(parseTypeInfo || this.typeCheckEnabled); compiler.init(externs, js, options); if (this.typeCheckEnabled) { BaseJSTypeTestCase.addNativeProperties(compiler.getTypeRegistry()); } test(compiler, maybeCreateArray(expected), error, warning, description); } private static String[] maybeCreateArray(String expected) { if (expected != null) { return new String[] {expected}; } return null; } /** * Verifies that the compiler pass's JS output matches the expected output. * * @param js Inputs * @param expected Expected JS output */ public void test(String[] js, String[] expected) { test(js, expected, null); } /** * Verifies that the compiler pass's JS output matches the expected output. * * @param js Inputs * @param expected Expected JS output */ public void test(List<SourceFile> js, List<SourceFile> expected) { test(js, expected, null); } /** * Verifies that the compiler pass's JS output matches the expected output, or that an expected * error is encountered. * * @param js Inputs * @param expected Expected JS output * @param error Expected error, or null if no error is expected */ private void test(String[] js, String[] expected, DiagnosticType error) { test(js, expected, error, null); } /** * Verifies that the compiler pass's JS output matches the expected output, * or that an expected error is encountered. * * @param js Inputs * @param expected Expected JS output * @param error Expected error, or null if no error is expected */ public void test(List<SourceFile> js, List<SourceFile> expected, DiagnosticType error) { test(js, expected, error, null); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param js Inputs * @param expected Expected JS output * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected */ public void test(String[] js, String[] expected, DiagnosticType error, DiagnosticType warning) { test(js, expected, error, warning, null); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param js Inputs * @param expected Expected JS output * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected */ public void test( List<SourceFile> js, List<SourceFile> expected, DiagnosticType error, DiagnosticType warning) { test(js, expected, error, warning, null); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param js Inputs * @param expected Expected JS output * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should not be examined */ public void test( String[] js, String[] expected, DiagnosticType error, DiagnosticType warning, String description) { List<SourceFile> inputs = new ArrayList<>(); for (int i = 0; i < js.length; i++) { inputs.add(SourceFile.fromCode("input" + i, js[i])); } test(inputs, expected, error, warning, description); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param js Inputs * @param expected Expected JS output * @param error Expected error, or null if no error is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should not be examined */ public void test( List<SourceFile> js, List<SourceFile> expected, DiagnosticType error, DiagnosticType warning, String description) { Compiler compiler = createCompiler(); lastCompiler = compiler; compiler.init(externsInputs, js, getOptions()); test(compiler, expected, error, warning, description); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionall) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param inputs Inputs * @param expected Expected JS output * @param error Expected error, or null if no error is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should no be examined */ public void test( List<SourceFile> inputs, String[] expected, DiagnosticType error, DiagnosticType warning, String description) { Compiler compiler = createCompiler(); lastCompiler = compiler; compiler.init(externsInputs, inputs, getOptions()); test(compiler, expected, error, warning, description); } /** * Verifies that the compiler pass's JS output matches the expected output. * * @param modules Module inputs * @param expected Expected JS outputs (one per module) */ public void test(JSModule[] modules, String[] expected) { test(modules, expected, null); } /** * Verifies that the compiler pass's JS output matches the expected output, * or that an expected error is encountered. * * @param modules Module inputs * @param expected Expected JS outputs (one per module) * @param error Expected error, or null if no error is expected */ public void test(JSModule[] modules, String[] expected, DiagnosticType error) { test(modules, expected, error, null); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param modules Module inputs * @param expected Expected JS outputs (one per module) * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected */ public void test( JSModule[] modules, String[] expected, DiagnosticType error, DiagnosticType warning) { Compiler compiler = createCompiler(); lastCompiler = compiler; compiler.initModules(externsInputs, ImmutableList.copyOf(modules), getOptions()); test(compiler, expected, error, warning); } /** * Verifies that the compiler pass's JS output is the same as its input. * * @param js Input and output */ public void testSame(String js) { test(js, js); } /** * Verifies that the compiler pass's JS output is the same as its input * and (optionally) that an expected warning is issued. * * @param js Input and output * @param warning Expected warning, or null if no warning is expected */ public void testSame(String js, DiagnosticType warning) { test(js, js, null, warning); } /** * Verifies that the compiler pass's JS output is the same as its input * and (optionally) that an expected warning is issued. * * @param externs Externs input * @param js Input and output * @param warning Expected warning, or null if no warning is expected */ public void testSame(String externs, String js, DiagnosticType warning) { testSame(externs, js, warning, null); } /** * Verifies that the compiler pass's JS output is the same as its input * and (optionally) that an expected warning is issued. * * @param externs Externs input * @param js Input and output * @param diag Expected error or warning, or null if none is expected * @param error true if diag is an error, false if it is a warning */ public void testSame(String externs, String js, DiagnosticType diag, boolean error) { if (error) { test(externs, js, (String) null, diag, null); } else { test(externs, js, js, null, diag); } } /** * Verifies that the compiler pass's JS output is the same as its input * and (optionally) that an expected warning and description is issued. * * @param externs Externs input * @param js Input and output * @param warning Expected warning, or null if no warning is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should not be examined */ public void testSame(String externs, String js, DiagnosticType warning, String description) { testSame(externs, js, warning, description, false); } /** * Verifies that the compiler pass's JS output is the same as its input * and (optionally) that an expected warning and description is issued. * * @param externs Externs input * @param js Input and output * @param type Expected warning or error, or null if no warning is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should not be examined * @param error Whether the "type" parameter represents an error. * (false indicated the type is a warning). Ignored if type is null. */ private void testSame( String externs, String js, DiagnosticType type, String description, boolean error) { List<SourceFile> externsInputs = ImmutableList.of(SourceFile.fromCode("externs", externs)); if (error) { test(externsInputs, js, null, type, null, description); } else { test(externsInputs, js, js, null, type, description); } } /** * Verifies that the compiler pass's JS output is the same as its input. * * @param js Inputs and outputs */ public void testSame(String[] js) { test(js, js); } /** * Verifies that the compiler pass's JS output is the same as its input. * * @param js Inputs and outputs */ public void testSame(List<SourceFile> js) { test(js, js); } /** * Verifies that the compiler pass's JS output is the same as its input, * and emits the given warning. * * @param js Inputs and outputs * @param warning Expected warning, or null if no warning is expected */ public void testSameWarning(String[] js, DiagnosticType warning) { test(js, js, null, warning); } /** * Verifies that the compiler pass's JS output is the same as the input. * * @param modules Module inputs */ public void testSame(JSModule[] modules) { testSame(modules, null); } /** * Verifies that the compiler pass's JS output is the same as the input. * * @param modules Module inputs * @param warning A warning, or null for no expected warning. */ public void testSame(JSModule[] modules, DiagnosticType warning) { try { String[] expected = new String[modules.length]; for (int i = 0; i < modules.length; i++) { expected[i] = ""; for (CompilerInput input : modules[i].getInputs()) { expected[i] += input.getSourceFile().getCode(); } } test(modules, expected, null, warning); } catch (IOException e) { throw new RuntimeException(e); } } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param compiler A compiler that has been initialized via * {@link Compiler#init} * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected */ protected void test( Compiler compiler, String[] expected, DiagnosticType error, DiagnosticType warning) { test(compiler, expected, error, warning, null); } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verified that the error is encountered. * * @param compiler A compiler that has been initialized via * {@link Compiler#init} * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected */ private void test( Compiler compiler, String[] expected, DiagnosticType error, DiagnosticType warning, String description) { if (expected == null) { test(compiler, (List<SourceFile>) null, error, warning, description); } else { List<SourceFile> inputs = new ArrayList<>(); for (int i = 0; i < expected.length; i++) { inputs.add(SourceFile.fromCode("expected" + i, expected[i])); } test(compiler, inputs, error, warning, description); } } /** * Verifies that the compiler pass's JS output matches the expected output * and (optionally) that an expected warning is issued. Or, if an error is * expected, this method just verifies that the error is encountered. * * @param compiler A compiler that has been initialized via * {@link Compiler#init} * @param expected Expected output, or null if an error is expected * @param error Expected error, or null if no error is expected * @param warning Expected warning, or null if no warning is expected * @param description The description of the expected warning, * or null if no warning is expected or if the warning's description * should not be examined */ private void test( Compiler compiler, List<SourceFile> expected, DiagnosticType error, DiagnosticType warning, String description) { Preconditions.checkState(!this.typeCheckEnabled || !this.newTypeInferenceEnabled); RecentChange recentChange = new RecentChange(); compiler.addChangeHandler(recentChange); compiler.getOptions().setNewTypeInference(this.newTypeInferenceEnabled); Node root = compiler.parseInputs(); String errorMsg = LINE_JOINER.join(compiler.getErrors()); if (root == null && expected == null && error != null) { // Might be an expected parse error. assertWithMessage("Expected one parse error, but got " + errorMsg) .that(compiler.getErrorCount()) .isEqualTo(1); JSError actualError = compiler.getErrors()[0]; assertWithMessage("Unexpected parse error(s): " + errorMsg) .that(actualError.getType()) .isEqualTo(error); if (description != null) { assertThat(actualError.description).isEqualTo(description); } return; } assertWithMessage("Unexpected parse error(s): " + errorMsg).that(root).isNotNull(); if (!expectParseWarningsThisTest) { assertWithMessage("Unexpected parser warning(s)").that(compiler.getWarnings()).isEmpty(); } else { assertThat(compiler.getWarningCount()).isGreaterThan(0); } if (astValidationEnabled) { (new AstValidator(compiler)).validateRoot(root); } Node externsRoot = root.getFirstChild(); Node mainRoot = root.getLastChild(); // Save the tree for later comparison. Node rootClone = root.cloneTree(); Node externsRootClone = rootClone.getFirstChild(); Node mainRootClone = rootClone.getLastChild(); int numRepetitions = getNumRepetitions(); ErrorManager[] errorManagers = new ErrorManager[numRepetitions]; int aggregateWarningCount = 0; List<JSError> aggregateWarnings = new ArrayList<>(); boolean hasCodeChanged = false; for (int i = 0; i < numRepetitions; ++i) { if (compiler.getErrorCount() == 0) { errorManagers[i] = new BlackHoleErrorManager(); compiler.setErrorManager(errorManagers[i]); if (polymerPass && i == 0) { recentChange.reset(); new PolymerPass(compiler).process(externsRoot, mainRoot); hasCodeChanged = hasCodeChanged || recentChange.hasCodeChanged(); } if (rewriteClosureCode && i == 0) { new ClosureRewriteClass(compiler).process(null, mainRoot); new ClosureRewriteModule(compiler, null, null).process(null, mainRoot); new ScopedAliases(compiler, null, CompilerOptions.NULL_ALIAS_TRANSFORMATION_HANDLER) .process(null, mainRoot); hasCodeChanged = hasCodeChanged || recentChange.hasCodeChanged(); } // Only run process closure primitives once, if asked. if (closurePassEnabled && i == 0) { recentChange.reset(); new ProcessClosurePrimitives(compiler, null, CheckLevel.ERROR, false) .process(externsRoot, mainRoot); hasCodeChanged = hasCodeChanged || recentChange.hasCodeChanged(); } if (transpileEnabled && i == 0) { recentChange.reset(); transpileToEs5(compiler, externsRoot, mainRoot); hasCodeChanged = hasCodeChanged || recentChange.hasCodeChanged(); } // Only run the type checking pass once, if asked. // Running it twice can cause unpredictable behavior because duplicate // objects for the same type are created, and the type system // uses reference equality to compare many types. if (!runTypeCheckAfterProcessing && typeCheckEnabled && i == 0) { TypeCheck check = createTypeCheck(compiler); check.processForTesting(externsRoot, mainRoot); } else if (!this.runNTIAfterProcessing && this.newTypeInferenceEnabled && i == 0) { runNewTypeInference(compiler, externsRoot, mainRoot); } // Only run the normalize pass once, if asked. if (normalizeEnabled && i == 0) { normalizeActualCode(compiler, externsRoot, mainRoot); } if (enableInferConsts && i == 0) { new InferConsts(compiler).process(externsRoot, mainRoot); } if (computeSideEffects && i == 0) { recentChange.reset(); PureFunctionIdentifier.Driver mark = new PureFunctionIdentifier.Driver(compiler, null); mark.process(externsRoot, mainRoot); hasCodeChanged = hasCodeChanged || recentChange.hasCodeChanged(); } if (markNoSideEffects && i == 0) { MarkNoSideEffectCalls mark = new MarkNoSideEffectCalls(compiler); mark.process(externsRoot, mainRoot); } if (gatherExternPropertiesEnabled && i == 0) { (new GatherExternProperties(compiler)).process(externsRoot, mainRoot); } recentChange.reset(); ChangeVerifier changeVerifier = null; if (checkAstChangeMarking) { changeVerifier = new ChangeVerifier(compiler); changeVerifier.snapshot(mainRoot); } getProcessor(compiler).process(externsRoot, mainRoot); if (checkAstChangeMarking) { // TODO(johnlenz): add support for multiple passes in getProcessor so that we can // check the AST marking after each pass runs. // Verify that changes to the AST are properly marked on the AST. changeVerifier.checkRecordedChanges(mainRoot); } if (astValidationEnabled) { (new AstValidator(compiler)).validateRoot(root); } if (checkLineNumbers) { (new LineNumberCheck(compiler)).process(externsRoot, mainRoot); } if (runTypeCheckAfterProcessing && typeCheckEnabled && i == 0) { TypeCheck check = createTypeCheck(compiler); check.processForTesting(externsRoot, mainRoot); } else if (this.runNTIAfterProcessing && this.newTypeInferenceEnabled && i == 0) { runNewTypeInference(compiler, externsRoot, mainRoot); } if (checkAccessControls) { (new CheckAccessControls(compiler, false)).process(externsRoot, mainRoot); } hasCodeChanged = hasCodeChanged || recentChange.hasCodeChanged(); aggregateWarningCount += errorManagers[i].getWarningCount(); Collections.addAll(aggregateWarnings, compiler.getWarnings()); if (normalizeEnabled) { boolean verifyDeclaredConstants = true; new Normalize.VerifyConstants(compiler, verifyDeclaredConstants) .process(externsRoot, mainRoot); } } } if (error == null) { assertThat(compiler.getErrors()).isEmpty(); // Verify the symbol table. ErrorManager symbolTableErrorManager = new BlackHoleErrorManager(); compiler.setErrorManager(symbolTableErrorManager); Node expectedRoot = null; if (expected != null) { expectedRoot = parseExpectedJs(expected); expectedRoot.detach(); } JSError[] stErrors = symbolTableErrorManager.getErrors(); if (expectedSymbolTableError != null) { assertEquals("There should be one error.", 1, stErrors.length); assertError(stErrors[0]).hasType(expectedSymbolTableError); } else { assertThat(stErrors).named("symbol table errors").isEmpty(); } if (warning == null) { assertThat(aggregateWarnings).named("aggregate warnings").isEmpty(); } else { assertEquals( "There should be one warning, repeated " + numRepetitions + " time(s). Warnings: \n" + LINE_JOINER.join(aggregateWarnings), numRepetitions, aggregateWarningCount); for (int i = 0; i < numRepetitions; ++i) { JSError[] warnings = errorManagers[i].getWarnings(); JSError actual = warnings[0]; assertError(actual).hasType(warning); validateSourceLocation(actual); if (description != null) { assertThat(actual.description).isEqualTo(description); } } } // If we ran normalize on the AST, we must also run normalize on the // clone before checking for changes. if (normalizeEnabled) { normalizeActualCode(compiler, externsRootClone, mainRootClone); } boolean codeChange = !mainRootClone.isEquivalentWithSideEffectsTo(mainRoot); boolean externsChange = !externsRootClone.isEquivalentWithSideEffectsTo(externsRoot); // Generally, externs should not be changed by the compiler passes. if (externsChange && !allowExternsChanges) { String explanation = externsRootClone.checkTreeEquals(externsRoot); fail( "Unexpected changes to externs" + "\nExpected: " + compiler.toSource(externsRootClone) + "\nResult: " + compiler.toSource(externsRoot) + "\n" + explanation); } if (!codeChange && !externsChange) { assertFalse( "compiler.reportCodeChange() was called " + "even though nothing changed", hasCodeChanged); } else { assertTrue( "compiler.reportCodeChange() should have been called." + "\nOriginal: " + mainRootClone.toStringTree() + "\nNew: " + mainRoot.toStringTree(), hasCodeChanged); } if (expected != null) { if (compareAsTree) { String explanation; if (compareJsDoc) { explanation = expectedRoot.checkTreeEqualsIncludingJsDoc(mainRoot); } else { explanation = expectedRoot.checkTreeEquals(mainRoot); } if (explanation != null) { String expectedAsSource = compiler.toSource(expectedRoot); String mainAsSource = compiler.toSource(mainRoot); if (expectedAsSource.equals(mainAsSource)) { fail("In: " + expectedAsSource + "\n" + explanation); } else { fail("\nExpected: " + expectedAsSource + "\nResult: " + mainAsSource + "\n" + explanation); } } } else { String[] expectedSources = new String[expected.size()]; for (int i = 0; i < expected.size(); ++i) { try { expectedSources[i] = expected.get(i).getCode(); } catch (IOException e) { throw new RuntimeException("failed to get source code", e); } } assertThat(compiler.toSource(mainRoot)).isEqualTo(Joiner.on("").join(expectedSources)); } } // Verify normalization is not invalidated. Node normalizeCheckRootClone = root.cloneTree(); Node normalizeCheckExternsRootClone = normalizeCheckRootClone.getFirstChild(); Node normalizeCheckMainRootClone = normalizeCheckRootClone.getLastChild(); new PrepareAst(compiler).process(normalizeCheckExternsRootClone, normalizeCheckMainRootClone); String explanation = normalizeCheckMainRootClone.checkTreeEquals(mainRoot); assertNull( "Node structure normalization invalidated." + "\nExpected: " + compiler.toSource(normalizeCheckMainRootClone) + "\nResult: " + compiler.toSource(mainRoot) + "\n" + explanation, explanation); // TODO(johnlenz): enable this for most test cases. // Currently, this invalidates test for while-loops, for-loop // initializers, and other naming. However, a set of code // (Closure primitive rewrites, etc) runs before the Normalize pass, // so this can't be force on everywhere. if (normalizeEnabled) { new Normalize(compiler, true) .process(normalizeCheckExternsRootClone, normalizeCheckMainRootClone); explanation = normalizeCheckMainRootClone.checkTreeEquals(mainRoot); assertNull( "Normalization invalidated." + "\nExpected: " + compiler.toSource(normalizeCheckMainRootClone) + "\nResult: " + compiler.toSource(mainRoot) + "\n" + explanation, explanation); } } else { assertNull("expected must be null if error != null", expected); assertEquals( "There should be one error of type '" + error.key + "' but there were: " + Arrays.toString(compiler.getErrors()), 1, compiler.getErrorCount()); JSError actualError = compiler.getErrors()[0]; assertEquals(errorMsg, error, actualError.getType()); validateSourceLocation(actualError); if (description != null) { assertThat(actualError.description).isEqualTo(description); } assertWithMessage("Some placeholders in the error message were not replaced") .that(actualError.description) .doesNotContainMatch("\\{\\d\\}"); if (warning != null) { String warnings = ""; for (JSError actualWarning : compiler.getWarnings()) { warnings += actualWarning.description + "\n"; assertWithMessage("Some placeholders in the warning message were not replaced") .that(actualWarning.description) .doesNotContainMatch("\\{\\d\\}"); } assertEquals("There should be one warning. " + warnings, 1, compiler.getWarningCount()); assertEquals(warnings, warning, compiler.getWarnings()[0].getType()); } } } private static void transpileToEs5(AbstractCompiler compiler, Node externsRoot, Node codeRoot) { List<PassFactory> factories = new ArrayList<>(); TranspilationPasses.addEs2017Passes(factories); TranspilationPasses.addEs6EarlyPasses(factories); TranspilationPasses.addEs6LatePasses(factories); TranspilationPasses.addRewritePolyfillPass(factories); for (PassFactory factory : factories) { factory.create(compiler).process(externsRoot, codeRoot); } } private void validateSourceLocation(JSError jserror) { // Make sure that source information is always provided. if (!allowSourcelessWarnings) { assertTrue( "Missing source file name in warning: " + jserror, jserror.sourceName != null && !jserror.sourceName.isEmpty()); assertTrue("Missing line number in warning: " + jserror, -1 != jserror.lineNumber); assertTrue("Missing char number in warning: " + jserror, -1 != jserror.getCharno()); } } private static void normalizeActualCode(Compiler compiler, Node externsRoot, Node mainRoot) { Normalize normalize = new Normalize(compiler, false); normalize.process(externsRoot, mainRoot); } /** * Parses expected JS inputs and returns the root of the parse tree. */ protected Node parseExpectedJs(String[] expected) { List<SourceFile> inputs = new ArrayList<>(); for (int i = 0; i < expected.length; i++) { inputs.add(SourceFile.fromCode("expected" + i, expected[i])); } return parseExpectedJs(inputs); } /** * Parses expected JS inputs and returns the root of the parse tree. */ protected Node parseExpectedJs(List<SourceFile> inputs) { Compiler compiler = createCompiler(); compiler.init(externsInputs, inputs, getOptions()); Node root = compiler.parseInputs(); assertNotNull("Unexpected parse error(s): " + LINE_JOINER.join(compiler.getErrors()), root); Node externsRoot = root.getFirstChild(); Node mainRoot = externsRoot.getNext(); // Only run the normalize pass, if asked. if (normalizeEnabled && !compiler.hasErrors()) { Normalize normalize = new Normalize(compiler, false); normalize.process(externsRoot, mainRoot); } if (closurePassEnabled && closurePassEnabledForExpected && !compiler.hasErrors()) { new ProcessClosurePrimitives(compiler, null, CheckLevel.ERROR, false) .process(externsRoot, mainRoot); } if (rewriteClosureCode) { new ClosureRewriteClass(compiler).process(externsRoot, mainRoot); new ClosureRewriteModule(compiler, null, null).process(externsRoot, mainRoot); new ScopedAliases(compiler, null, CompilerOptions.NULL_ALIAS_TRANSFORMATION_HANDLER) .process(externsRoot, mainRoot); } if (transpileEnabled && transpileExpected && !compiler.hasErrors()) { transpileToEs5(compiler, externsRoot, mainRoot); } return mainRoot; } protected void testExternChanges(String input, String expectedExtern) { testExternChanges("", input, expectedExtern); } protected void testExternChanges(String extern, String input, String expectedExtern) { testExternChanges(extern, input, expectedExtern, (DiagnosticType[]) null); } protected void testExternChanges(String input, String expectedExtern, DiagnosticType... warnings) { testExternChanges("", input, expectedExtern, warnings); } protected void testExternChanges(String extern, String input, String expectedExtern, DiagnosticType... warnings) { Compiler compiler = createCompiler(); CompilerOptions options = getOptions(); compiler.init( ImmutableList.of(SourceFile.fromCode("extern", extern)), ImmutableList.of(SourceFile.fromCode("input", input)), options); compiler.parseInputs(); assertThat(compiler.hasErrors()).isFalse(); Node externsAndJs = compiler.getRoot(); Node root = externsAndJs.getLastChild(); Node externs = externsAndJs.getFirstChild(); Node expected = compiler.parseTestCode(expectedExtern); assertThat(compiler.getErrors()).isEmpty(); (getProcessor(compiler)).process(externs, root); if (compareAsTree) { // Ignore and remove empty externs, so that if we start with an empty extern and only add // to the synthetic externs, we can still enable compareAsTree. if (externs.hasMoreThanOneChild()) { for (Node c : externs.children()) { if (!c.hasChildren()) { c.detach(); } } } // Expected output parsed without implied block. Preconditions.checkState(externs.isRoot()); Preconditions.checkState(compareJsDoc); Preconditions.checkState( externs.hasOneChild(), "Compare as tree only works when output has a single script."); externs = externs.getFirstChild(); String explanation = expected.checkTreeEqualsIncludingJsDoc(externs); assertNull("" + "\nExpected: " + compiler.toSource(expected) + "\nResult: " + compiler.toSource(externs) + "\n" + explanation, explanation); } else { String externsCode = compiler.toSource(externs); String expectedCode = compiler.toSource(expected); assertThat(externsCode).isEqualTo(expectedCode); } if (warnings != null) { String warningMessage = ""; for (JSError actualWarning : compiler.getWarnings()) { warningMessage += actualWarning.description + "\n"; } assertEquals("There should be " + warnings.length + " warnings. " + warningMessage, warnings.length, compiler.getWarningCount()); for (int i = 0; i < warnings.length; i++) { DiagnosticType warning = warnings[i]; assertEquals(warningMessage, warning, compiler.getWarnings()[i].getType()); } } } protected Node parseExpectedJs(String expected) { return parseExpectedJs(new String[] {expected}); } /** * Generates a list of modules from a list of inputs, such that each module * depends on the module before it. */ static JSModule[] createModuleChain(String... inputs) { return createModuleChain(Arrays.asList(inputs), "i", ".js"); } static JSModule[] createModuleChain( List<String> inputs, String fileNamePrefix, String fileNameSuffix) { JSModule[] modules = createModules(inputs, fileNamePrefix, fileNameSuffix); for (int i = 1; i < modules.length; i++) { modules[i].addDependency(modules[i - 1]); } return modules; } /** * Generates a list of modules from a list of inputs, such that each module * depends on the first module. */ static JSModule[] createModuleStar(String... inputs) { JSModule[] modules = createModules(inputs); for (int i = 1; i < modules.length; i++) { modules[i].addDependency(modules[0]); } return modules; } /** * Generates a list of modules from a list of inputs, such that modules * form a bush formation. In a bush formation, module 2 depends * on module 1, and all other modules depend on module 2. */ static JSModule[] createModuleBush(String... inputs) { Preconditions.checkState(inputs.length > 2); JSModule[] modules = createModules(inputs); for (int i = 1; i < modules.length; i++) { modules[i].addDependency(modules[i == 1 ? 0 : 1]); } return modules; } /** * Generates a list of modules from a list of inputs, such that modules * form a tree formation. In a tree formation, module N depends on * module `floor(N/2)`, So the modules form a balanced binary tree. */ static JSModule[] createModuleTree(String... inputs) { JSModule[] modules = createModules(inputs); for (int i = 1; i < modules.length; i++) { modules[i].addDependency(modules[(i - 1) / 2]); } return modules; } /** * Generates a list of modules from a list of inputs. Does not generate any * dependencies between the modules. */ static JSModule[] createModules(String... inputs) { return createModules(Arrays.asList(inputs), "i", ".js"); } static JSModule[] createModules( List<String> inputs, String fileNamePrefix, String fileNameSuffix) { JSModule[] modules = new JSModule[inputs.size()]; for (int i = 0; i < inputs.size(); i++) { JSModule module = modules[i] = new JSModule("m" + i); module.add(SourceFile.fromCode(fileNamePrefix + i + fileNameSuffix, inputs.get(i))); } return modules; } Compiler createCompiler() { Compiler compiler = new Compiler(); compiler.setLanguageMode(acceptedLanguage); return compiler; } protected void setExpectedSymbolTableError(DiagnosticType type) { this.expectedSymbolTableError = type; } /** Finds the first matching qualified name node in post-traversal order. */ public final Node findQualifiedNameNode(final String name, Node root) { return findQualifiedNameNodes(name, root).get(0); } /** Finds all the matching qualified name nodes in post-traversal order. */ public final List<Node> findQualifiedNameNodes(final String name, Node root) { final List<Node> matches = new ArrayList<>(); NodeUtil.visitPostOrder( root, new NodeUtil.Visitor() { @Override public void visit(Node n) { if (name.equals(n.getQualifiedName())) { matches.add(n); } } }, Predicates.<Node>alwaysTrue()); return matches; } /** A Compiler that records requested runtime libraries, rather than injecting. */ protected static class NoninjectingCompiler extends Compiler { protected final Set<String> injected = new HashSet<>(); @Override Node ensureLibraryInjected(String library, boolean force) { injected.add(library); return null; } } }