/* * 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 com.google.javascript.jscomp.CompilerOptions.LanguageMode; public final class Es6RewriteClassTest 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) {}", "", "/**", " * @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) {};"); public Es6RewriteClassTest() { 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("es6ConvertSuper", new Es6ConvertSuper(compiler))); optimizer.addOneTimePass(makePassFactory("es6ExtractClasses", new Es6ExtractClasses(compiler))); optimizer.addOneTimePass(makePassFactory("es6RewriteClass", new Es6RewriteClass(compiler))); optimizer.addOneTimePass( makePassFactory( "Es6ConvertSuperConstructorCalls", new Es6ConvertSuperConstructorCalls(compiler))); return optimizer; } @Override protected int getNumRepetitions() { return 1; } public void testClassStatement() { test("class C { }", "/** @constructor @struct */ let C = function() {};"); test( "class C { constructor() {} }", "/** @constructor @struct */ let C = function() {};"); test( "class C { method() {}; }", LINE_JOINER.join( "/** @constructor @struct */", "let C = function() {};", "C.prototype.method = function() {};")); test( "class C { constructor(a) { this.a = a; } }", "/** @constructor @struct */ let C = function(a) { this.a = a; };"); test( "class C { constructor() {} foo() {} }", LINE_JOINER.join( "/** @constructor @struct */", "let C = function() {};", "C.prototype.foo = function() {};")); test( "class C { constructor() {}; foo() {}; bar() {} }", LINE_JOINER.join( "/** @constructor @struct */", "let C = function() {};", "C.prototype.foo = function() {};", "C.prototype.bar = function() {};")); test( "class C { foo() {}; bar() {} }", LINE_JOINER.join( "/** @constructor @struct */", "let 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 */", "let 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 */", " let Foo = function() {};", "} else {", " /** @constructor @struct */", " let Foo = function() {};", "}")); } public void testClassWithNgInject() { test( "class A { /** @ngInject */ constructor($scope) {} }", "/** @constructor @struct @ngInject */ let A = function($scope) {}"); test( "/** @ngInject */ class A { constructor($scope) {} }", "/** @constructor @struct @ngInject */ let A = function($scope) {}"); } public void testAnonymousSuper() { test( "f(class extends D { f() { super.g() } })", LINE_JOINER.join( "/** @constructor @struct", " * @extends {D}", " * @param {...?} var_args", " */", "const 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 testNewTarget() { testError("function Foo() { new.target; }", CANNOT_CONVERT_YET); } public void testClassWithJsDoc() { test("class C { }", "/** @constructor @struct */ let C = function() { };"); test( "/** @deprecated */ class C { }", "/** @constructor @struct @deprecated */ let C = function() {};"); test( "/** @dict */ class C { }", "/** @constructor @dict */ let C = function() {};"); test( "/** @template T */ class C { }", "/** @constructor @struct @template T */ let C = function() {};"); test( "/** @final */ class C { }", "/** @constructor @struct @final */ let C = function() {};"); test( "/** @private */ class C { }", "/** @constructor @struct @private */ let 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", " */", "let 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", " */", "let 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", " */", "let C = function(b) {};")); test( "class C { /** @throws {Error} */ constructor() {} }", LINE_JOINER.join( "/**", " * @throws {Error}", " * @constructor", " * @struct", " */", "let C = function() {};")); test( "class C { /** @private */ constructor() {} }", LINE_JOINER.join( "/**", " * @private", " * @constructor", " * @struct", " */", "let C = function() {};")); test( "class C { /** @deprecated */ constructor() {} }", LINE_JOINER.join( "/**", " * @deprecated", " * @constructor", " * @struct", " */", "let C = function() {};")); test( "class C { /** @template T */ constructor() {} }", LINE_JOINER.join( "/**", " * @constructor", " * @struct", " * @template T", " */", "let C = function() {};")); test( "/** @template S */ class C { /** @template T */ constructor() {} }", LINE_JOINER.join( "/**", " * @constructor", " * @struct", " * @template S, T", " */", "let C = function() {};")); test( "/** @template S */ class C { /** @template T, U */ constructor() {} }", LINE_JOINER.join( "/**", " * @constructor", " * @struct", " * @template S, T, U", " */", "let C = function() {};")); } public void testMemberWithJsDoc() { test( "class C { /** @param {boolean} b */ foo(b) {} }", LINE_JOINER.join( "/**", " * @constructor", " * @struct", " */", "let C = function() {};", "", "/** @param {boolean} b */", "C.prototype.foo = function(b) {};")); } public void testClassStatementInsideIf() { test( "if (foo) { class C { } }", "if (foo) { /** @constructor @struct */ let C = function() {}; }"); test( "if (foo) class C {}", "if (foo) { /** @constructor @struct */ let 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 testcode$classdecl$var0 = function() {};", "var C = testcode$classdecl$var0;")); test( "var C = class C { foo() {} }", LINE_JOINER.join( "/** @constructor @struct */", "const 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 testcode$classdecl$var0 = function() {};", "window['MediaSource'] = testcode$classdecl$var0;")); } public void testClassExpression() { test( "var C = new (class {})();", LINE_JOINER.join( "/** @constructor @struct */", "const testcode$classdecl$var0=function(){};", "var C=new testcode$classdecl$var0")); test( "(condition ? obj1 : obj2).prop = class C { };", LINE_JOINER.join( "/** @constructor @struct */", "const testcode$classdecl$var0 = function(){};", "(condition ? obj1 : obj2).prop = testcode$classdecl$var0;")); } /** * 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 */", "let D = function() {};", "/** @constructor @struct", " * @extends {D}", " * @param {...?} var_args", " */", "let 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 */", "let D = function() {};", "/** @constructor @struct @extends {D} */", "let 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 */", "let D = function() {};", "/** @constructor @struct @extends {D} */", "let 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", " */", "let 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 */", "let D = function() {};", "/** @constructor @struct", " * @extends {D}", " * @param {...?} var_args", " */", "let 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", " */", "let Error = function(msg) {", " /** @const */ this.message = msg;", "};", "/** @constructor @struct", " * @extends {Error}", " * @param {...?} var_args", " */", "let 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", " */", "let Error = function(msg) {", " /** @const */ this.message = msg;", "};", "/** @constructor @struct", " * @extends {Error}", " */", "let 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", " */", "let 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}", " */", "let 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 */", "let D = function() {};", "D.prototype.f = function() {};", "/**", " * @struct @interface", " * @param {...?} var_args", " * @extends{D} */", "let 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 */", "let D = function() {};", "D.prototype.f = function() {};", "/**", " * @struct @record", " * @param {...?} var_args", " * @extends{D} */", "let 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 */", "let D = function() {};", "D.prototype.f = function() {};", "/** @constructor @struct @implements{D} */", "let 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 */", "let D = function() {};", "/** @constructor @struct", " * @extends {D}", " */", "let C = function() {};")); } public void testSuperCall() { test( "class D {} class C extends D { constructor() { super(); } }", LINE_JOINER.join( "/** @constructor @struct */", "let D = function() {};", "/** @constructor @struct @extends {D} */", "let 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 */", "let D = function() {}", "/** @constructor @struct @extends {D} */", "let 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 */", "let D = function() {}", "/** @constructor @struct @extends {D} */", "let 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 */", "let D = function() {}", "/** @constructor @struct @extends {D} */", "let 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 */", "let D = function() {}", "/** @constructor @struct @extends {D} */", "let 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 */", "let C = function() {}", "C.prototype.method = function() {", " /** @constructor @struct @extends {C} */", " let D = function() {", " C.call(this);", " }", " $jscomp.inherits(D, C);", "};")); test( "class D {} class C extends D { constructor() {}; f() {super();} }", LINE_JOINER.join( "/** @constructor @struct */", "let D = function() {};", "/** @constructor @struct @extends {D} */", "let 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", " */", "let D = function(str) {", " this.str = str;", " return;", "}", "/**", " * @constructor @struct @extends {D}", " * @param {string} str", " * @param {number} n", " */", "let 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} */", "let 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 */", "let D = function(name) {", " this.name = name;", "}", "/** @constructor @struct @extends {D}", " * @param {string} str", " * @param {number} n */", "let 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 */", "let 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 */", "let Base = function() {};", "Base.prototype.method = function() { return 5; };", "", "/** @constructor @struct @extends {Base} */", "let 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 */", "let Base = function() {};", "Base.prototype.method = function() { this._x = 5; };", "", "/** @constructor @struct @extends {Base} */", "let 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", " */", "let 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", " */", "let 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", " */", "let Object = function() {", "};", "/**", " * @constructor @struct @extends {Object}", " * @param {string} msg", " */", "let 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", " */", "let Object = function() {", "};", "/**", " * @constructor @struct @extends {Object}", " * @param {...?} var_args", " */", "let 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 testcode$classdecl$var0 = function(){};", "var F = testcode$classdecl$var0;")); test( "F = class G {}", LINE_JOINER.join( "/** @constructor @struct */", "const testcode$classdecl$var0 = function(){};", "F = testcode$classdecl$var0;")); } public void testClassNested() { test( "class C { f() { class D {} } }", LINE_JOINER.join( "/** @constructor @struct */", "let C = function() {};", "C.prototype.f = function() {", " /** @constructor @struct */", " let D = function() {}", "};")); test( "class C { f() { class D extends C {} } }", LINE_JOINER.join( "/** @constructor @struct */", "let C = function() {};", "C.prototype.f = function() {", " /**", " * @constructor @struct", " * @param {...?} var_args", " * @extends{C} */", " let 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 testSuperCallNonConstructor() { test( "class S extends B { static f() { super(); } }", LINE_JOINER.join( "/** @constructor @struct", " * @extends {B}", " * @param {...?} var_args", " */", "let 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", " */", "let 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 */ let F = function() {}", "/** @this {?} */ F.f = function() { return this; };")); } public void testStaticMethods() { test("class C { static foo() {} }", "/** @constructor @struct */ let C = function() {}; C.foo = function() {};"); test("class C { static foo() {}; foo() {} }", LINE_JOINER.join( "/** @constructor @struct */", "let C = function() {};", "", "C.foo = function() {};", "", "C.prototype.foo = function() {};")); test("class C { static foo() {}; bar() { C.foo(); } }", LINE_JOINER.join( "/** @constructor @struct */", "let 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 */", "let D = function() {};", "D.f = function () {};", "/** @constructor @struct @extends{D} */", "let 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 */", "let D = function() {};", "D.f = function() {};", "/** @constructor @struct @extends{D} */", "let 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 */", "let D = function() {};", "D.f = function() {};", "/** @constructor @struct @extends{D} */", "let 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", " */", "let 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 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 testcode$classdecl$var0 = function() {};", " var self = ns.Child = testcode$classdecl$var0", "}")); } /** * 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 */", "let 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 */", "let 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 */", "let 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 */", "let 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 */", "let 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", " */", "let 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 */", "let 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 */", "let 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 testEs5GetterWithExport() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "class C { /** @export @return {string} */ get foo() {} }", LINE_JOINER.join( "/**", " * @constructor @struct", " */", "let C = function() {}", "", "/**", " * @export", " * @type {string}", " */", "C.prototype.foo;", "", "$jscomp.global.Object.defineProperties(C.prototype, {", " foo: {", " configurable: true,", " enumerable: true,", "", " /**", " * @return {string}", " * @this {C}", " * @export", " */", " get: function() {},", " }", "});")); } public void testEs5SetterWithExport() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "class C { /** @export @param {string} x */ set foo(x) {} }", LINE_JOINER.join( "/**", " * @constructor @struct", " */", "let C = function() {}", "", "/**", " * @export", " * @type {string}", " */", "C.prototype.foo;", "", "$jscomp.global.Object.defineProperties(C.prototype, {", " foo: {", " configurable: true,", " enumerable: true,", "", " /**", " * @param {string} x", " * @this {C}", " * @export", " */", " set: function(x) {},", " }", "});")); } /** * @bug 20536614 */ public void testStaticGetterSetter() { setLanguageOut(LanguageMode.ECMASCRIPT5); test( "class C { static get foo() {} }", LINE_JOINER.join( "/** @constructor @struct */", "let 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 */", "let 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", " */", "let 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 */", "let C = function() {};", "/** @nocollapse @type {?} */", "C.foo;", "$jscomp.global.Object.defineProperties(C, {", " foo: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " set: function(x) {}", " }", "});")); } 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 */", "let C = function() {};", "/** @type {boolean} */", "C.prototype[foo];", "$jscomp.global.Object.defineProperties(", " C.prototype,", " {", " [foo]: {", " configurable:true,", " enumerable:true,", " /** @this {C} */", " get: function() {},", " /** @this {C} */", " set: function(val) {},", " },", " });")); testError( LINE_JOINER.join( "/** @unrestricted */", "class C {", " /** @return {boolean} */", " get [foo]() {}", " /** @param {string} val */", " set [foo](val) {}", "}"), CONFLICTING_GETTER_SETTER_TYPE); } public void testComputedPropClass() { test( "class C { [foo]() { alert(1); } }", LINE_JOINER.join( "/** @constructor @struct */", "let C = function() {};", "C.prototype[foo] = function() { alert(1); };")); test( "class C { static [foo]() { alert(2); } }", LINE_JOINER.join( "/** @constructor @struct */", "let C = function() {};", "C[foo] = function() { alert(2); };")); } @Override protected Compiler createCompiler() { return new NoninjectingCompiler(); } @Override NoninjectingCompiler getLastCompiler() { return (NoninjectingCompiler) super.getLastCompiler(); } }