/* * Copyright 2014 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.javascript.jscomp.Es6RewriteClass.CLASS_REASSIGNMENT; import static com.google.javascript.jscomp.Es6RewriteClass.CONFLICTING_GETTER_SETTER_TYPE; import static com.google.javascript.jscomp.Es6RewriteClass.DYNAMIC_EXTENDS_TYPE; import static com.google.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT; import static com.google.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT_YET; import static com.google.javascript.jscomp.TypeCheck.INSTANTIATE_ABSTRACT_CLASS; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; /** * Test cases for ES6 transpilation. Despite the name, this isn't just testing {@link * Es6ToEs3Converter}, but also some other ES6 transpilation passes. See #getProcessor. * * @author tbreisacher@google.com (Tyler Breisacher) */ // TODO(tbreisacher): Rename this to Es6TranspilationIntegrationTest since it's really testing // a lot of different passes. Also create a unit test for Es6ToEs3Converter. public final class Es6ToEs3ConverterTest extends CompilerTestCase { private static final String EXTERNS_BASE = LINE_JOINER.join( "/** @constructor @template T */", "function Arguments() {}", "", "/**", " * @constructor", " * @param {...*} var_args", " * @return {!Array}", " * @template T", " */", "function Array(var_args) {}", "", "/** @constructor @template T */", "function Iterable() {}", "", "/** @constructor @template T */", "function Iterator() {}", "", "Iterator.prototype.next = function() {};", "", "/**", " * @record", " * @template VALUE", " */", "function IIterableResult() {}", "", "/** @type {boolean} */", "IIterableResult.prototype.done;", "", "/** @type {VALUE} */", "IIterableResult.prototype.value;", "", "/**", " * @param {...*} var_args", " * @return {*}", " */", "Function.prototype.apply = function(var_args) {};", "", "/**", " * @param {...*} var_args", " * @return {*}", " */", "Function.prototype.call = function(var_args) {};", "", // Stub out just enough of ES6 runtime libraries to satisfy the typechecker. // In a real compilation, the needed parts of the library are loaded automatically. "/**", " * @param {function(new: ?)} subclass", " * @param {function(new: ?)} superclass", " */", "$jscomp.inherits = function(subclass, superclass) {};", "", "/**", " * @param {string|!Array<T>|!Iterable<T>|!Iterator<T>|!Arguments<T>} iterable", " * @return {!Iterator<T>}", " * @template T", " */", "$jscomp.makeIterator = function(iterable) {};", "", "/**", " * @param {string|!Array<T>|!Iterable<T>|!Iterator<T>|!Arguments<T>} iterable", " * @return {!Array<T>}", " * @template T", " */", "$jscomp.arrayFromIterable = function(iterable) {};", "", "$jscomp.global.Object = function() {};", "", "/**", "* @param {!Object} obj", "* @param {!Object} props", "* @return {!Object}", "*/", "$jscomp.global.Object.defineProperties = function(obj, props) {};"); public Es6ToEs3ConverterTest() { super(EXTERNS_BASE); } @Override public void setUp() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); setLanguageOut(LanguageMode.ECMASCRIPT3); runTypeCheckAfterProcessing = true; } protected final PassFactory makePassFactory( String name, final CompilerPass pass) { return new PassFactory(name, true/* one-time pass */) { @Override protected CompilerPass create(AbstractCompiler compiler) { return pass; } }; } @Override protected CompilerPass getProcessor(final Compiler compiler) { PhaseOptimizer optimizer = new PhaseOptimizer(compiler, null); optimizer.addOneTimePass( makePassFactory("Es6RenameVariablesInParamLists", new Es6RenameVariablesInParamLists(compiler))); optimizer.addOneTimePass( makePassFactory("es6ConvertSuper", new Es6ConvertSuper(compiler))); optimizer.addOneTimePass(makePassFactory("es6ExtractClasses", new Es6ExtractClasses(compiler))); optimizer.addOneTimePass(makePassFactory("es6RewriteClass", new Es6RewriteClass(compiler))); optimizer.addOneTimePass(makePassFactory("convertEs6", new Es6ToEs3Converter(compiler))); optimizer.addOneTimePass( makePassFactory("Es6RewriteBlockScopedDeclaration", new Es6RewriteBlockScopedDeclaration(compiler))); optimizer.addOneTimePass( makePassFactory( "Es6ConvertSuperConstructorCalls", new Es6ConvertSuperConstructorCalls(compiler))); return optimizer; } @Override protected int getNumRepetitions() { return 1; } public void testObjectLiteralStringKeysWithNoValue() { test("var x = {a, b};", "var x = {a: a, b: b};"); assertThat(getLastCompiler().injected).isEmpty(); } public void testObjectLiteralMemberFunctionDef() { test( "var x = {/** @return {number} */ a() { return 0; } };", "var x = {/** @return {number} */ a: function() { return 0; } };"); assertThat(getLastCompiler().injected).isEmpty(); } public void testClassGenerator() { test( "class C { *foo() { yield 1; } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype.foo = function*() { yield 1;};")); assertThat(getLastCompiler().injected).isEmpty(); } public void testClassStatement() { test("class C { }", "/** @constructor @struct */ var C = function() {};"); test( "class C { constructor() {} }", "/** @constructor @struct */ var C = function() {};"); test( "class C { method() {}; }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype.method = function() {};")); test( "class C { constructor(a) { this.a = a; } }", "/** @constructor @struct */ var C = function(a) { this.a = a; };"); test( "class C { constructor() {} foo() {} }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype.foo = function() {};")); test( "class C { constructor() {}; foo() {}; bar() {} }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype.foo = function() {};", "C.prototype.bar = function() {};")); test( "class C { foo() {}; bar() {} }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype.foo = function() {};", "C.prototype.bar = function() {};")); test( LINE_JOINER.join( "class C {", " constructor(a) { this.a = a; }", "", " foo() { console.log(this.a); }", "", " bar() { alert(this.a); }", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var C = function(a) { this.a = a; };", "C.prototype.foo = function() { console.log(this.a); };", "C.prototype.bar = function() { alert(this.a); };")); test( LINE_JOINER.join( "if (true) {", " class Foo{}", "} else {", " class Foo{}", "}"), LINE_JOINER.join( "if (true) {", " /** @constructor @struct */", " var Foo = function() {};", "} else {", " /** @constructor @struct */", " var Foo$0 = function() {};", "}")); } public void testClassWithNgInject() { test( "class A { /** @ngInject */ constructor($scope) {} }", "/** @constructor @struct @ngInject */ var A = function($scope) {}"); test( "/** @ngInject */ class A { constructor($scope) {} }", "/** @constructor @struct @ngInject */ var A = function($scope) {}"); } public void testAnonymousSuper() { test( "f(class extends D { f() { super.g() } })", LINE_JOINER.join( "/** @constructor @struct @const", " * @extends {D}", " * @param {...?} var_args", " */", "var testcode$classdecl$var0 = function(var_args) {", " return D.apply(this,arguments) || this; ", "};", "$jscomp.inherits(testcode$classdecl$var0, D);", "testcode$classdecl$var0.prototype.f = function() { D.prototype.g.call(this); };", "f(testcode$classdecl$var0)")); } public void testExponentiationOperator() { setLanguage(LanguageMode.ECMASCRIPT_2016, LanguageMode.ECMASCRIPT5); test("2 ** 2;", "Math.pow(2,2)"); } public void testExponentiationAssignmentOperator() { setLanguage(LanguageMode.ECMASCRIPT_2016, LanguageMode.ECMASCRIPT5); test("x **= 2;", "x=Math.pow(x,2)"); } public void testNewTarget() { testError("function Foo() { new.target; }", CANNOT_CONVERT_YET); } public void testClassWithJsDoc() { test("class C { }", "/** @constructor @struct */ var C = function() { };"); test( "/** @deprecated */ class C { }", "/** @constructor @struct @deprecated */ var C = function() {};"); test( "/** @dict */ class C { }", "/** @constructor @dict */ var C = function() {};"); test( "/** @template T */ class C { }", "/** @constructor @struct @template T */ var C = function() {};"); test( "/** @final */ class C { }", "/** @constructor @struct @final */ var C = function() {};"); test( "/** @private */ class C { }", "/** @constructor @struct @private */ var C = function() {};"); } public void testInterfaceWithJsDoc() { test( LINE_JOINER.join( "/**", " * Converts Xs to Ys.", " * @interface", " */", "class Converter {", " /**", " * @param {X} x", " * @return {Y}", " */", " convert(x) {}", "}"), LINE_JOINER.join( "/**", " * Converts Xs to Ys.", " * @struct @interface", " */", "var Converter = function() { };", "", "/**", " * @param {X} x", " * @return {Y}", " */", "Converter.prototype.convert = function(x) {};")); } public void testRecordWithJsDoc() { test( LINE_JOINER.join( "/**", " * @record", " */", "class Converter {", " /**", " * @param {X} x", " * @return {Y}", " */", " convert(x) {}", "}"), LINE_JOINER.join( "/**", " * @struct @record", " */", "var Converter = function() { };", "", "/**", " * @param {X} x", " * @return {Y}", " */", "Converter.prototype.convert = function(x) {};")); } public void testCtorWithJsDoc() { test( "class C { /** @param {boolean} b */ constructor(b) {} }", LINE_JOINER.join( "/**", " * @param {boolean} b", " * @constructor", " * @struct", " */", "var C = function(b) {};")); test( "class C { /** @throws {Error} */ constructor() {} }", LINE_JOINER.join( "/**", " * @throws {Error}", " * @constructor", " * @struct", " */", "var C = function() {};")); test( "class C { /** @private */ constructor() {} }", LINE_JOINER.join( "/**", " * @private", " * @constructor", " * @struct", " */", "var C = function() {};")); test( "class C { /** @deprecated */ constructor() {} }", LINE_JOINER.join( "/**", " * @deprecated", " * @constructor", " * @struct", " */", "var C = function() {};")); test( "class C { /** @template T */ constructor() {} }", LINE_JOINER.join( "/**", " * @constructor", " * @struct", " * @template T", " */", "var C = function() {};")); test( "/** @template S */ class C { /** @template T */ constructor() {} }", LINE_JOINER.join( "/**", " * @constructor", " * @struct", " * @template S, T", " */", "var C = function() {};")); test( "/** @template S */ class C { /** @template T, U */ constructor() {} }", LINE_JOINER.join( "/**", " * @constructor", " * @struct", " * @template S, T, U", " */", "var C = function() {};")); } public void testMemberWithJsDoc() { test( "class C { /** @param {boolean} b */ foo(b) {} }", LINE_JOINER.join( "/**", " * @constructor", " * @struct", " */", "var C = function() {};", "", "/** @param {boolean} b */", "C.prototype.foo = function(b) {};")); } public void testClassStatementInsideIf() { test( "if (foo) { class C { } }", "if (foo) { /** @constructor @struct */ var C = function() {}; }"); test( "if (foo) class C {}", "if (foo) { /** @constructor @struct */ var C = function() {}; }"); } /** * Class expressions that are the RHS of a 'var' statement. */ public void testClassExpressionInVar() { test("var C = class { }", "/** @constructor @struct */ var C = function() {}"); test( "var C = class { foo() {} }", LINE_JOINER.join( "/** @constructor @struct */ var C = function() {}", "", "C.prototype.foo = function() {}")); test( "var C = class C { }", LINE_JOINER.join( "/** @constructor @struct @const */", "var testcode$classdecl$var0 = function() {};", "var C = testcode$classdecl$var0;")); test( "var C = class C { foo() {} }", LINE_JOINER.join( "/** @constructor @struct @const */", "var testcode$classdecl$var0 = function() {}", "testcode$classdecl$var0.prototype.foo = function() {};", "", "var C = testcode$classdecl$var0;")); } /** * Class expressions that are the RHS of an assignment. */ public void testClassExpressionInAssignment() { test("goog.example.C = class { }", "/** @constructor @struct */ goog.example.C = function() {}"); test( "goog.example.C = class { foo() {} }", LINE_JOINER.join( "/** @constructor @struct */ goog.example.C = function() {}", "goog.example.C.prototype.foo = function() {};")); } public void testClassExpressionInAssignment_getElem() { test( "window['MediaSource'] = class {};", LINE_JOINER.join( "/** @constructor @struct @const */", "var testcode$classdecl$var0 = function() {};", "window['MediaSource'] = testcode$classdecl$var0;")); } public void testClassExpression() { test( "var C = new (class {})();", LINE_JOINER.join( "/** @constructor @struct @const */", "var testcode$classdecl$var0=function(){};", "var C=new testcode$classdecl$var0")); test( "(condition ? obj1 : obj2).prop = class C { };", LINE_JOINER.join( "/** @constructor @struct @const */", "var testcode$classdecl$var0 = function(){};", "(condition ? obj1 : obj2).prop = testcode$classdecl$var0;")); } public void testAbstractClass() { enableTypeCheck(); test( "/** @abstract */ class Foo {} var x = new Foo();", "/** @abstract @constructor @struct */ var Foo = function() {}; var x = new Foo();", null, INSTANTIATE_ABSTRACT_CLASS); } /** * We don't bother transpiling this case because the transpiled code will be very difficult to * typecheck. */ public void testClassExpression_cannotConvert() { testError("var C = new (foo || (foo = class { }))();", CANNOT_CONVERT); } public void testExtends() { test( "class D {} class C extends D {}", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "/** @constructor @struct", " * @extends {D}", " * @param {...?} var_args", " */", "var C = function(var_args) { D.apply(this, arguments); };", "$jscomp.inherits(C, D);")); assertThat(getLastCompiler().injected).containsExactly("es6/util/inherits"); test( "class D {} class C extends D { constructor() { super(); } }", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "/** @constructor @struct @extends {D} */", "var C = function() {", " D.call(this);", "}", "$jscomp.inherits(C, D);")); test( "class D {} class C extends D { constructor(str) { super(str); } }", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "/** @constructor @struct @extends {D} */", "var C = function(str) { ", " D.call(this, str);", "}", "$jscomp.inherits(C, D);")); test( "class C extends ns.D { }", LINE_JOINER.join( "/** @constructor @struct", " * @extends {ns.D}", " * @param {...?} var_args", " */", "var C = function(var_args) {", " return ns.D.apply(this, arguments) || this;", "};", "$jscomp.inherits(C, ns.D);")); // Don't inject $jscomp.inherits() or apply() for externs testExternChanges( "class D {} class C extends D {}", "", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "/** @constructor @struct", " * @extends {D}", " * @param {...?} var_args", " */", "var C = function(var_args) {};")); } public void testExtendNonNativeError() { test( LINE_JOINER.join( "class Error {", " /** @param {string} msg */", " constructor(msg) {", " /** @const */ this.message = msg;", " }", "}", "class C extends Error {}"), // autogenerated constructor LINE_JOINER.join( "/** @constructor @struct", " * @param {string} msg", " */", "var Error = function(msg) {", " /** @const */ this.message = msg;", "};", "/** @constructor @struct", " * @extends {Error}", " * @param {...?} var_args", " */", "var C = function(var_args) { Error.apply(this, arguments); };", "$jscomp.inherits(C, Error);")); test( LINE_JOINER.join( "", "class Error {", " /** @param {string} msg */", " constructor(msg) {", " /** @const */ this.message = msg;", " }", "}", "class C extends Error {", " constructor() {", " super('C error');", // explicit super() call " }", "}"), LINE_JOINER.join( "/** @constructor @struct", " * @param {string} msg", " */", "var Error = function(msg) {", " /** @const */ this.message = msg;", "};", "/** @constructor @struct", " * @extends {Error}", " */", "var C = function() { Error.call(this, 'C error'); };", "$jscomp.inherits(C, Error);")); } public void testExtendNativeError() { test( "class C extends Error {}", // autogenerated constructor LINE_JOINER.join( "/** @constructor @struct", " * @extends {Error}", " * @param {...?} var_args", " */", "var C = function(var_args) {", " var $jscomp$tmp$error;", " $jscomp$tmp$error = Error.apply(this, arguments),", " this.message = $jscomp$tmp$error.message,", " ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack),", " this;", "};", "$jscomp.inherits(C, Error);")); test( LINE_JOINER.join( "", "class C extends Error {", " constructor() {", " var self = super('C error') || this;", // explicit super() call in an expression " }", "}"), LINE_JOINER.join( "/** @constructor @struct", " * @extends {Error}", " */", "var C = function() {", " var $jscomp$tmp$error;", " var self =", " ($jscomp$tmp$error = Error.call(this, 'C error'),", " this.message = $jscomp$tmp$error.message,", " ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack),", " this)", " || this;", "};", "$jscomp.inherits(C, Error);")); } public void testInvalidExtends() { testError("class C extends foo() {}", DYNAMIC_EXTENDS_TYPE); testError("class C extends function(){} {}", DYNAMIC_EXTENDS_TYPE); testError("class A {}; class B {}; class C extends (foo ? A : B) {}", DYNAMIC_EXTENDS_TYPE); } public void testExtendsInterface() { test( LINE_JOINER.join( "/** @interface */", "class D {", " f() {}", "}", "/** @interface */", "class C extends D {", " g() {}", "}"), LINE_JOINER.join( "/** @struct @interface */", "var D = function() {};", "D.prototype.f = function() {};", "/**", " * @struct @interface", " * @param {...?} var_args", " * @extends{D} */", "var C = function(var_args) {};", "C.prototype.g = function() {};")); } public void testExtendsRecord() { test( LINE_JOINER.join( "/** @record */", "class D {", " f() {}", "}", "/** @record */", "class C extends D {", " g() {}", "}"), LINE_JOINER.join( "/** @struct @record */", "var D = function() {};", "D.prototype.f = function() {};", "/**", " * @struct @record", " * @param {...?} var_args", " * @extends{D} */", "var C = function(var_args) {};", "C.prototype.g = function() {};")); } public void testImplementsInterface() { test( LINE_JOINER.join( "/** @interface */", "class D {", " f() {}", "}", "/** @implements {D} */", "class C {", " f() {console.log('hi');}", "}"), LINE_JOINER.join( "/** @struct @interface */", "var D = function() {};", "D.prototype.f = function() {};", "/** @constructor @struct @implements{D} */", "var C = function() {};", "C.prototype.f = function() {console.log('hi');};")); } public void testSuperCallInExterns() { // Drop super() calls in externs. testExternChanges( LINE_JOINER.join( "class D {}", "class C extends D {", " constructor() {", " super();", " }", "}"), "", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "/** @constructor @struct", " * @extends {D}", " */", "var C = function() {};")); } public void testSuperCall() { test( "class D {} class C extends D { constructor() { super(); } }", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "/** @constructor @struct @extends {D} */", "var C = function() {", " D.call(this);", "}", "$jscomp.inherits(C, D);")); test( "class D {} class C extends D { constructor(str) { super(str); } }", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {}", "/** @constructor @struct @extends {D} */", "var C = function(str) {", " D.call(this,str);", "}", "$jscomp.inherits(C, D);")); test( "class D {} class C extends D { constructor(str, n) { super(str); this.n = n; } }", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {}", "/** @constructor @struct @extends {D} */", "var C = function(str, n) {", " D.call(this,str);", " this.n = n;", "}", "$jscomp.inherits(C, D);")); test( LINE_JOINER.join( "class D {}", "class C extends D {", " constructor() { }", " foo() { return super.foo(); }", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {}", "/** @constructor @struct @extends {D} */", "var C = function() { }", "$jscomp.inherits(C, D);", "C.prototype.foo = function() {", " return D.prototype.foo.call(this);", "}")); test( LINE_JOINER.join( "class D {}", "class C extends D {", " constructor() {}", " foo(bar) { return super.foo(bar); }", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {}", "/** @constructor @struct @extends {D} */", "var C = function() {};", "$jscomp.inherits(C, D);", "C.prototype.foo = function(bar) {", " return D.prototype.foo.call(this, bar);", "}")); test( "class C { method() { class D extends C { constructor() { super(); }}}}", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {}", "C.prototype.method = function() {", " /** @constructor @struct @extends{C} */", " var D = function() {", " C.call(this);", " }", " $jscomp.inherits(D, C);", "};")); test( "class D {} class C extends D { constructor() {}; f() {super();} }", LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "/** @constructor @struct @extends {D} */", "var C = function() {}", "$jscomp.inherits(C, D);", "C.prototype.f = function() {", " D.prototype.f.call(this);", "}")); } public void testSuperKnownNotToChangeThis() { test( LINE_JOINER.join( "class D {", " /** @param {string} str */", " constructor(str) {", " this.str = str;", " return;", // Empty return should not trigger this-changing behavior. " }", "}", "class C extends D {", " /**", " * @param {string} str", " * @param {number} n", " */", " constructor(str, n) {", // This is nuts, but confirms that super() used in an expression works. " super(str).n = n;", // Also confirm that an existing empty return is handled correctly. " return;", " }", "}"), LINE_JOINER.join( "/**", " * @constructor @struct", " * @param {string} str", " */", "var D = function(str) {", " this.str = str;", " return;", "}", "/**", " * @constructor @struct @extends {D}", " * @param {string} str", " * @param {number} n", " */", "var C = function(str, n) {", " (D.call(this,str), this).n = n;", // super() returns `this`. " return;", "}", "$jscomp.inherits(C, D);")); } public void testSuperMightChangeThis() { // Class D is unknown, so we must assume its constructor could change `this`. test( LINE_JOINER.join( "class C extends D {", " constructor(str, n) {", // This is nuts, but confirms that super() used in an expression works. " super(str).n = n;", // Also confirm that an existing empty return is handled correctly. " return;", " }", "}"), LINE_JOINER.join( "/** @constructor @struct @extends {D} */", "var C = function(str, n) {", " var $jscomp$super$this;", " ($jscomp$super$this = D.call(this,str) || this).n = n;", " return $jscomp$super$this;", // Duplicate because of existing return statement. " return $jscomp$super$this;", "}", "$jscomp.inherits(C, D);")); } public void testAlternativeSuperCalls() { test( LINE_JOINER.join( "class D {", " /** @param {string} name */", " constructor(name) {", " this.name = name;", " }", "}", "class C extends D {", " /** @param {string} str", " * @param {number} n */", " constructor(str, n) {", " if (n >= 0) {", " super('positive: ' + str);", " } else {", " super('negative: ' + str);", " }", " this.n = n;", " }", "}"), LINE_JOINER.join( "/** @constructor @struct", " * @param {string} name */", "var D = function(name) {", " this.name = name;", "}", "/** @constructor @struct @extends {D}", " * @param {string} str", " * @param {number} n */", "var C = function(str, n) {", " if (n >= 0) {", " D.call(this, 'positive: ' + str);", " } else {", " D.call(this, 'negative: ' + str);", " }", " this.n = n;", "}", "$jscomp.inherits(C, D);")); // Class being extended is unknown, so we must assume super() could change the value of `this`. test( LINE_JOINER.join( "class C extends D {", " /** @param {string} str", " * @param {number} n */", " constructor(str, n) {", " if (n >= 0) {", " super('positive: ' + str);", " } else {", " super('negative: ' + str);", " }", " this.n = n;", " }", "}"), LINE_JOINER.join( "/** @constructor @struct @extends {D}", " * @param {string} str", " * @param {number} n */", "var C = function(str, n) {", " var $jscomp$super$this;", " if (n >= 0) {", " $jscomp$super$this = D.call(this, 'positive: ' + str) || this;", " } else {", " $jscomp$super$this = D.call(this, 'negative: ' + str) || this;", " }", " $jscomp$super$this.n = n;", " return $jscomp$super$this;", "}", "$jscomp.inherits(C, D);")); } public void testComputedSuper() { testError( LINE_JOINER.join( "class Foo {", " ['m']() { return 1; }", "}", "", "class Bar extends Foo {", " ['m']() {", " return super['m']() + 1;", " }", "}"), CANNOT_CONVERT_YET); } public void testSuperMethodInGetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( LINE_JOINER.join( "class Base {", " method() {", " return 5;", " }", "}", "", "class Subclass extends Base {", " constructor() {", " super();", " }", "", " get x() {", " return super.method();", " }", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var Base = function() {};", "Base.prototype.method = function() { return 5; };", "", "/** @constructor @struct @extends {Base} */", "var Subclass = function() { Base.call(this); };", "", "/** @type {?} */", "Subclass.prototype.x;", "$jscomp.inherits(Subclass, Base);", "$jscomp.global.Object.defineProperties(Subclass.prototype, {", " x: {", " configurable:true,", " enumerable:true,", " /** @this {Subclass} */", " get: function() { return Base.prototype.method.call(this); },", " }", "});")); } public void testSuperMethodInSetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( LINE_JOINER.join( "class Base {", " method() {", " this._x = 5;", " }", "}", "", "class Subclass extends Base {", " constructor() {", " super();", " }", "", " set x(value) {", " super.method();", " }", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var Base = function() {};", "Base.prototype.method = function() { this._x = 5; };", "", "/** @constructor @struct @extends {Base} */", "var Subclass = function() { Base.call(this); };", "", "/** @type {?} */", "Subclass.prototype.x;", "$jscomp.inherits(Subclass, Base);", "$jscomp.global.Object.defineProperties(Subclass.prototype, {", " x: {", " configurable:true,", " enumerable:true,", " /** @this {Subclass} */", " set: function(value) { Base.prototype.method.call(this); },", " }", "});")); } public void testExtendFunction() { // Function and other native classes cannot be correctly extended in transpiled form. // Test both explicit and automatically generated constructors. testError( LINE_JOINER.join( "class FooFunction extends Function {", " /** @param {string} msg */", " constructor(msg) {", " super();", " this.msg = msg;", " }", "}"), CANNOT_CONVERT); testError( "class FooFunction extends Function {}", CANNOT_CONVERT); } public void testExtendObject() { // Object can be correctly extended in transpiled form, but we don't want or need to call // the `Object()` constructor in place of `super()`. Just replace `super()` with `this` instead. // Test both explicit and automatically generated constructors. test( LINE_JOINER.join( "class Foo extends Object {", " /** @param {string} msg */", " constructor(msg) {", " super();", " this.msg = msg;", " }", "}"), LINE_JOINER.join( "/**", " * @constructor @struct @extends {Object}", " * @param {string} msg", " */", "var Foo = function(msg) {", " this;", // super() replaced with its return value " this.msg = msg;", "};", "$jscomp.inherits(Foo, Object);")); test( "class Foo extends Object {}", LINE_JOINER.join( "/**", " * @constructor @struct @extends {Object}", " * @param {...?} var_args", " */", "var Foo = function(var_args) {", " this;", // super.apply(this, arguments) replaced with its return value "};", "$jscomp.inherits(Foo, Object);")); } public void testExtendNonNativeObject() { // No special handling when Object is redefined. test( LINE_JOINER.join( "class Object {}", "class Foo extends Object {", " /** @param {string} msg */", " constructor(msg) {", " super();", " this.msg = msg;", " }", "}"), LINE_JOINER.join( "/**", " * @constructor @struct", " */", "var Object = function() {", "};", "/**", " * @constructor @struct @extends {Object}", " * @param {string} msg", " */", "var Foo = function(msg) {", " Object.call(this);", " this.msg = msg;", "};", "$jscomp.inherits(Foo, Object);")); test( LINE_JOINER.join( "class Object {}", "class Foo extends Object {}"), // autogenerated constructor LINE_JOINER.join( "/**", " * @constructor @struct", " */", "var Object = function() {", "};", "/**", " * @constructor @struct @extends {Object}", " * @param {...?} var_args", " */", "var Foo = function(var_args) {", " Object.apply(this, arguments);", // all arguments passed on to super() "};", "$jscomp.inherits(Foo, Object);")); } public void testMultiNameClass() { test( "var F = class G {}", LINE_JOINER.join( "/** @constructor @struct @const */", "var testcode$classdecl$var0 = function(){};", "var F = testcode$classdecl$var0;")); test( "F = class G {}", LINE_JOINER.join( "/** @constructor @struct @const */", "var testcode$classdecl$var0 = function(){};", "F = testcode$classdecl$var0;")); } public void testClassNested() { test( "class C { f() { class D {} } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype.f = function() {", " /** @constructor @struct */", " var D = function() {}", "};")); test( "class C { f() { class D extends C {} } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype.f = function() {", " /**", " * @constructor @struct", " * @param {...?} var_args", " * @extends{C} */", " var D = function(var_args) {", " C.apply(this, arguments); ", " };", " $jscomp.inherits(D, C);", "};")); } public void testSuperGet() { testError("class D {} class C extends D { f() {var i = super.c;} }", CANNOT_CONVERT_YET); testError("class D {} class C extends D { static f() {var i = super.c;} }", CANNOT_CONVERT_YET); testError("class D {} class C extends D { f() {var i; i = super[s];} }", CANNOT_CONVERT_YET); testError("class D {} class C extends D { f() {return super.s;} }", CANNOT_CONVERT_YET); testError("class D {} class C extends D { f() {m(super.s);} }", CANNOT_CONVERT_YET); testError( "class D {} class C extends D { foo() { return super.m.foo(); } }", CANNOT_CONVERT_YET); testError( "class D {} class C extends D { static foo() { return super.m.foo(); } }", CANNOT_CONVERT_YET); } public void testSuperNew() { testError("class D {} class C extends D { f() {var s = new super;} }", CANNOT_CONVERT_YET); testError("class D {} class C extends D { f(str) {var s = new super(str);} }", CANNOT_CONVERT_YET); } public void testSuperSpread() { test( LINE_JOINER.join( "class D {}", "class C extends D {", " constructor(args) {", " super(...args)", " }", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var D = function(){};", "/** @constructor @struct @extends {D} */", "var C=function(args) {", " D.apply(this, [].concat($jscomp.arrayFromIterable(args)));", "};", "$jscomp.inherits(C,D);")); assertThat(getLastCompiler().injected) .containsExactly("es6/util/arrayfromiterable", "es6/util/inherits"); } public void testSuperCallNonConstructor() { test( "class S extends B { static f() { super(); } }", LINE_JOINER.join( "/** @constructor @struct", " * @extends {B}", " * @param {...?} var_args", " */", "var S = function(var_args) { return B.apply(this, arguments) || this; };", "$jscomp.inherits(S, B);", "/** @this {?} */", "S.f=function() { B.f.call(this) }")); test( "class S extends B { f() { super(); } }", LINE_JOINER.join( "/** @constructor @struct", " * @extends {B}", " * @param {...?} var_args", " */", "var S = function(var_args) { return B.apply(this, arguments) || this; };", "$jscomp.inherits(S, B);", "S.prototype.f=function() {", " B.prototype.f.call(this);", "}")); } public void testStaticThis() { test( "class F { static f() { return this; } }", LINE_JOINER.join( "/** @constructor @struct */ var F = function() {}", "/** @this {?} */ F.f = function() { return this; };")); } public void testStaticMethods() { test("class C { static foo() {} }", "/** @constructor @struct */ var C = function() {}; C.foo = function() {};"); test("class C { static foo() {}; foo() {} }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "", "C.foo = function() {};", "", "C.prototype.foo = function() {};")); test("class C { static foo() {}; bar() { C.foo(); } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "", "C.foo = function() {};", "", "C.prototype.bar = function() { C.foo(); };")); } public void testStaticInheritance() { test( LINE_JOINER.join( "class D {", " static f() {}", "}", "class C extends D { constructor() {} }", "C.f();"), LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "D.f = function () {};", "/** @constructor @struct @extends{D} */", "var C = function() {};", "$jscomp.inherits(C, D);", "C.f();")); test( LINE_JOINER.join( "class D {", " static f() {}", "}", "class C extends D {", " constructor() {}", " f() {}", "}", "C.f();"), LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "D.f = function() {};", "/** @constructor @struct @extends{D} */", "var C = function() { };", "$jscomp.inherits(C, D);", "C.prototype.f = function() {};", "C.f();")); test( LINE_JOINER.join( "class D {", " static f() {}", "}", "class C extends D {", " constructor() {}", " static f() {}", " g() {}", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var D = function() {};", "D.f = function() {};", "/** @constructor @struct @extends{D} */", "var C = function() { };", "$jscomp.inherits(C, D);", "C.f = function() {};", "C.prototype.g = function() {};")); } public void testInheritFromExterns() { test( LINE_JOINER.join( "/** @constructor */ function ExternsClass() {}", "ExternsClass.m = function() {};"), "class CodeClass extends ExternsClass {}", LINE_JOINER.join( "/** @constructor @struct", " * @extends {ExternsClass}", " * @param {...?} var_args", " */", "var CodeClass = function(var_args) {", " return ExternsClass.apply(this,arguments) || this;", "};", "$jscomp.inherits(CodeClass,ExternsClass)"), null, null); } public void testMockingInFunction() { // Classes cannot be reassigned in function scope. testError("function f() { class C {} C = function() {};}", CLASS_REASSIGNMENT); } // Make sure we don't crash on this code. // https://github.com/google/closure-compiler/issues/752 public void testGithub752() { test( "function f() { var a = b = class {};}", LINE_JOINER.join( "function f() {", " /** @constructor @struct @const */", " var testcode$classdecl$var0 = function() {};", " var a = b = testcode$classdecl$var0;", "}")); test( "var ns = {}; function f() { var self = ns.Child = class {};}", LINE_JOINER.join( "var ns = {};", "function f() {", " /** @constructor @struct @const */", " var testcode$classdecl$var0 = function() {};", " var self = ns.Child = testcode$classdecl$var0", "}")); } public void testInvalidClassUse() { enableTypeCheck(); test( LINE_JOINER.join( "/** @constructor @struct */", "function Foo() {}", "Foo.prototype.f = function() {};", "class Sub extends Foo {}", "(new Sub).f();"), LINE_JOINER.join( "/** @constructor @struct */", "function Foo() {}", "Foo.prototype.f = function() {};", "/**", " * @constructor @struct", " * @extends {Foo}", " * @param {...?} var_args", " */", "var Sub=function(var_args) { Foo.apply(this, arguments); }", "$jscomp.inherits(Sub, Foo);", "(new Sub).f();")); testWarning( LINE_JOINER.join( "/** @constructor @struct */", "function Foo() {}", "Foo.f = function() {};", "class Sub extends Foo {}", "Sub.f();"), TypeCheck.INEXISTENT_PROPERTY); test( LINE_JOINER.join( "/** @constructor */", "function Foo() {}", "Foo.f = function() {};", "class Sub extends Foo {}"), LINE_JOINER.join( "/** @constructor */", "function Foo() {}", "Foo.f = function() {};", "/** @constructor @struct", " * @extends {Foo}", " * @param {...?} var_args", " */", "var Sub = function(var_args) { Foo.apply(this, arguments); };", "$jscomp.inherits(Sub, Foo);")); } /** * Getters and setters are supported, both in object literals and in classes, but only * if the output language is ES5. */ public void testEs5GettersAndSettersClasses() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "class C { get value() { return 0; } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {?} */", "C.prototype.value;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " get: function() {", " return 0;", " }", " }", "});")); test( "class C { set value(val) { this.internalVal = val; } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {?} */", "C.prototype.value;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " set: function(val) {", " this.internalVal = val;", " }", " }", "});")); test( LINE_JOINER.join( "class C {", " set value(val) {", " this.internalVal = val;", " }", " get value() {", " return this.internalVal;", " }", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {?} */", "C.prototype.value;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " set: function(val) {", " this.internalVal = val;", " },", " /** @this {C} */", " get: function() {", " return this.internalVal;", " }", " }", "});")); test( LINE_JOINER.join( "class C {", " get alwaysTwo() {", " return 2;", " }", "", " get alwaysThree() {", " return 3;", " }", "}"), LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {?} */", "C.prototype.alwaysTwo;", "/** @type {?} */", "C.prototype.alwaysThree;", "$jscomp.global.Object.defineProperties(C.prototype, {", " alwaysTwo: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " get: function() {", " return 2;", " }", " },", " alwaysThree: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " get: function() {", " return 3;", " }", " },", "});")); } public void testEs5GettersAndSettersOnClassesWithClassSideInheritance() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "class C { static get value() {} } class D extends C { static get value() {} }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @nocollapse @type {?} */", "C.value;", "$jscomp.global.Object.defineProperties(C, {", " value: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " get: function() {}", " }", "});", "/** @constructor @struct", " * @extends {C}", " * @param {...?} var_args", " */", "var D = function(var_args) {", " C.apply(this,arguments); ", "};", "/** @nocollapse @type {?} */", "D.value;", "$jscomp.inherits(D, C);", "$jscomp.global.Object.defineProperties(D, {", " value: {", " configurable: true,", " enumerable: true,", " /** @this {D} */", " get: function() {}", " }", "});")); } /** * Check that the types from the getter/setter are copied to the declaration on the prototype. */ public void testEs5GettersAndSettersClassesWithTypes() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "class C { /** @return {number} */ get value() { return 0; } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {number} */", "C.prototype.value;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /**", " * @return {number}", " * @this {C}", " */", " get: function() {", " return 0;", " }", " }", "});")); test( "class C { /** @param {string} v */ set value(v) { } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {string} */", "C.prototype.value;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /**", " * @this {C}", " * @param {string} v", " */", " set: function(v) {}", " }", "});")); testError( LINE_JOINER.join( "class C {", " /** @return {string} */", " get value() { }", "", " /** @param {number} v */", " set value(v) { }", "}"), CONFLICTING_GETTER_SETTER_TYPE); } public void testClassEs5GetterSetterIncorrectTypes() { enableTypeCheck(); setLanguageOut(LanguageMode.ECMASCRIPT5); // Using @type instead of @return on a getter. test( "class C { /** @type {string} */ get value() { } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {?} */", "C.prototype.value;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /** @type {string} */", " get: function() {}", " }", "});"), null, TypeValidator.TYPE_MISMATCH_WARNING); // Using @type instead of @param on a setter. test( "class C { /** @type {string} */ set value(v) { } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {?} */", "C.prototype.value;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /** @type {string} */", " set: function(v) {}", " }", "});"), null, TypeValidator.TYPE_MISMATCH_WARNING); } /** * @bug 20536614 */ public void testStaticGetterSetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "class C { static get foo() {} }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @nocollapse @type {?} */", "C.foo;", "$jscomp.global.Object.defineProperties(C, {", " foo: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " get: function() {}", " }", "})")); test( LINE_JOINER.join("class C { static get foo() {} }", "class Sub extends C {}"), LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @nocollapse @type {?} */", "C.foo;", "$jscomp.global.Object.defineProperties(C, {", " foo: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " get: function() {}", " }", "})", "", "/** @constructor @struct", " * @extends {C}", " * @param {...?} var_args", " */", "var Sub = function(var_args) {", " C.apply(this, arguments);", "};", "$jscomp.inherits(Sub, C)")); } public void testStaticSetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "class C { static set foo(x) {} }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @nocollapse @type {?} */", "C.foo;", "$jscomp.global.Object.defineProperties(C, {", " foo: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " set: function(x) {}", " }", "});")); } public void testInitSymbol() { test("let a = alert(Symbol.thimble);", "$jscomp.initSymbol(); var a = alert(Symbol.thimble)"); assertThat(getLastCompiler().injected).containsExactly("es6/symbol"); test( LINE_JOINER.join( "function f() {", " let x = 1;", " let y = Symbol('nimble');", "}"), LINE_JOINER.join( "function f() {", " var x = 1;", " $jscomp.initSymbol();", " var y = Symbol('nimble');", "}")); test( LINE_JOINER.join( "function f() {", " if (true) {", " let Symbol = function() {};", " }", " alert(Symbol.ism)", "}"), LINE_JOINER.join( "function f() {", " if (true) {", " var Symbol$0 = function() {};", " }", " $jscomp.initSymbol();", " alert(Symbol.ism)", "}")); // No $jscomp.initSymbol because "Symbol" doesn't refer to the global Symbol function here. test( LINE_JOINER.join( "function f() {", " if (true) {", " let Symbol = function() {};", " alert(Symbol.ism)", " }", "}"), LINE_JOINER.join( "function f() {", " if (true) {", " var Symbol = function() {};", " alert(Symbol.ism)", " }", "}")); // No $jscomp.initSymbol in externs testExternChanges( "alert(Symbol.thimble);", "", "alert(Symbol.thimble)"); } public void testInitSymbolIterator() { test( "var x = {[Symbol.iterator]: function() { return this; }};", LINE_JOINER.join( "$jscomp.initSymbol();", "$jscomp.initSymbolIterator();", "var $jscomp$compprop0 = {};", "var x = ($jscomp$compprop0[Symbol.iterator] = function() {return this;},", " $jscomp$compprop0)")); } public void testClassComputedPropGetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "/** @unrestricted */ class C { /** @return {number} */ get [foo]() { return 4; }}", LINE_JOINER.join( "/** @constructor @unrestricted */", "var C = function() {};", "/** @type {number} */", "C.prototype[foo];", "var $jscomp$compprop0 = {};", "$jscomp.global.Object.defineProperties(", " C.prototype,", " ($jscomp$compprop0[foo] = {", " configurable:true,", " enumerable:true,", " /** @this {C} */", " get: function() { return 4; }", " }, $jscomp$compprop0));")); assertThat(getLastCompiler().injected).containsExactly("util/global"); testError("class C { get [add + expr]() {} }", CANNOT_CONVERT); } public void testClassComputedPropSetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "/** @unrestricted */ class C { /** @param {string} val */ set [foo](val) {}}", LINE_JOINER.join( "/** @constructor @unrestricted */", "var C = function() {};", "/** @type {string} */", "C.prototype[foo];", "var $jscomp$compprop0={};", "$jscomp.global.Object.defineProperties(", " C.prototype,", " ($jscomp$compprop0[foo] = {", " configurable:true,", " enumerable:true,", " /** @this {C} */", " set: function(val) {}", " }, $jscomp$compprop0));")); testError("class C { get [sub - expr]() {} }", CANNOT_CONVERT); } public void testClassStaticComputedProps() { setLanguageOut(LanguageMode.ECMASCRIPT5); testError("/** @unrestricted */ class C { static set [foo](val) {}}", CANNOT_CONVERT_YET); testError("/** @unrestricted */ class C { static get [foo]() {}}", CANNOT_CONVERT_YET); } public void testClassComputedPropGetterAndSetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( LINE_JOINER.join( "/** @unrestricted */", "class C {", " /** @return {boolean} */", " get [foo]() {}", " /** @param {boolean} val */", " set [foo](val) {}", "}"), LINE_JOINER.join( "/** @constructor @unrestricted */", "var C = function() {};", "/** @type {boolean} */", "C.prototype[foo];", "var $jscomp$compprop0={};", "$jscomp.global.Object.defineProperties(", " C.prototype,", " ($jscomp$compprop0[foo] = {", " configurable:true,", " enumerable:true,", " /** @this {C} */", " get: function() {},", " /** @this {C} */", " set: function(val) {},", " }, $jscomp$compprop0));")); testError( LINE_JOINER.join( "/** @unrestricted */", "class C {", " /** @return {boolean} */", " get [foo]() {}", " /** @param {string} val */", " set [foo](val) {}", "}"), CONFLICTING_GETTER_SETTER_TYPE); } /** * ES5 getters and setters should report an error if the languageOut is ES3. */ public void testEs5GettersAndSetters_es3() { testError("let x = { get y() {} };", CANNOT_CONVERT); testError("let x = { set y(value) {} };", CANNOT_CONVERT); } /** * ES5 getters and setters on object literals should be left alone if the languageOut is ES5. */ public void testEs5GettersAndSettersObjLit_es5() { setLanguageOut(LanguageMode.ECMASCRIPT5); testSame("var x = { get y() {} };"); testSame("var x = { set y(value) {} };"); } public void testRestParameter() { test("function f(...zero) { return zero; }", LINE_JOINER.join( "function f(zero) {", " var $jscomp$restParams = [];", " for (var $jscomp$restIndex = 0; $jscomp$restIndex < arguments.length;", " ++$jscomp$restIndex) {", " $jscomp$restParams[$jscomp$restIndex - 0] = arguments[$jscomp$restIndex];", " }", " {", " var zero$0 = $jscomp$restParams;", " return zero$0;", " }", "}")); test("function f(zero, ...one) {}", "function f(zero, one) {}"); test("function f(zero, one, ...two) {}", "function f(zero, one, two) {}"); // Function-level and inline type test("/** @param {...number} zero */ function f(...zero) {}", "/** @param {...number} zero */ function f(zero) {}"); test("function f(/** ...number */ ...zero) {}", "function f(/** ...number */ zero) {}"); // Make sure we get type checking inside the function for the rest parameters. test( "/** @param {...number} two */ function f(zero, one, ...two) { return two; }", LINE_JOINER.join( "/** @param {...number} two */ function f(zero, one, two) {", " var $jscomp$restParams = [];", " for (var $jscomp$restIndex = 2; $jscomp$restIndex < arguments.length;", " ++$jscomp$restIndex) {", " $jscomp$restParams[$jscomp$restIndex - 2] = arguments[$jscomp$restIndex];", " }", " {", " var /** !Array<number> */ two$0 = $jscomp$restParams;", " return two$0;", " }", "}")); test( "/** @param {...number} two */ var f = function(zero, one, ...two) { return two; }", LINE_JOINER.join( "/** @param {...number} two */ var f = function(zero, one, two) {", " var $jscomp$restParams = [];", " for (var $jscomp$restIndex = 2; $jscomp$restIndex < arguments.length;", " ++$jscomp$restIndex) {", " $jscomp$restParams[$jscomp$restIndex - 2] = arguments[$jscomp$restIndex];", " }", " {", " var /** !Array<number> */ two$0 = $jscomp$restParams;", " return two$0;", " }", "}")); test( "/** @param {...number} two */ ns.f = function(zero, one, ...two) { return two; }", LINE_JOINER.join( "/** @param {...number} two */ ns.f = function(zero, one, two) {", " var $jscomp$restParams = [];", " for (var $jscomp$restIndex = 2; $jscomp$restIndex < arguments.length;", " ++$jscomp$restIndex) {", " $jscomp$restParams[$jscomp$restIndex - 2] = arguments[$jscomp$restIndex];", " }", " {", " var /** !Array<number> */ two$0 = $jscomp$restParams;", " return two$0;", " }", "}")); // Warn on /** number */ testWarning("function f(/** number */ ...zero) {}", Es6ToEs3Converter.BAD_REST_PARAMETER_ANNOTATION); testWarning("/** @param {number} zero */ function f(...zero) {}", Es6ToEs3Converter.BAD_REST_PARAMETER_ANNOTATION); } public void testDefaultAndRestParameters() { test( "function f(zero, one, ...two) {one = (one === undefined) ? 1 : one;}", LINE_JOINER.join( "function f(zero, one, two) {", " var $jscomp$restParams = [];", " for (var $jscomp$restIndex = 2; $jscomp$restIndex < arguments.length;", " ++$jscomp$restIndex) {", " $jscomp$restParams[$jscomp$restIndex - 2] = arguments[$jscomp$restIndex];", " }", " {", " var two$0 = $jscomp$restParams;", " one = (one === undefined) ? 1 : one;", " }", "}")); } public void testForOf() { // With array literal and declaring new bound variable. test( "for (var i of [1,2,3]) { console.log(i); }", LINE_JOINER.join( "for (var $jscomp$iter$0 = $jscomp.makeIterator([1,2,3]),", " $jscomp$key$i = $jscomp$iter$0.next();", " !$jscomp$key$i.done; $jscomp$key$i = $jscomp$iter$0.next()) {", " var i = $jscomp$key$i.value;", " {", " console.log(i);", " }", "}")); assertThat(getLastCompiler().injected).containsExactly("es6/util/makeiterator"); // With simple assign instead of var declaration in bound variable. test( "for (i of [1,2,3]) { console.log(i); }", LINE_JOINER.join( "for (var $jscomp$iter$0 = $jscomp.makeIterator([1,2,3]),", " $jscomp$key$i = $jscomp$iter$0.next();", " !$jscomp$key$i.done; $jscomp$key$i = $jscomp$iter$0.next()) {", " i = $jscomp$key$i.value;", " {", " console.log(i);", " }", "}")); // With name instead of array literal. test( "for (var i of arr) { console.log(i); }", LINE_JOINER.join( "for (var $jscomp$iter$0 = $jscomp.makeIterator(arr),", " $jscomp$key$i = $jscomp$iter$0.next();", " !$jscomp$key$i.done; $jscomp$key$i = $jscomp$iter$0.next()) {", " var i = $jscomp$key$i.value;", " {", " console.log(i);", " }", "}")); // With empty loop body. test( "for (var i of [1,2,3]);", LINE_JOINER.join( "for (var $jscomp$iter$0 = $jscomp.makeIterator([1,2,3]),", " $jscomp$key$i = $jscomp$iter$0.next();", " !$jscomp$key$i.done; $jscomp$key$i = $jscomp$iter$0.next()) {", " var i = $jscomp$key$i.value;", " {}", "}")); // With no block in for loop body. test( "for (var i of [1,2,3]) console.log(i);", LINE_JOINER.join( "for (var $jscomp$iter$0 = $jscomp.makeIterator([1,2,3]),", " $jscomp$key$i = $jscomp$iter$0.next();", " !$jscomp$key$i.done; $jscomp$key$i = $jscomp$iter$0.next()) {", " var i = $jscomp$key$i.value;", " {", " console.log(i);", " }", "}")); // Iteration var shadows an outer var () test( "var i = 'outer'; for (let i of [1, 2, 3]) { alert(i); } alert(i);", LINE_JOINER.join( "var i = 'outer';", "for (var $jscomp$iter$0 = $jscomp.makeIterator([1,2,3]),", " $jscomp$key$i = $jscomp$iter$0.next();", " !$jscomp$key$i.done; $jscomp$key$i = $jscomp$iter$0.next()) {", " var i$1 = $jscomp$key$i.value;", " {", " alert(i$1);", " }", "}", "alert(i);")); } public void testForOfRedeclaredVar() { test( LINE_JOINER.join( "for (let x of []) {", " let x = 0;", "}"), LINE_JOINER.join( "for(var $jscomp$iter$0=$jscomp.makeIterator([]),", " $jscomp$key$x=$jscomp$iter$0.next();", " !$jscomp$key$x.done;$jscomp$key$x=$jscomp$iter$0.next()) {", " var x = $jscomp$key$x.value;", " {", " var x$1 = 0;", " }", "}")); } public void testForOfJSDoc() { test( "for (/** @type {string} */ let x of []) {}", LINE_JOINER.join( "for(var $jscomp$iter$0=$jscomp.makeIterator([]),", " $jscomp$key$x=$jscomp$iter$0.next();", " !$jscomp$key$x.done;$jscomp$key$x=$jscomp$iter$0.next()) {", " /** @type {string} */", " var x = $jscomp$key$x.value;", " {}", "}")); test( "for (/** @type {string} */ x of []) {}", LINE_JOINER.join( "for(var $jscomp$iter$0=$jscomp.makeIterator([]),", " $jscomp$key$x=$jscomp$iter$0.next();", " !$jscomp$key$x.done;$jscomp$key$x=$jscomp$iter$0.next()) {", " /** @type {string} */", " x = $jscomp$key$x.value;", " {}", "}")); } public void testSpreadArray() { test("var arr = [1, 2, ...mid, 4, 5];", "var arr = [].concat([1, 2], $jscomp.arrayFromIterable(mid), [4, 5]);"); assertThat(getLastCompiler().injected).containsExactly("es6/util/arrayfromiterable"); test("var arr = [1, 2, ...mid(), 4, 5];", "var arr = [].concat([1, 2], $jscomp.arrayFromIterable(mid()), [4, 5]);"); test("var arr = [1, 2, ...mid, ...mid2(), 4, 5];", "var arr = [].concat([1, 2], $jscomp.arrayFromIterable(mid)," + " $jscomp.arrayFromIterable(mid2()), [4, 5]);"); test("var arr = [...mid()];", "var arr = [].concat($jscomp.arrayFromIterable(mid()));"); test("f(1, [2, ...mid, 4], 5);", "f(1, [].concat([2], $jscomp.arrayFromIterable(mid), [4]), 5);"); test( "function f() { return [...arguments]; };", LINE_JOINER.join( "function f() {", " return [].concat($jscomp.arrayFromIterable(arguments));", "};")); test( "function f() { return [...arguments, 2]; };", LINE_JOINER.join( "function f() {", " return [].concat($jscomp.arrayFromIterable(arguments), [2]);", "};")); } public void testArgumentsEscaped() { test( LINE_JOINER.join( "function g(x) {", " return [...x];", "}", "function f() {", " return g(arguments);", "}"), LINE_JOINER.join( "function g(x) {", " return [].concat($jscomp.arrayFromIterable(x));", "}", "function f() {", " return g(arguments);", "}")); } public void testForOfOnNonIterable() { enableTypeCheck(); testWarning( LINE_JOINER.join( "var arrayLike = {", " 0: 'x',", " 1: 'y',", " length: 2,", "};", "for (var x of arrayLike) {}"), TypeValidator.TYPE_MISMATCH_WARNING); } public void testSpreadCall() { test("f(...arr);", "f.apply(null, [].concat($jscomp.arrayFromIterable(arr)));"); test("f(0, ...g());", "f.apply(null, [].concat([0], $jscomp.arrayFromIterable(g())));"); test("f(...arr, 1);", "f.apply(null, [].concat($jscomp.arrayFromIterable(arr), [1]));"); test("f(0, ...g(), 2);", "f.apply(null, [].concat([0], $jscomp.arrayFromIterable(g()), [2]));"); test("obj.m(...arr);", "obj.m.apply(obj, [].concat($jscomp.arrayFromIterable(arr)));"); test("x.y.z.m(...arr);", "x.y.z.m.apply(x.y.z, [].concat($jscomp.arrayFromIterable(arr)));"); test("f(a, ...b, c, ...d, e);", "f.apply(null, [].concat([a], $jscomp.arrayFromIterable(b)," + " [c], $jscomp.arrayFromIterable(d), [e]));"); test("Factory.create().m(...arr);", LINE_JOINER.join( "var $jscomp$spread$args0;", "($jscomp$spread$args0 = Factory.create()).m.apply(" + "$jscomp$spread$args0, [].concat($jscomp.arrayFromIterable(arr)));" )); test("var x = b ? Factory.create().m(...arr) : null;", LINE_JOINER.join( "var $jscomp$spread$args0;", "var x = b ? ($jscomp$spread$args0 = Factory.create()).m.apply($jscomp$spread$args0, ", " [].concat($jscomp.arrayFromIterable(arr))) : null;")); test("getF()(...args);", "getF().apply(null, [].concat($jscomp.arrayFromIterable(args)));"); test( "F.c().m(...a); G.d().n(...b);", LINE_JOINER.join( "var $jscomp$spread$args0;", "($jscomp$spread$args0 = F.c()).m.apply($jscomp$spread$args0,", " [].concat($jscomp.arrayFromIterable(a)));", "var $jscomp$spread$args1;", "($jscomp$spread$args1 = G.d()).n.apply($jscomp$spread$args1,", " [].concat($jscomp.arrayFromIterable(b)));")); enableTypeCheck(); testWarning( LINE_JOINER.join( "class C {}", "class Factory {", " /** @return {C} */", " static create() {return new C()}", "}", "var arr = [1,2]", "Factory.create().m(...arr);"), TypeCheck.INEXISTENT_PROPERTY); test( LINE_JOINER.join( "class C { m(a) {} }", "class Factory {", " /** @return {C} */", " static create() {return new C()}", "}", "var arr = [1,2]", "Factory.create().m(...arr);"), LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype.m = function(a) {};", "/** @constructor @struct */", "var Factory = function() {};", "/** @return {C} */", "Factory.create = function() {return new C()};", "var arr = [1,2]", "var $jscomp$spread$args0;", "($jscomp$spread$args0 = Factory.create()).m.apply(", " $jscomp$spread$args0, [].concat($jscomp.arrayFromIterable(arr)));")); } public void testSpreadNew() { setLanguageOut(LanguageMode.ECMASCRIPT5); test("new F(...args);", "new (Function.prototype.bind.apply(F, [null].concat($jscomp.arrayFromIterable(args))));"); } public void testMethodInObject() { test("var obj = { f() {alert(1); } };", "var obj = { f: function() {alert(1); } };"); test( "var obj = { f() { alert(1); }, x };", "var obj = { f: function() { alert(1); }, x: x };"); } public void testComputedPropertiesWithMethod() { test( "var obj = { ['f' + 1]: 1, m() {}, ['g' + 1]: 1, };", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "var obj = ($jscomp$compprop0['f' + 1] = 1,", " ($jscomp$compprop0.m = function() {}, ", " ($jscomp$compprop0['g' + 1] = 1, $jscomp$compprop0)));")); } public void testComputedProperties() { test( "var obj = { ['f' + 1] : 1, ['g' + 1] : 1 };", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "var obj = ($jscomp$compprop0['f' + 1] = 1,", " ($jscomp$compprop0['g' + 1] = 1, $jscomp$compprop0));")); test( "var obj = { ['f'] : 1};", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "var obj = ($jscomp$compprop0['f'] = 1,", " $jscomp$compprop0);")); test( "var o = { ['f'] : 1}; var p = { ['g'] : 1};", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "var o = ($jscomp$compprop0['f'] = 1,", " $jscomp$compprop0);", "var $jscomp$compprop1 = {};", "var p = ($jscomp$compprop1['g'] = 1,", " $jscomp$compprop1);")); test( "({['f' + 1] : 1})", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "($jscomp$compprop0['f' + 1] = 1,", " $jscomp$compprop0)")); test( "({'a' : 2, ['f' + 1] : 1})", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "($jscomp$compprop0['a'] = 2,", " ($jscomp$compprop0['f' + 1] = 1, $jscomp$compprop0));")); test( "({['f' + 1] : 1, 'a' : 2})", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "($jscomp$compprop0['f' + 1] = 1,", " ($jscomp$compprop0['a'] = 2, $jscomp$compprop0));")); test("({'a' : 1, ['f' + 1] : 1, 'b' : 1})", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "($jscomp$compprop0['a'] = 1,", " ($jscomp$compprop0['f' + 1] = 1, ($jscomp$compprop0['b'] = 1, $jscomp$compprop0)));" )); test( "({'a' : x++, ['f' + x++] : 1, 'b' : x++})", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "($jscomp$compprop0['a'] = x++, ($jscomp$compprop0['f' + x++] = 1,", " ($jscomp$compprop0['b'] = x++, $jscomp$compprop0)))")); test( "({a : x++, ['f' + x++] : 1, b : x++})", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "($jscomp$compprop0.a = x++, ($jscomp$compprop0['f' + x++] = 1,", " ($jscomp$compprop0.b = x++, $jscomp$compprop0)))")); test( "({a, ['f' + 1] : 1})", LINE_JOINER.join( "var $jscomp$compprop0 = {};", " ($jscomp$compprop0.a = a, ($jscomp$compprop0['f' + 1] = 1, $jscomp$compprop0))")); test( "({['f' + 1] : 1, a})", LINE_JOINER.join( "var $jscomp$compprop0 = {};", " ($jscomp$compprop0['f' + 1] = 1, ($jscomp$compprop0.a = a, $jscomp$compprop0))")); test( "var obj = { [foo]() {}}", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "var obj = ($jscomp$compprop0[foo] = function(){}, $jscomp$compprop0)")); test( "var obj = { *[foo]() {}}", LINE_JOINER.join( "var $jscomp$compprop0 = {};", "var obj = (", " $jscomp$compprop0[foo] = function*(){},", " $jscomp$compprop0)")); } public void testComputedPropGetterSetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); testSame("var obj = {get latest () {return undefined;}}"); testSame("var obj = {set latest (str) {}}"); test( "var obj = {'a' : 2, get l () {return null;}, ['f' + 1] : 1}", LINE_JOINER.join( "var $jscomp$compprop0 = {get l () {return null;}};", "var obj = ($jscomp$compprop0['a'] = 2,", " ($jscomp$compprop0['f' + 1] = 1, $jscomp$compprop0));")); test( "var obj = {['a' + 'b'] : 2, set l (str) {}}", LINE_JOINER.join( "var $jscomp$compprop0 = {set l (str) {}};", "var obj = ($jscomp$compprop0['a' + 'b'] = 2, $jscomp$compprop0);")); } public void testComputedPropClass() { test( "class C { [foo]() { alert(1); } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype[foo] = function() { alert(1); };")); test( "class C { static [foo]() { alert(2); } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C[foo] = function() { alert(2); };")); } public void testComputedPropGeneratorMethods() { test( "class C { *[foo]() { yield 1; } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C.prototype[foo] = function*() { yield 1; };")); test( "class C { static *[foo]() { yield 2; } }", LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "C[foo] = function*() { yield 2; };")); } public void testBlockScopedGeneratorFunction() { // Functions defined in a block get translated to a var test( "{ function *f() {yield 1;} }", "{ var f = function*() { yield 1; }; }"); } public void testComputedPropCannotConvert() { testError("var o = { get [foo]() {}}", CANNOT_CONVERT_YET); testError("var o = { set [foo](val) {}}", CANNOT_CONVERT_YET); } public void testNoComputedProperties() { testSame("({'a' : 1})"); testSame("({'a' : 1, f : 1, b : 1})"); } public void testUntaggedTemplateLiteral() { test("``", "''"); test("`\"`", "'\\\"'"); test("`'`", "\"'\""); test("`\\``", "'`'"); test("`\\\"`", "'\\\"'"); test("`\\\\\"`", "'\\\\\\\"'"); test("`\"\\\\`", "'\"\\\\'"); test("`$$`", "'$$'"); test("`$$$`", "'$$$'"); test("`\\$$$`", "'$$$'"); test("`hello`", "'hello'"); test("`hello\nworld`", "'hello\\nworld'"); test("`hello\rworld`", "'hello\\nworld'"); test("`hello\r\nworld`", "'hello\\nworld'"); test("`hello\n\nworld`", "'hello\\n\\nworld'"); test("`hello\\r\\nworld`", "'hello\\r\\nworld'"); test("`${world}`", "'' + world"); test("`hello ${world}`", "'hello ' + world"); test("`${hello} world`", "hello + ' world'"); test("`${hello}${world}`", "'' + hello + world"); test("`${a} b ${c} d ${e}`", "a + ' b ' + c + ' d ' + e"); test("`hello ${a + b}`", "'hello ' + (a + b)"); test("`hello ${a, b, c}`", "'hello ' + (a, b, c)"); test("`hello ${a ? b : c}${a * b}`", "'hello ' + (a ? b : c) + (a * b)"); } public void testTaggedTemplateLiteral() { test( "tag``", LINE_JOINER.join( "var $jscomp$templatelit$0 = /** @type {!ITemplateArray} */ (['']);", "$jscomp$templatelit$0.raw = [''];", "tag($jscomp$templatelit$0);")); test( "tag`${hello} world`", LINE_JOINER.join( "var $jscomp$templatelit$0 = /** @type {!ITemplateArray} */ (['', ' world']);", "$jscomp$templatelit$0.raw = ['', ' world'];", "tag($jscomp$templatelit$0, hello);")); test( "tag`${hello} ${world}`", LINE_JOINER.join( "var $jscomp$templatelit$0 = /** @type {!ITemplateArray} */ (['', ' ', '']);", "$jscomp$templatelit$0.raw = ['', ' ', ''];", "tag($jscomp$templatelit$0, hello, world);")); test( "tag`\"`", LINE_JOINER.join( "var $jscomp$templatelit$0 = /** @type {!ITemplateArray} */ (['\\\"']);", "$jscomp$templatelit$0.raw = ['\\\"'];", "tag($jscomp$templatelit$0);")); // The cooked string and the raw string are different. test( "tag`a\tb`", LINE_JOINER.join( "var $jscomp$templatelit$0 = /** @type {!ITemplateArray} */ (['a\tb']);", "$jscomp$templatelit$0.raw = ['a\\tb'];", "tag($jscomp$templatelit$0);")); test( "tag()`${hello} world`", LINE_JOINER.join( "var $jscomp$templatelit$0 = /** @type {!ITemplateArray} */ (['', ' world']);", "$jscomp$templatelit$0.raw = ['', ' world'];", "tag()($jscomp$templatelit$0, hello);")); test( "a.b`${hello} world`", LINE_JOINER.join( "var $jscomp$templatelit$0 = /** @type {!ITemplateArray} */ (['', ' world']);", "$jscomp$templatelit$0.raw = ['', ' world'];", "a.b($jscomp$templatelit$0, hello);")); // https://github.com/google/closure-compiler/issues/1299 test( "tag`<p class=\"foo\">${x}</p>`", LINE_JOINER.join( "var $jscomp$templatelit$0 = " + "/** @type {!ITemplateArray} */ (['<p class=\"foo\">', '</p>']);", "$jscomp$templatelit$0.raw = ['<p class=\"foo\">', '</p>'];", "tag($jscomp$templatelit$0, x);")); test( "tag`<p class='foo'>${x}</p>`", LINE_JOINER.join( "var $jscomp$templatelit$0 = " + "/** @type {!ITemplateArray} */ (['<p class=\\'foo\\'>', '</p>']);", "$jscomp$templatelit$0.raw = ['<p class=\\'foo\\'>', '</p>'];", "tag($jscomp$templatelit$0, x);")); } public void testUnicodeEscapes() { test("var \\u{73} = \'\\u{2603}\'", "var s = \'\u2603\'"); // ☃ test("var \\u{63} = \'\\u{1f42a}\'", "var c = \'\uD83D\uDC2A\'"); // 🐪 test("var str = `begin\\u{2026}end`", "var str = 'begin\\u2026end'"); } @Override protected Compiler createCompiler() { return new NoninjectingCompiler(); } @Override NoninjectingCompiler getLastCompiler() { return (NoninjectingCompiler) super.getLastCompiler(); } }