/* * Copyright 2015 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 com.google.javascript.jscomp.CompilerOptions.LanguageMode; /** * Tests for the new type inference on transpiled code (ES6 and beyond). * * <p>Eventually, NTI will typecheck all language features natively, without transpiling. * * @author dimvar@google.com (Dimitris Vardoulakis) */ public final class NewTypeInferenceWithTranspilationTest extends NewTypeInferenceTestBase { @Override protected void setUp() throws Exception { super.setUp(); compilerOptions.setLanguageIn(LanguageMode.ECMASCRIPT_2015); compilerOptions.setLanguageOut(LanguageMode.ECMASCRIPT3); } public void testSimpleClasses() { typeCheck("class Foo {}"); typeCheck(LINE_JOINER.join( "class Foo {}", "class Bar {}", "/** @type {!Foo} */ var x = new Bar;"), NewTypeInference.MISTYPED_ASSIGN_RHS); typeCheck(LINE_JOINER.join( "class Foo {", " constructor(x) {", " /** @type {string} */", " this.x = x;", " }", "}", "(new Foo('')).x - 5;"), NewTypeInference.INVALID_OPERAND_TYPE); } public void testClassInheritance() { typeCheck(LINE_JOINER.join( "class Foo {}", "class Bar extends Foo {}")); } public void testTaggedTemplateLitGlobalThisRef() { typeCheck( "taggedTemp`${this.toString}TaggedTemp`", NewTypeInference.GLOBAL_THIS); } public void testTaggedTemplate() { typeCheck("String.raw`one ${1} two`"); } public void testConstEmptyArrayNoWarning() { typeCheck("const x = [];"); } public void testFunctionSubtypingForReceiverType() { typeCheck(LINE_JOINER.join( "class Foo {", " method() {}", "}", "class Bar extends Foo {}", "function f(/** function(this:Bar) */ x) {}", "f(Foo.prototype.method);")); typeCheck(LINE_JOINER.join( "class Foo {", " method() { return 123; }", "}", "class Bar extends Foo {}", "/**", " * @template T", " * @param {function(this:Bar):T} x", " */", "function f(x) {}", "f(Foo.prototype.method);")); typeCheck(LINE_JOINER.join( "class Controller {}", "class SubController extends Controller {", " method() {}", "}", "/** @param {{always: function(this:Controller)}} spec */", "function vsyncMethod(spec) {}", "vsyncMethod({always: (new SubController).method});"), NewTypeInference.INVALID_ARGUMENT_TYPE); } public void testDetectPropertyDefinitionOnNullableObject() { typeCheck(LINE_JOINER.join( "/** @unrestricted */", "class Foo {}", "function f(/** ?Foo */ x) {", " /** @type {number} */", " x.prop = 123;", "}", "function g(/** !Foo */ x) {", " return x.prop - 5;", "}"), NewTypeInference.NULLABLE_DEREFERENCE); } public void testDetectPropertyDefinitionOnQualifiedName() { typeCheck(LINE_JOINER.join( "/** @unrestricted */", "class A {}", "/** @unrestricted */", "class B {}", "function f(/** !B */ x) {", " return x.prop;", "}", "/** @type {!A} */", "var a = new A;", "/** @type {!B} */", "a.b = new B;", "/** @type {number} */", "a.b.prop = 123;")); } public void testThisIsNull() { typeCheck(LINE_JOINER.join( "class Foo {", " method() {}", "}", "/**", " * @param {function(this:null)} x", " */", "function f(x) {}", "f((new Foo).method);"), NewTypeInference.INVALID_ARGUMENT_TYPE); } public void testFunctionsWithUntypecheckedArguments() { typeCheck(LINE_JOINER.join( "class Foo {}", "/**", " * @param {function(function(new:Foo, ...?))} f1", " * @param {function(new:Foo, ?)} f2", " */", "function f(f1, f2) {", " f1(f2);", "}")); typeCheck(LINE_JOINER.join( "class Foo {}", "/**", " * @template T", " * @param {function(...?):T} x", " * @return {T}", " */", "function f(x) {", " return x();", "}", "/** @type {function(?):!Foo} */", "function g(x) { return new Foo; }", "f(g) - 5;"), NewTypeInference.INVALID_OPERAND_TYPE); } public void testMethodOverridesWithoutJsdoc() { typeCheck(LINE_JOINER.join( "class A { someMethod(x) {} }", "class B extends A { someMethod() {} }")); typeCheck(LINE_JOINER.join( "class A { someMethod(x) {} }", "class B extends A { someMethod(x, y) { return y + 1; } }"), GlobalTypeInfo.INVALID_PROP_OVERRIDE); typeCheck(LINE_JOINER.join( "class Foo {", " /** @param {...number} var_args */", " method(var_args) {}", "}", "class Bar extends Foo {", " method(x) {}", "}", "(new Bar).method('str');"), NewTypeInference.INVALID_ARGUMENT_TYPE); typeCheck(LINE_JOINER.join( "class Foo {", " /** @param {...number} var_args */", " method(var_args) {}", "}", "class Bar extends Foo {", " method(x,y,z) {}", "}", "(new Bar).method('str');"), NewTypeInference.WRONG_ARGUMENT_COUNT); } public void testOuterVarDefinitionJoinDoesntCrash() { typeCheck(LINE_JOINER.join( "/** @constructor */ function Foo(){}", "function f() {", " if (true) {", " function g() { new Foo; }", " g();", " }", "}")); // typeCheck(LINE_JOINER.join( // "function f() {", // " if (true) {", // " function g() { new Foo; }", // " g();", // " }", // "}"), // VarCheck.UNDEFINED_VAR_ERROR); } public void testSuper() { compilerOptions.setLanguageIn(LanguageMode.ECMASCRIPT_2015); typeCheck(LINE_JOINER.join( "class A {", " constructor(/** string */ x) {}", "}", "class B extends A {", " constructor() {", " super(123);", " }", "}"), NewTypeInference.INVALID_ARGUMENT_TYPE); typeCheck(LINE_JOINER.join( "class A {", " foo(/** string */ x) {}", "}", "class B extends A {", " foo(/** string */ y) {", " super.foo(123);", " }", "}"), NewTypeInference.INVALID_ARGUMENT_TYPE); typeCheck( LINE_JOINER.join( "class A {", " /**", " * @template T", " * @param {T} x", " */", " constructor(x) {}", "}", "/** @extends {A<string>} */", "class B extends A {", " /**", " * @param {string} x", " */", " constructor(x) {", " super(123);", " }", "}"), NewTypeInference.INVALID_ARGUMENT_TYPE); typeCheck(LINE_JOINER.join( "class A {", " static foo(/** string */ x) {}", "}", "class B extends A {", " static foo(/** string */ y) {", " super.foo(123);", " }", "}"), NewTypeInference.INVALID_ARGUMENT_TYPE); // Test that when the superclass has a qualified name, using super works. typeCheck(LINE_JOINER.join( "/** @const */", "var ns = {};", "ns.A = class {", " static foo(/** string */ x) {}", "};", "class B extends ns.A {", " static foo(/** string */ y) {", " super.foo(123);", " }", "}"), NewTypeInference.INVALID_ARGUMENT_TYPE); typeCheck(LINE_JOINER.join( "/** @template T */", "class Collection {", " constructor() {", " /** @type {!Array<T>} */", " this.items = [];", " }", " /** @param {T} item */", " add(item) { this.items.push(item); }", "}", "/** @extends {Collection<number>} */", "class NumberCollection extends Collection {", " constructor() {", " super();", " }", " /** @override */", " add(item) {", " super.add(item);", " }", "}")); } public void testDefaultValuesForArguments() { typeCheck(LINE_JOINER.join( "/** @param {{ focus: (undefined|string) }=} x */", "function f(x = {}) {", " return { a: x.focus };", "}")); } public void testDestructuring() { typeCheck(LINE_JOINER.join( "function f({ myprop1: { myprop2: prop } }) {", " return prop;", "}")); typeCheck(LINE_JOINER.join( "/**", " * @param {{prop: (number|undefined)}} x", " * @return {number}", " */", "function f({prop = 1} = {}) {", " return prop;", "}")); } public void testAbstractMethodCalls() { typeCheck(LINE_JOINER.join( "/** @abstract */", "class A {", " /** @abstract */", " foo() {}", "}", "class B extends A {", " foo() { super.foo(); }", "}"), NewTypeInference.ABSTRACT_SUPER_METHOD_NOT_CALLABLE); typeCheck(LINE_JOINER.join( "/** @abstract */", "class A {", " foo() {}", "}", "class B extends A {", " foo() { super.foo(); }", "}")); typeCheck(LINE_JOINER.join( "/** @abstract */", "class A {", " /** @abstract */", " foo() {}", " bar() {", " this.foo();", " }", "}", "class B extends A {", " foo() {}", " bar() {", " this.foo();", " }", "}")); typeCheck(LINE_JOINER.join( "/** @abstract */", "class A {", " /** @abstract */", " foo() {}", "}", "class B extends A {", " foo() {}", " /** @param {!Array} arr */", " bar(arr) {", " this.foo(...arr);", " }", "}")); // This should generate a warning typeCheck(LINE_JOINER.join( "/** @abstract */", "class A {", " /** @abstract */", " foo() {}", "}", "class B extends A {", " foo() {}", " bar() {", " A.prototype.foo();", " }", "}")); typeCheck(LINE_JOINER.join( "/** @abstract */", "class A {", " /** @abstract */", " foo() {}", "}", "class B extends A {", " foo() {}", " bar() {", " B.prototype.foo();", " }", "}")); typeCheck(LINE_JOINER.join( "/** @abstract */", "class A {", " /** @abstract */", " foo() {}", " bar() {}", "}", "class B extends A {", " foo() {}", " bar() {", " A.prototype.bar();", " }", "}")); } // super is handled natively in both type checkers, which results in ASTs that // are temporarily invalid: a super call that is not in a class. // Avoid crashing. public void testDontCrashWithInvalidIntermediateASTwithSuper() { typeCheck(LINE_JOINER.join( "class Foo {}", "function g(x) {}", "g(function f(x) { return class extends Foo {} });")); } public void testDontWarnAboutUnknownExtends() { typeCheck(LINE_JOINER.join( "function f(clazz) {", " class Foo extends clazz {}", "}")); } public void testMixedClassInheritance() { typeCheck(LINE_JOINER.join( "/** @record */", "function IToggle(){}", "/** @return {number} */", "IToggle.prototype.foo = function(){};", "/**", " * @template T", " * @param {function(new:T)} superClass", " */", "function addToggle(superClass) {", " /** @implements {IToggle} */", " class Toggle extends superClass {", " foo() {", " return 5;", " }", " }", " return Toggle;", "}", "class Bar {}", "/**", " * @constructor", " * @extends {Bar}", " * @implements {IToggle}", " */", "const ToggleBar = addToggle(Bar);", "class Foo extends ToggleBar {}", "const instance = new Foo();", "const number = instance.foo();")); } public void testLoopVariableTranspiledToProperty() { typeCheck(LINE_JOINER.join( "function f() {", " for (let i = 0; i < 1; ++i) {", " const x = 1;", " function b() {", " return x;", " }", " }", "}")); // TODO(dimvar): catch this warning once we typecheck ES6 natively. typeCheck(LINE_JOINER.join( "function f() {", " for (let i = 0; i < 1; ++i) {", " const x = 1;", " x = 2;", " function b() {", " return x;", " }", " }", "}")); } public void testInterfacePropertiesInConstructor() { typeCheck(LINE_JOINER.join( "/** @record */", "class Foo {", " constructor() {", " /** @type {number} */", " this.myprop;", " }", "}", "function f(/** !Foo */ x) {", " var /** number */ y = x.myprop;", "}")); typeCheck(LINE_JOINER.join( "/** @record */", "class Foo {", " constructor() {", " /** @type {number} */", " this.myprop;", " }", "}", "function f(/** !Foo */ x) {", " var /** string */ y = x.myprop;", "}"), NewTypeInference.MISTYPED_ASSIGN_RHS); typeCheck(LINE_JOINER.join( "/**", " * @record", " * @template T", " */", "class Foo {", " constructor() {", " /** @type {T} */", " this.myprop;", " }", "}", "function f(/** !Foo<number> */ x) {", " var /** string */ y = x.myprop;", "}"), NewTypeInference.MISTYPED_ASSIGN_RHS); } }