From b904af77370212e9adcd381d67d1857571267ac6 Mon Sep 17 00:00:00 2001 From: lharker Date: Mon, 2 Apr 2018 10:37:33 -0700 Subject: [PATCH] Preserve type information in Es6RewriteGenerators Tested through the AstValidator's type info validation feature and through checking the actual type on a subset of the unit tests in Es6RewriteGenerators. This moves typechecking in Es6RewriteGenerators before transpilation, and also adds an option to inject the required polyfills (base.js and es6/generator_engine.js) before typechecking in the unit tests. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=191312444 --- .../jscomp/Es6RewriteGenerators.java | 191 ++++++++++--- .../google/javascript/jscomp/TypeCheck.java | 10 +- .../javascript/jscomp/CompilerTestCase.java | 15 + .../jscomp/Es6RewriteGeneratorsTest.java | 260 +++++++++++++++++- 4 files changed, 421 insertions(+), 55 deletions(-) diff --git a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java index d230ea30e0e..ddd49f617ba 100644 --- a/src/com/google/javascript/jscomp/Es6RewriteGenerators.java +++ b/src/com/google/javascript/jscomp/Es6RewriteGenerators.java @@ -17,8 +17,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static com.google.javascript.jscomp.Es6ToEs3Util.withType; import com.google.common.collect.Iterables; +import com.google.javascript.jscomp.AbstractCompiler.MostRecentTypechecker; import com.google.javascript.jscomp.ControlFlowGraph.Branch; import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge; import com.google.javascript.jscomp.parsing.parser.FeatureSet; @@ -28,6 +30,11 @@ import com.google.javascript.rhino.JSDocInfoBuilder; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; +import com.google.javascript.rhino.jstype.FunctionType; +import com.google.javascript.rhino.jstype.JSType; +import com.google.javascript.rhino.jstype.JSTypeNative; +import com.google.javascript.rhino.jstype.JSTypeRegistry; +import com.google.javascript.rhino.jstype.ObjectType; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; @@ -77,9 +84,38 @@ final class Es6RewriteGenerators implements HotSwapCompilerPass { private final AbstractCompiler compiler; + private final JSTypeRegistry registry; + + private final boolean shouldAddTypes; + + private final JSType unknownType; + private final JSType numberType; + private final JSType booleanType; + private final JSType nullType; + private final JSType nullableStringType; + Es6RewriteGenerators(AbstractCompiler compiler) { checkNotNull(compiler); this.compiler = compiler; + registry = compiler.getTypeRegistry(); + + if (compiler.getMostRecentTypechecker() == MostRecentTypechecker.OTI) { + // typechecking has run, so we must preserve and propagate type information + shouldAddTypes = true; + unknownType = registry.getNativeType(JSTypeNative.UNKNOWN_TYPE); + numberType = registry.getNativeType(JSTypeNative.NUMBER_TYPE); + booleanType = registry.getNativeType(JSTypeNative.BOOLEAN_TYPE); + nullType = registry.getNativeType(JSTypeNative.NULL_TYPE); + nullableStringType = + registry.createNullableType(registry.getNativeType(JSTypeNative.STRING_TYPE)); + } else { + shouldAddTypes = false; + unknownType = null; + numberType = null; + booleanType = null; + nullType = null; + nullableStringType = null; + } } @Override @@ -206,10 +242,35 @@ private class SingleGeneratorFunctionTranspiler { /** The body of a replacement function. */ Node newGeneratorBody; + /** The original inferred return type of the Generator */ + JSType originalGenReturnType; + SingleGeneratorFunctionTranspiler(Node genFunc, int genaratorNestingLevel) { this.generatorNestingLevel = genaratorNestingLevel; this.originalGeneratorBody = genFunc.getLastChild(); - this.context = new TranspilationContext(); + ObjectType contextType = null; + if (shouldAddTypes) { + // Find the yield type of the generator. + // e.g. given @return {!Generator}, we want this.yieldType to be number. + JSType yieldType = unknownType; + if (genFunc.getJSType() != null && genFunc.getJSType().isFunctionType()) { + FunctionType fnType = genFunc.getJSType().toMaybeFunctionType(); + this.originalGenReturnType = fnType.getReturnType(); + yieldType = TypeCheck.getTemplateTypeOfGenerator(registry, this.originalGenReturnType); + } + + JSType globalContextType = registry.getGlobalType("$jscomp.generator.Context"); + if (globalContextType == null) { + // We don't have the es6/generator polyfill, which can happen in tests using a + // NonInjectingCompiler or if someone sets --inject_libraries=false. Don't crash, just + // back off on giving some type information. + contextType = registry.getNativeObjectType(JSTypeNative.OBJECT_TYPE); + } else { + contextType = + registry.createTemplatizedType(globalContextType.toMaybeObjectType(), yieldType); + } + } + this.context = new TranspilationContext(contextType); } public void transpile() { @@ -233,8 +294,7 @@ public void transpile() { // switch ($jscomp$generator$context.nextAddress) { // } - Node switchNode = - IR.switchNode(IR.getprop(context.getJsContextNameNode(genFunc), "nextAddress")); + Node switchNode = IR.switchNode(context.getContextField(genFunc, "nextAddress")); // Prepare a "program" function: // function ($jscomp$generator$context) { @@ -250,9 +310,8 @@ public void transpile() { // once, which messes up its understanding of the types assigned to variables // within it. IR.doNode( // TODO(skill): Remove do-while loop when this pass moves after - // type checking or when OTI is fixed to handle this correctly. - IR.block(switchNode), - IR.number(0)))); + // type checking or when OTI is fixed to handle this correctly. + IR.block(switchNode), withType(IR.number(0), numberType)))); // Propagate all suppressions from original generator function to a new "program" function. JSDocInfoBuilder jsDocBuilder = new JSDocInfoBuilder(false); @@ -269,13 +328,23 @@ public void transpile() { // Replace original generator function body with: // return $jscomp.generator.createGenerator(, ); + Node createGenerator = + IR.getprop( + withType( + IR.getprop(withType(IR.name("$jscomp"), unknownType), "generator"), unknownType), + "createGenerator"); newGeneratorBody = IR.block( - IR.returnNode( - IR.call( - IR.getprop(IR.name("$jscomp"), "generator", "createGenerator"), - genFuncName.cloneNode(), - program)).useSourceInfoFromForTree(originalGeneratorBody)); + IR.returnNode( + withType( + IR.call(createGenerator, genFuncName.cloneNode(), program), + this.originalGenReturnType)) + .useSourceInfoFromForTree(originalGeneratorBody)); + + if (shouldAddTypes) { + createGenerator.setJSType(registry.createFunctionType(originalGenReturnType)); + program.setJSType(registry.createFunctionType(unknownType)); + } // Newly introduced functions have to be reported immediately. compiler.reportChangeToChangeScope(program); @@ -612,7 +681,7 @@ void transpileIf(Node n, @Nullable TranspilationContext.Case breakCase) { // Only "else" block is unmarked, swap "if" and "else" blocks and negate the condition. if (ifBlock.isGeneratorMarker() && !elseBlock.isGeneratorMarker()) { - condition = IR.not(condition).useSourceInfoFrom(condition); + condition = withType(IR.not(condition), booleanType).useSourceInfoFrom(condition); Node tmpNode = ifBlock; ifBlock = elseBlock; elseBlock = tmpNode; @@ -689,8 +758,12 @@ void transpileFor( condition = prepareNodeForWrite(maybeDecomposeExpression(condition.detach())); context.writeGeneratedNode( IR.ifNode( - IR.not(condition).useSourceInfoFrom(condition), - context.createJumpToBlock(endCase, /** allowEmbedding=*/ true, n)) + withType(IR.not(condition), booleanType).useSourceInfoFrom(condition), + context.createJumpToBlock( + endCase, + /** allowEmbedding= */ + true, + n)) .useSourceInfoFrom(n)); } @@ -753,24 +826,33 @@ void transpileForIn( .useSourceInfoFrom(target); // "$context.forIn(x)" forIn.addChildToFront(context.callContextMethod(target, "forIn", detachedCond)); + ObjectType propertyIteratorType = + shouldAddTypes ? forIn.getFirstChild().getJSType().toMaybeObjectType() : null; + forIn.setJSType(propertyIteratorType); // "var ..., $for$in = $context.forIn(expr)" init.addChildToBack(forIn); // "(i = $for$in.getNext()) != null" + Node forInGetNext = + withType( + IR.getprop( + forIn.cloneNode(), IR.string("getNext").useSourceInfoFrom(detachedCond)), + shouldAddTypes ? propertyIteratorType.getPropertyType("getNext") : null) + .useSourceInfoFrom(detachedCond); + Node forCond = IR.ne( - IR.assign( - target, - IR.call( - IR.getprop( - forIn.cloneNode(), - IR.string("getNext").useSourceInfoFrom(detachedCond)) - .useSourceInfoFrom(detachedCond)) - .useSourceInfoFrom(detachedCond)) - .useSourceInfoFrom(detachedCond), - IR.nullNode().useSourceInfoFrom(forIn)) - .useSourceInfoFrom(detachedCond); + IR.assign( + withType(target, nullableStringType), + withType(IR.call(forInGetNext), nullableStringType) + .useSourceInfoFrom(detachedCond)) + .useSourceInfoFrom(detachedCond), + withType(IR.nullNode(), nullType).useSourceInfoFrom(forIn)) + .useSourceInfoFrom(detachedCond); forCond.setGeneratorMarker(target.isGeneratorMarker()); + // annotate types + forCond.setJSType(booleanType); + forCond.getFirstChild().setJSType(nullableStringType); // Prepare "for" statement. // "for (var i, $for$in = $context.forIn(expr); (i = $for$in.getNext()) != null; ) {}" @@ -797,8 +879,12 @@ void transpileWhile( Node body = n.removeFirstChild(); context.writeGeneratedNode( IR.ifNode( - IR.not(condition).useSourceInfoFrom(condition), - context.createJumpToBlock(endCase, /** allowEmbedding=*/ true, n)) + withType(IR.not(condition), booleanType).useSourceInfoFrom(condition), + context.createJumpToBlock( + endCase, + /** allowEmbedding= */ + true, + n)) .useSourceInfoFrom(n)); // Transpile "while" body @@ -1046,12 +1132,16 @@ private class TranspilationContext { boolean thisReferenceFound; boolean argumentsReferenceFound; - TranspilationContext() { + /** The JSType for this context. May be null. */ + final ObjectType contextType; + + TranspilationContext(ObjectType contextType) { programEndCase = new Case(); checkState(programEndCase.id == 0); currentCase = new Case(); checkState(currentCase.id == 1); allCases.add(currentCase); + this.contextType = contextType; } /** @@ -1241,7 +1331,8 @@ Case maybeCreateCase(@Nullable Case other) { /** Returns the name node of context parameter passed to the program. */ Node getJsContextNameNode(Node sourceNode) { - return getScopedName(GENERATOR_CONTEXT).useSourceInfoFrom(sourceNode); + return withType( + getScopedName(GENERATOR_CONTEXT).useSourceInfoFrom(sourceNode), this.contextType); } /** Returns unique name in the current context. */ @@ -1251,15 +1342,25 @@ Node getScopedName(String name) { /** Creates node that access a specified field of the current context. */ Node getContextField(Node sourceNode, String fieldName) { - return IR.getprop( - getJsContextNameNode(sourceNode), - IR.string(fieldName).useSourceInfoFrom(sourceNode)) + return withType( + IR.getprop( + getJsContextNameNode(sourceNode), + IR.string(fieldName).useSourceInfoFrom(sourceNode)), + shouldAddTypes ? this.contextType.getPropertyType(fieldName) : null) .useSourceInfoFrom(sourceNode); } /** Creates node that make a call to a context function. */ Node callContextMethod(Node sourceNode, String methodName, Node... args) { - return IR.call(getContextField(sourceNode, methodName), args).useSourceInfoFrom(sourceNode); + Node contextField = getContextField(sourceNode, methodName); + Node callNode = IR.call(contextField, args).useSourceInfoFrom(sourceNode); + if (shouldAddTypes) { + callNode.setJSType( + contextField.getJSType().isFunctionType() + ? contextField.getJSType().toMaybeFunctionType().getReturnType() + : unknownType); + } + return callNode; } /** Creates node that make a call to a context function. */ @@ -1479,12 +1580,14 @@ void enterCatchBlock(@Nullable Case finallyCase, Node exceptionName) { args.add(nextCatchCase.getNumber(exceptionName)); } + Node enterCatchBlockCall = + callContextMethod(exceptionName, "enterCatchBlock", args.toArray(new Node[0])); + exceptionName.setJSType(enterCatchBlockCall.getJSType()); writeGeneratedNode( IR.exprResult( - IR.assign( - exceptionName, - callContextMethod( - exceptionName, "enterCatchBlock", args.toArray(new Node[0]))) + withType( + IR.assign(exceptionName, enterCatchBlockCall), + enterCatchBlockCall.getJSType()) .useSourceInfoFrom(exceptionName)) .useSourceInfoFrom(exceptionName)); } @@ -1510,7 +1613,7 @@ void enterFinallyBlock( if (nextCatchCase != null || nextFinallyCase != null) { args.add( nextCatchCase == null - ? IR.number(0).useSourceInfoFrom(sourceNode) + ? withType(IR.number(0), numberType).useSourceInfoFrom(sourceNode) : nextCatchCase.getNumber(sourceNode)); if (nextFinallyCase != null) { args.add(nextFinallyCase.getNumber(sourceNode)); @@ -1519,11 +1622,11 @@ void enterFinallyBlock( } else { args.add( nextCatchCase == null - ? IR.number(0).useSourceInfoFrom(sourceNode) + ? withType(IR.number(0), numberType).useSourceInfoFrom(sourceNode) : nextCatchCase.getNumber(sourceNode)); args.add( nextFinallyCase == null - ? IR.number(0).useSourceInfoFrom(sourceNode) + ? withType(IR.number(0), numberType).useSourceInfoFrom(sourceNode) : nextFinallyCase.getNumber(sourceNode)); args.add(IR.number(nestedFinallyBlockCount).useSourceInfoFrom(sourceNode)); } @@ -1649,7 +1752,8 @@ private class Case { } Node createCaseNode() { - return IR.caseNode(IR.number(id).useSourceInfoFrom(caseBlock), caseBlock) + return IR.caseNode( + withType(IR.number(id), numberType).useSourceInfoFrom(caseBlock), caseBlock) .useSourceInfoFrom(caseBlock); } @@ -1658,7 +1762,7 @@ Node getNumber(Node sourceNode) { if (jumpTo != null) { return jumpTo.getNumber(sourceNode); } - Node node = IR.number(id).useSourceInfoFrom(sourceNode); + Node node = withType(IR.number(id).useSourceInfoFrom(sourceNode), numberType); references.add(node); return node; } @@ -1904,7 +2008,8 @@ void visitVar(Node varStatement) { commaExpression = commaExpression == null ? assignment - : IR.comma(commaExpression, assignment).useSourceInfoFrom(assignment); + : withType(IR.comma(commaExpression, assignment), assignment.getJSType()) + .useSourceInfoFrom(assignment); } varStatement.replaceWith(IR.exprResult(commaExpression)); } diff --git a/src/com/google/javascript/jscomp/TypeCheck.java b/src/com/google/javascript/jscomp/TypeCheck.java index 744e90b909b..7d437629f90 100644 --- a/src/com/google/javascript/jscomp/TypeCheck.java +++ b/src/com/google/javascript/jscomp/TypeCheck.java @@ -2161,13 +2161,17 @@ private void visitYield(NodeTraversal t, Node n) { "Yielded type does not match declared return type."); } + private JSType getTemplateTypeOfGenerator(JSType generator) { + return getTemplateTypeOfGenerator(typeRegistry, generator); + } + /** * Returns the given type's resolved template type corresponding to the corresponding to the * Generator, Iterable or Iterator template key if possible. * - * If the given type is not an Iterator or Iterable, returns the unknown type.. + *

If the given type is not an Iterator or Iterable, returns the unknown type.. */ - private JSType getTemplateTypeOfGenerator(JSType generator) { + static JSType getTemplateTypeOfGenerator(JSTypeRegistry typeRegistry, JSType generator) { ObjectType dereferencedType = generator.dereference(); if (dereferencedType != null) { TemplateTypeMap templateTypeMap = dereferencedType.getTemplateTypeMap(); @@ -2183,7 +2187,7 @@ private JSType getTemplateTypeOfGenerator(JSType generator) { return templateTypeMap.getResolvedTemplateType(typeRegistry.getIteratorTemplate()); } } - return getNativeType(UNKNOWN_TYPE); + return typeRegistry.getNativeType(UNKNOWN_TYPE); } /** diff --git a/test/com/google/javascript/jscomp/CompilerTestCase.java b/test/com/google/javascript/jscomp/CompilerTestCase.java index 0401f8572f3..079b52d022e 100644 --- a/test/com/google/javascript/jscomp/CompilerTestCase.java +++ b/test/com/google/javascript/jscomp/CompilerTestCase.java @@ -66,6 +66,9 @@ public abstract class CompilerTestCase extends TestCase { /** Externs for the test */ final List externsInputs; + /** Libraries to inject before typechecking */ + final Set librariesToInject; + /** Whether to include synthetic code when comparing actual to expected */ private boolean compareSyntheticCode; @@ -554,6 +557,7 @@ public abstract class CompilerTestCase extends TestCase { */ protected CompilerTestCase(String externs) { this.externsInputs = ImmutableList.of(SourceFile.fromCode("externs", externs)); + librariesToInject = new HashSet<>(); } /** @@ -993,6 +997,11 @@ protected static void runNewTypeInference(Compiler compiler, Node externs, Node nti.process(externs, js); } + /** Ensures the given library is injected before typechecking */ + protected final void ensureLibraryInjected(String resourceName) { + librariesToInject.add(resourceName); + } + /** * Verifies that the compiler pass's JS output matches the expected output. * @@ -1490,6 +1499,12 @@ private void testInternal( hasCodeChanged = hasCodeChanged || recentChange.hasCodeChanged(); } + if (!librariesToInject.isEmpty() && i == 0) { + for (String resourceName : librariesToInject) { + compiler.ensureLibraryInjected(resourceName, true); + } + } + // 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 diff --git a/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java b/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java index 46adc959d7e..fffb8a5abc1 100644 --- a/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java +++ b/test/com/google/javascript/jscomp/Es6RewriteGeneratorsTest.java @@ -15,18 +15,30 @@ */ package com.google.javascript.jscomp; +import static com.google.common.base.Preconditions.checkState; + import com.google.javascript.jscomp.CompilerOptions.LanguageMode; +import com.google.javascript.rhino.Node; /** Unit tests for {@link Es6RewriteGenerators}. */ public final class Es6RewriteGeneratorsTest extends CompilerTestCase { private boolean allowMethodCallDecomposing; + public Es6RewriteGeneratorsTest() { + super(DEFAULT_EXTERNS); + } + @Override protected void setUp() throws Exception { super.setUp(); allowMethodCallDecomposing = false; setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); - enableRunTypeCheckAfterProcessing(); + enableTypeCheck(); + enableTypeInfoValidation(); + // Es6RewriteGenerators uses named types declared in generator_engine.js + ensureLibraryInjected("es6/generator_engine"); + // generator_engine depends on util/global, which declares externs for 'window' and 'global' + allowExternsChanges(); disableCompareSyntheticCode(); } @@ -49,9 +61,15 @@ private void rewriteGeneratorBody(String beforeBody, String afterBody) { private void rewriteGeneratorBodyWithVars( String beforeBody, String varDecls, String afterBody) { + rewriteGeneratorBodyWithVarsAndReturnType(beforeBody, varDecls, afterBody, "?"); + } + + private void rewriteGeneratorBodyWithVarsAndReturnType( + String beforeBody, String varDecls, String afterBody, String returnType) { test( - "function *f() {" + beforeBody + "}", + "/** @return {" + returnType + "} */ function *f() {" + beforeBody + "}", lines( + "/** @return {" + returnType + "} */", "function f() {", varDecls, " return $jscomp.generator.createGenerator(", @@ -333,9 +351,11 @@ public void testDecomposableExpression() { allowMethodCallDecomposing = true; rewriteGeneratorBodyWithVars( - "obj.bar(yield 5);", - lines("var JSCompiler_temp_const$jscomp$1;", "var JSCompiler_temp_const$jscomp$0;"), + "var obj = {bar: function(x) {}}; obj.bar(yield 5);", + lines( + "var obj; var JSCompiler_temp_const$jscomp$1;", "var JSCompiler_temp_const$jscomp$0;"), lines( + " obj = {bar: function(x) {}};", " JSCompiler_temp_const$jscomp$1 = obj;", " JSCompiler_temp_const$jscomp$0 = JSCompiler_temp_const$jscomp$1.bar;", " return $jscomp$generator$context.yield(5, 2);", @@ -856,12 +876,11 @@ public void testVar() { rewriteGeneratorBodyWithVars( lines( - "/** @const @type {?} */", - "var /** @const @type {number} */ a = 10, b, c = yield 10, d = yield 20, f, g='test';"), + "var /** @const @type {number} */ a = 10, b, c = yield 10, d = yield 20, f, g='test';"), lines( - "/** @type {?} */ var /** @type {number} */ a, b;", - "/** @type {?} */ var c;", - "/** @type {?} */ var d, f, g;"), + "var /** @type {number} */ a, b;", + "var c;", // note that the yields cause the var declarations to be split up + "var d, f, g;"), lines( " /** @const @type {number} */ a = 10;", " return $jscomp$generator$context.yield(10, 2);", @@ -1112,4 +1131,227 @@ public void testFinally() { " thrown = $jscomp$generator$context.enterCatchBlock();", " return $jscomp$generator$context.yield(thrown,0)")); } + + /** Tests correctness of type information after transpilation */ + public void testYield_withTypes() { + Node returnNode = + testAndReturnBodyForNumericGenerator( + "yield 1 + 2;", "", "return $jscomp$generator$context.yield(1 + 2, 0);") + .getSecondChild() + .getFirstChild(); + + checkState(returnNode.isReturn(), returnNode); + Node callNode = returnNode.getFirstChild(); + checkState(callNode.isCall(), callNode); + // TODO(lharker): this should really be {value: number} and may indicate a bug in OTI + // Possibly the same as https://github.com/google/closure-compiler/issues/2867. + assertEquals("{value: VALUE}", callNode.getJSType().toString()); + + Node yieldFn = callNode.getFirstChild(); + Node jscompGeneratorContext = yieldFn.getFirstChild(); + assertTrue(yieldFn.getJSType().isFunctionType()); + assertEquals( + "$jscomp.generator.Context", jscompGeneratorContext.getJSType().toString()); + + // Check types on "1 + 2" are still present after transpilation + Node yieldedValue = callNode.getSecondChild(); // 1 + 2 + + checkState(yieldedValue.isAdd(), yieldedValue); + assertEquals("number", yieldedValue.getJSType().toString()); + assertEquals("number", yieldedValue.getFirstChild().getJSType().toString()); // 1 + assertEquals("number", yieldedValue.getSecondChild().getJSType().toString()); // 2 + + Node zero = yieldedValue.getNext(); + checkState(0 == zero.getDouble(), zero); + assertEquals("number", zero.getJSType().toString()); + } + + public void testYieldAll_withTypes() { + Node returnNode = + testAndReturnBodyForNumericGenerator( + "yield * [1, 2];", "", "return $jscomp$generator$context.yieldAll([1, 2], 0);") + .getSecondChild() + .getFirstChild(); + + checkState(returnNode.isReturn(), returnNode); + Node callNode = returnNode.getFirstChild(); + checkState(callNode.isCall(), callNode); + // TODO(lharker): this really should be {value: number} + assertEquals("(undefined|{value: VALUE})", callNode.getJSType().toString()); + + Node yieldAllFn = callNode.getFirstChild(); + checkState(yieldAllFn.isGetProp()); + assertTrue(yieldAllFn.getJSType().isFunctionType()); + + // Check that the original types on "[1, 2]" are still present after transpilation + Node yieldedValue = callNode.getSecondChild(); // [1, 2] + + checkState(yieldedValue.isArrayLit(), yieldedValue); + assertEquals("Array", yieldedValue.getJSType().toString()); // [1, 2] + assertEquals("number", yieldedValue.getFirstChild().getJSType().toString()); // 1 + assertEquals("number", yieldedValue.getSecondChild().getJSType().toString()); // 2 + + Node zero = yieldedValue.getNext(); + checkState(0 == zero.getDouble(), zero); + assertEquals("number", zero.getJSType().toString()); + } + + public void testGeneratorForIn_withTypes() { + Node case1Node = + testAndReturnBodyForNumericGenerator( + "for (var i in []) { yield 3; };", + "var i, $jscomp$generator$forin$0;", + lines( + "$jscomp$generator$forin$0 = $jscomp$generator$context.forIn([]);", + "case 2:", + "if (!((i = $jscomp$generator$forin$0.getNext()) != null)) {", + " $jscomp$generator$context.jumpTo(4);", + " break;", + "}", + "return $jscomp$generator$context.yield(3, 2);", + "case 4:", + ";", + "$jscomp$generator$context.jumpToEnd();")); + + // $jscomp$generator$forin$0 = $jscomp$generator$context.forIn([]); + Node assign = case1Node.getSecondChild().getFirstFirstChild(); + checkState(assign.isAssign(), assign); + assertEquals("$jscomp.generator.Context.PropertyIterator", assign.getJSType().toString()); + assertEquals( + "$jscomp.generator.Context.PropertyIterator", + assign.getFirstChild().getJSType().toString()); + assertEquals( + "$jscomp.generator.Context.PropertyIterator", + assign.getSecondChild().getJSType().toString()); + + // if (!((i = $jscomp$generator$forin$0.getNext()) != null)) { + Node case2Node = case1Node.getNext(); + Node ifNode = case2Node.getSecondChild().getFirstChild(); + checkState(ifNode.isIf(), ifNode); + Node ifCond = ifNode.getFirstChild(); + checkState(ifCond.isNot(), ifCond); + assertEquals("boolean", ifCond.getJSType().toString()); + Node ne = ifCond.getFirstChild(); + assertEquals("boolean", ifCond.getJSType().toString()); + + Node lhs = ne.getFirstChild(); // i = $jscomp$generator$forin$0.getNext() + assertEquals("(null|string)", lhs.getJSType().toString()); + assertEquals("(null|string)", lhs.getFirstChild().getJSType().toString()); + assertEquals("(null|string)", lhs.getSecondChild().getJSType().toString()); + Node getNextFn = lhs.getSecondChild().getFirstChild(); + assertTrue(getNextFn.getJSType().isFunctionType()); + + Node rhs = ne.getSecondChild(); + checkState(rhs.isNull(), rhs); + assertEquals("null", rhs.getJSType().toString()); + + // $jscomp$generator$context.jumpToEnd() + Node case4Node = case2Node.getNext(); + Node jumpToEndCall = case4Node.getSecondChild().getFirstChild().getNext().getFirstChild(); + checkState(jumpToEndCall.isCall()); + + Node jumpToEndFn = jumpToEndCall.getFirstChild(); + Node jscompGeneratorContext = jumpToEndFn.getFirstChild(); + + assertEquals("undefined", jumpToEndCall.getJSType().toString()); + assertEquals( + "$jscomp.generator.Context", jscompGeneratorContext.getJSType().toString()); + } + + public void testGeneratorTryCatch_withTypes() { + Node case0Node = + testAndReturnBodyForNumericGenerator( + "try {yield 1;} catch (e) {}", + "var e;", + lines( + " $jscomp$generator$context.setCatchFinallyBlocks(2);", + " return $jscomp$generator$context.yield(1, 4);", + "case 4:", + " $jscomp$generator$context.leaveTryBlock(0)", + " break;", + "case 2:", + " e=$jscomp$generator$context.enterCatchBlock();", + " $jscomp$generator$context.jumpToEnd();")); + Node case2Node = case0Node.getNext().getNext(); + + // Test that "e = $jscomp$generator$context.enterCatchBlock();" has the unknown type + Node eAssign = case2Node.getSecondChild().getFirstFirstChild(); + checkState(eAssign.isAssign(), eAssign); + assertEquals("?", eAssign.getJSType().toString()); + assertEquals("?", eAssign.getFirstChild().getJSType().toString()); + assertEquals("?", eAssign.getSecondChild().getJSType().toString()); + + Node enterCatchBlockFn = eAssign.getSecondChild().getFirstChild(); + checkState(enterCatchBlockFn.isGetProp()); + assertTrue(enterCatchBlockFn.getJSType().isFunctionType()); + } + + public void testGeneratorMultipleVars_withTypes() { + Node case0Node = + testAndReturnBodyForNumericGenerator( + "var a = 1, b = '2';", + "var a, b;", + "a = 1, b = '2'; $jscomp$generator$context.jumpToEnd();"); + Node comma = case0Node.getSecondChild().getFirstFirstChild(); + checkState(comma.isComma(), comma); + assertEquals("string", comma.getJSType().toString()); + + // a = 1 + Node assignA = comma.getFirstChild(); + checkState(assignA.isAssign(), assignA); + assertEquals("number", assignA.getJSType().toString()); + assertEquals("number", assignA.getFirstChild().getJSType().toString()); + assertEquals("number", assignA.getSecondChild().getJSType().toString()); + + // b = '2'; + Node assignB = comma.getSecondChild(); + checkState(assignB.isAssign(), assignB); + assertEquals("string", assignB.getJSType().toString()); + assertEquals("string", assignB.getFirstChild().getJSType().toString()); + assertEquals("string", assignB.getSecondChild().getJSType().toString()); + } + + /** + * Tests that the given generator transpiles to the given body, and does some basic checks on the + * transpiled generator. + * + * @return The first case statement in the switch inside the transpiled generator + */ + private Node testAndReturnBodyForNumericGenerator( + String beforeBody, String varDecls, String afterBody) { + rewriteGeneratorBodyWithVarsAndReturnType( + beforeBody, varDecls, afterBody, "!Generator"); + + Node transpiledGenerator = getLastCompiler().getJsRoot().getLastChild().getLastChild(); + Node program = getAndCheckGeneratorProgram(transpiledGenerator); + + Node programBlock = NodeUtil.getFunctionBody(program); + // do switch ($jscomp$generator.context.nextAddress) {" + Node doNode = programBlock.getFirstChild(); + Node switchNode = doNode.getFirstFirstChild(); + checkState(switchNode.isSwitch()); + Node nextAddress = switchNode.getFirstChild(); + assertEquals("number", nextAddress.getJSType().toString()); + + return switchNode.getSecondChild(); + } + + /** Get the "program" function from a tranpsiled generator */ + private Node getAndCheckGeneratorProgram(Node genFunction) { + Node returnNode = genFunction.getLastChild().getLastChild(); + Node callNode = returnNode.getFirstChild(); + checkState(callNode.isCall(), callNode); + + Node createGenerator = callNode.getFirstChild(); + assertTrue(createGenerator.getJSType().isFunctionType()); // $jscomp.generator.createGenerator + assertEquals( + "Generator", + createGenerator.getJSType().toMaybeFunctionType().getReturnType().toString()); + + Node program = createGenerator.getNext().getNext(); + + assertTrue("Expected function: " + program.getJSType(), program.getJSType().isFunctionType()); + assertEquals("?", program.getJSType().toMaybeFunctionType().getReturnType().toString()); + return program; + } }