From fecf27d0ed9c6cbfb55aa5ae1913671787f4ef2c Mon Sep 17 00:00:00 2001 From: sdh Date: Tue, 26 Jun 2018 20:07:34 -0700 Subject: [PATCH] Add basic ES6 class method support to TypedScopeCreator Methods are added to the type's property map, but no checking of overrides is done yet, and 'super' is not yet supported. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202241474 --- .../google/javascript/jscomp/NodeUtil.java | 5 +- .../javascript/jscomp/TypedScopeCreator.java | 45 ++- .../javascript/jscomp/NodeUtilTest.java | 18 + .../jscomp/TypeCheckNoTranspileTest.java | 382 +++++++++++++++++- .../jscomp/TypedScopeCreatorTest.java | 190 ++++++++- 5 files changed, 626 insertions(+), 14 deletions(-) diff --git a/src/com/google/javascript/jscomp/NodeUtil.java b/src/com/google/javascript/jscomp/NodeUtil.java index a55b347c906..05ef2d991d0 100644 --- a/src/com/google/javascript/jscomp/NodeUtil.java +++ b/src/com/google/javascript/jscomp/NodeUtil.java @@ -5239,6 +5239,8 @@ public static Node getBestLValue(Node n) { Node parent = n.getParent(); if (isFunctionDeclaration(n) || isClassDeclaration(n)) { return n.getFirstChild(); + } else if (n.isClassMembers()) { + return getBestLValue(parent); } else if (parent.isName()) { return parent; } else if (parent.isAssign()) { @@ -5316,7 +5318,8 @@ static String getBestLValueName(@Nullable Node lValue) { return null; } String methodName = lValue.getString(); - return className + ".prototype." + methodName; + String maybePrototype = lValue.isStaticMember() ? "." : ".prototype."; + return className + maybePrototype + methodName; } // TODO(sdh): Tighten this to simply require !lValue.isQuotedString() // Could get rid of the isJSIdentifier check, but may need to fix depot. diff --git a/src/com/google/javascript/jscomp/TypedScopeCreator.java b/src/com/google/javascript/jscomp/TypedScopeCreator.java index 11528dab387..f71e2ec40a1 100644 --- a/src/com/google/javascript/jscomp/TypedScopeCreator.java +++ b/src/com/google/javascript/jscomp/TypedScopeCreator.java @@ -1074,21 +1074,32 @@ private FunctionType createFunctionTypeFromNodes( boolean isFnLiteral = rValue != null && rValue.isFunction(); Node fnRoot = isFnLiteral ? rValue : null; Node parametersNode = isFnLiteral ? rValue.getSecondChild() : null; + Node classRoot = + lvalueNode != null && lvalueNode.getParent().isClassMembers() + ? lvalueNode.getGrandparent() + : null; // Find the type of any overridden function. Node ownerNode = NodeUtil.getBestLValueOwner(lvalueNode); String ownerName = NodeUtil.getBestLValueName(ownerNode); - TypedVar ownerVar = null; - String propName = null; ObjectType ownerType = null; - if (ownerName != null) { - ownerVar = currentScope.getVar(ownerName); + if (ownerNode != null && classRoot != null) { + // Static members are owned by the constructor, non-statics are owned by the prototype. + ownerType = JSType.toMaybeFunctionType(classRoot.getJSType()); + if (!lvalueNode.isStaticMember() && ownerType != null) { + ownerType = ((FunctionType) ownerType).getPrototype(); + ownerName = ownerName + ".prototype"; + } + } else if (ownerName != null) { + TypedVar ownerVar = currentScope.getVar(ownerName); if (ownerVar != null) { ownerType = ObjectType.cast(ownerVar.getType()); } - if (name != null) { - propName = name.substring(ownerName.length() + 1); - } + } + + String propName = null; + if (ownerName != null && name != null) { + propName = name.substring(ownerName.length() + 1); } ObjectType prototypeOwner = getPrototypeOwnerType(ownerType); @@ -1549,6 +1560,8 @@ JSType getDeclaredType(JSDocInfo info, Node lValue, @Nullable Node rValue) { && shouldUseFunctionLiteralType( JSType.toMaybeFunctionType(rValue.getJSType()), info, lValue)) { return rValue.getJSType(); + } else if (rValue != null && rValue.isClass()) { + return rValue.getJSType(); } else if (info != null) { if (info.hasEnumParameterType()) { if (rValue != null && rValue.isObjectLit()) { @@ -2321,9 +2334,10 @@ void visitPreorder(NodeTraversal t, Node n, Node parent) { if (n.isFunction()) { if (parent.getString().equals("constructor")) { // Constructor has already been analyzed, so pull that here. - n.setJSType(currentScope.getRootNode().getJSType()); + setDeferredType(n, currentScope.getRootNode().getJSType()); + } else { + defineFunctionLiteral(n); } - // TODO(sdh): handle non-constructor methods. } } @@ -2335,7 +2349,20 @@ void visitPostorder(NodeTraversal t, Node n, Node parent) { // Declare bleeding class name in scope. Pull the type off the AST. checkState(!n.getString().isEmpty()); // anonymous classes have EMPTY nodes, not NAME defineSlot(n, parent.getJSType(), false); + } else if (n.isMemberFunctionDef() && !"constructor".equals(n.getString())) { + defineMemberFunction(n); + } + } + + void defineMemberFunction(Node n) { + // MEMBER_FUNCTION_DEF -> CLASS_MEMBERS -> CLASS + Node ownerNode = n.getGrandparent(); + checkState(ownerNode.isClass()); + ObjectType ownerType = ownerNode.getJSType().toMaybeFunctionType(); + if (!n.isStaticMember()) { + ownerType = ((FunctionType) ownerType).getPrototype(); } + ownerType.defineDeclaredProperty(n.getString(), n.getLastChild().getJSType(), n); } } // end ClassScopeBuilder diff --git a/test/com/google/javascript/jscomp/NodeUtilTest.java b/test/com/google/javascript/jscomp/NodeUtilTest.java index f0fd12a158b..1858d337498 100644 --- a/test/com/google/javascript/jscomp/NodeUtilTest.java +++ b/test/com/google/javascript/jscomp/NodeUtilTest.java @@ -46,6 +46,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import javax.annotation.Nullable; import junit.framework.TestCase; @@ -2348,6 +2349,23 @@ public void testGetBestLValue() { assertEquals("x", getFunctionLValue("var x = (y, function() {});")); } + public void testGetBestLValueName() { + Function getBestName = + (js) -> NodeUtil.getBestLValueName(NodeUtil.getBestLValue(getFunctionNode(js))); + assertEquals("x", getBestName.apply("var x = function() {};")); + assertEquals("x", getBestName.apply("x = function() {};")); + assertEquals("x", getBestName.apply("function x() {};")); + assertEquals("x", getBestName.apply("var x = y ? z : function() {};")); + assertEquals("x", getBestName.apply("var x = y ? function() {} : z;")); + assertEquals("x", getBestName.apply("var x = y && function() {};")); + assertEquals("x", getBestName.apply("var x = y || function() {};")); + assertEquals("x", getBestName.apply("var x = (y, function() {});")); + assertEquals("C.prototype.d", getBestName.apply("C.prototype.d = function() {};")); + assertEquals("C.prototype.d", getBestName.apply("class C { d() {} };")); + assertEquals("C.d", getBestName.apply("C.d = function() {};")); + assertEquals("C.d", getBestName.apply("class C { static d() {} };")); + } + public void testGetRValueOfLValue() { assertTrue(functionIsRValueOfAssign("x = function() {};")); assertTrue(functionIsRValueOfAssign("x += function() {};")); diff --git a/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java b/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java index 5caf13d94e9..bb2523d502a 100644 --- a/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java @@ -2151,7 +2151,6 @@ public void testClassSyntaxRecord() { } public void testClassSyntaxRecordMismatch() { - // TODO(sdh): Should be an error. testTypes( lines( "/** @record */", // @@ -2161,7 +2160,13 @@ public void testClassSyntaxRecordMismatch() { " this.foo;", " }", "}", - "var /** !Rec */ rec = {foo: string};")); + "var /** !Rec */ rec = {foo: 'string'};"), + lines( + "initializing variable", + "found : {foo: string}", + "required: Rec", + "missing : []", + "mismatch: [foo]")); } public void testClassJSDocExtendsInconsistentWithExtendsClause() { @@ -2183,6 +2188,117 @@ public void testClassJSDocExtendsWithMissingExtendsClause() { "class Foo {}")); } + public void testClassExtendsGetElem() { + testTypes( + lines( + "class Foo {}", + "/** @const {!Object} */", + "var obj = {};", + "class Bar extends obj['abc'] {}", + "var /** !Foo */ foo = new Bar();"), + // TODO(sdh): It would be good to recognize that Bar actually *is* a Foo. + lines( + "initializing variable", + "found : Bar", + "required: Foo")); + } + + public void testClassExtendsFunctionCall() { + testTypes( + lines( + "class Foo {}", + "/** @return {function(new:Foo)} */", + "function mixin() {}", + "class Bar extends mixin() {}", + "var /** !Foo */ foo = new Bar();"), + // TODO(sdh): It would be good to recognize that Bar actually *is* a Foo. + lines( + "initializing variable", + "found : Bar", + "required: Foo")); + } + + public void testClassImplementsInterface() { + testTypes( + lines( + "/** @interface */", + "class Foo { foo() {} }", + "/** @implements {Foo} */", + "class Bar {", + " /** @override */", + " foo() {}", + "}")); + } + + public void testClassMissingInterfaceMethod() { + testTypes( + lines( + "/** @interface */", + "class Foo { foo() {} }", + "/** @implements {Foo} */", + "class Bar {}"), + "property foo on interface Foo is not implemented by type Bar"); + } + + public void testClassAbstractClassNeedNonExplicitlyOverrideUnimplementedInterfaceMethods() { + testTypes( + lines( + "/** @interface */", + "class Foo { foo() {} }", + "/** @abstract @implements {Foo} */", + "class Bar {}"), + // TODO(sdh): allow this without error, provided we can get the error on the concrete class + "property foo on interface Foo is not implemented by type Bar"); + } + + public void testClassIncompatibleInterfaceMethodImplementation() { + testTypes( + lines( + "/** @interface */", + "class Foo {", + " /** @return {number} */ foo() {}", + "}", + "/** @implements {Foo} */", + "class Bar {", + " /** @override @return {number|string} */", + " foo() {}", + "}"), + lines( + "mismatch of the foo property on type Bar and the type of the property it overrides " + + "from interface Foo", + "original: function(this:Foo): number", + "override: function(this:Bar): (number|string)")); + } + + public void testClassMissingTransitiveInterfaceMethod() { + testTypes( + lines( + "/** @interface */", + "class Foo { foo() {} }", + "/** @interface @extends {Foo} */", + "class Bar {}", + "/** @implements {Bar} */", + "class Baz {}"), + "property foo on interface Foo is not implemented by type Baz"); + } + + public void testClassMixinAllowsNonOverriddenInterfaceMethods() { + testTypes( + lines( + "/** @interface */", + "class Foo {", + " /** @return {number} */ foo() {}", + "}", + "class Bar {}", + // TODO(sdh): Intersection types would allow annotating this correctly. + "/** @return {function(new:Bar)} */", + "function mixin() {}", + "/** @implements {Foo} */", + "class Baz extends mixin() {}"), + // TODO(sdh): This is supposed to be allowed. + "property foo on interface Foo is not implemented by type Baz"); + } + public void testClassMissingSuperCall() { // TODO(sdh): Should be an error to access 'this' before super (but maybe not in TypeCheck). testTypes( @@ -2283,6 +2399,268 @@ public void testClassConstructorTypeParametersWithParameterTypeMismatch() { // "required: function(number): ?")); } + public void testClassSideInheritanceFillsInParameterTypesWhenCheckingBody() { + testTypes( + lines( + "class Foo {", + " static foo(/** string */ arg) {}", + "}", + "class Bar extends Foo {", + // TODO(sdh): Should need @override here. + " static foo(arg) {", + " var /** null */ x = arg;", + " }", + "}"), + lines( + "initializing variable", // + "found : string", + "required: null")); + } + + public void testClassMethodParameters() { + testTypes( + lines( + "class C {", + " /** @param {number} arg */", + " m(arg) {}", + "}", + "new C().m('x');"), + lines( + "actual parameter 1 of C.prototype.m does not match formal parameter", + "found : string", + "required: number")); + } + + public void testClassInheritedMethodParameters() { + testTypes( + lines( + "var B = class {", + " /** @param {boolean} arg */", + " method(arg) {}", + "};", + "var C = class extends B {};", + "new C().method(1);"), + lines( + "actual parameter 1 of B.prototype.method does not match formal parameter", + "found : number", + "required: boolean")); + } + + public void testClassMethodReturns() { + testTypes( + lines( + "var D = class {", + " /** @return {number} */", + " m() {}", + "}", + "var /** null */ x = new D().m();"), + lines( + "initializing variable", // + "found : number", + "required: null")); + } + + public void testClassInheritedMethodReturns() { + testTypes( + lines( + "class Q {", + " /** @return {string} */", + " method() {}", + "};", + "var P = class extends Q {};", + "var /** null */ x = new P().method();"), + lines( + "initializing variable", // + "found : string", + "required: null")); + } + + public void testClassStaticMethodParameters() { + testTypes( + lines( + "class C {", + " /** @param {number} arg */", + " static m(arg) {}", + "}", + "C.m('x');"), + lines( + "actual parameter 1 of C.m does not match formal parameter", + "found : string", + "required: number")); + } + + public void testClassInheritedStaticMethodParameters() { + testTypes( + lines( + "var B = class {", + " /** @param {boolean} arg */", + " static method(arg) {}", + "};", + "var C = class extends B {};", + "C.method(1);"), + lines( + "actual parameter 1 of C.method does not match formal parameter", + "found : number", + "required: boolean")); + } + + public void testClassStaticMethodReturns() { + testTypes( + lines( + "var D = class {", + " /** @return {number} */", + " static m() {}", + "};", + "var /** null */ x = D.m();"), + lines( + "initializing variable", // + "found : number", + "required: null")); + } + + public void testClassInheritedStaticMethodReturns() { + testTypes( + lines( + "class Q {", + " /** @return {string} */", + " static method() {}", + "}", + "class P extends Q {}", + "var /** null */ x = P.method();"), + lines( + "initializing variable", // + "found : string", + "required: null")); + } + + public void testClassStaticMethodCalledOnInstance() { + testTypes( + lines( + "class C {", + " static m() {}", + "}", + "new C().m();"), + // TODO(sdh): This error message should be different from the converse case. + // Probably should say "Instance property m never defined on C". + "Property m never defined on C"); + } + + public void testClassInstanceMethodCalledOnClass() { + testTypes( + lines( + "class C {", + " m() {}", + "}", + "C.m();"), + // TODO(sdh): This error message should be different from the converse case. + // Probably should say "Static property m never defined on C". + "Property m never defined on C"); + } + + public void testClassInstanceMethodOverriddenWithWidenedType() { + testTypes( + lines( + "class Base {", + " /** @param {string} arg */", + " method(arg) {}", + "}", + "class Sub extends Base {", + " /** @override @param {string|number} arg */", + " method(arg) {}", + "}")); + } + + public void testClassStaticMethodOverriddenWithWidenedType() { + testTypes( + lines( + "class Base {", + " /** @param {string} arg */", + " static method(arg) {}", + "}", + "class Sub extends Base {", + // TODO(sdh): should need @override + " /** @param {string|number} arg */", + " static method(arg) {}", + "}")); + } + + public void testClassStaticMethodOverriddenWithIncompatibleType() { + testTypes( + lines( + "class Base {", + " /** @param {string|number} arg */", + " static method(arg) {}", + "}", + "class Sub extends Base {", + " /** @override */", + " static method(arg) {}", + "}")); + // TODO(sdh): This should actually check the override. + // lines( + // "mismatch of the method property type and the type of the property it overrides " + // + "from superclass Base", + // "original: function((number|string)): undefined", + // "override: function(string): undefined")); + } + + public void testClassTreatedAsStruct() { + testTypes( + lines( + "class Foo {}", // + "var foo = new Foo();", + "foo.x = 42;"), + "Cannot add a property to a struct instance after it is constructed." + + " (If you already declared the property, make sure to give it a type.)"); + } + + public void testClassTreatedAsStructSymbolAccess() { + testTypesWithCommonExterns( + lines( + "class Foo {}", // + "var foo = new Foo();", + "foo[Symbol.iterator] = 42;")); + } + + public void testClassAnnotatedWithUnrestricted() { + disableStrictMissingPropertyChecks(); + testTypes( + lines( + "/** @unrestricted */ class Foo {}", // + "var foo = new Foo();", + "foo.x = 42;")); + } + + public void testClassAnnotatedWithDictDotAccess() { + disableStrictMissingPropertyChecks(); + testTypes( + lines( + "/** @dict */ class Foo {}", + "var foo = new Foo();", + "foo.x = 42;"), + "Cannot do '.' access on a dict"); + } + + public void testClassAnnotatedWithDictComputedAccess() { + testTypes( + lines( + "/** @dict */ class Foo {}", + "var foo = new Foo();", + "foo['x'] = 42;")); + } + + public void testClassSuperConstructorParameterMismatch() { + testTypes( + lines( + "class Foo {}", + "class Bar extends Foo {", + " constructor() {", + " super(1);", + " }", + "}"), + "Function super: called with 1 argument(s). Function requires at least 0 argument(s) " + + "and no more than 0 argument(s)."); + } + public void testAsyncFunctionWithoutJSDoc() { testTypes("async function f() { return 3; }"); } diff --git a/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java b/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java index 43fb262092f..755b5895c34 100644 --- a/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java +++ b/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java @@ -1421,7 +1421,132 @@ public void testClassDeclarationWithNestedExtendsAndInheritedConstructor() { assertType(params.get(0)).isString(); } - public void testClassExpressionDeclaration() { + public void testClassDeclarationWithMethod() { + testSame( + lines( + "class Foo {", + " /** @param {string} arg */", + " method(arg) {", + " METHOD:;", + " var /** number */ foo;", + " }", + "}")); + TypedScope methodBlockScope = getLabeledStatement("METHOD").enclosingScope; + TypedScope methodScope = methodBlockScope.getParentScope(); + assertScope(methodBlockScope).declares("foo").directly().withTypeThat().isNumber(); + assertScope(methodScope).declares("arg").directly().withTypeThat().isString(); + + FunctionType method = (FunctionType) methodScope.getRootNode().getJSType(); + assertType(method).toStringIsEqualTo("function(this:Foo, string): undefined"); + FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); + assertType(foo.getInstanceType()).withTypeOfProp("method").isEqualTo(method); + } + + public void testClassDeclarationWithMethodAndInlineParamDocs() { + testSame( + lines( + "class Foo {", + " method(/** string */ arg) {", + " METHOD:;", + " var /** number */ foo;", + " }", + "}")); + TypedScope methodBlockScope = getLabeledStatement("METHOD").enclosingScope; + TypedScope methodScope = methodBlockScope.getParentScope(); + assertScope(methodBlockScope).declares("foo").directly().withTypeThat().isNumber(); + assertScope(methodScope).declares("arg").directly().withTypeThat().isString(); + + FunctionType method = (FunctionType) methodScope.getRootNode().getJSType(); + assertType(method).toStringIsEqualTo("function(this:Foo, string): undefined"); + FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); + assertType(foo.getInstanceType()).withTypeOfProp("method").isEqualTo(method); + } + + public void testClassDeclarationWithOverriddenMethod() { + testSame( + lines( + "class Bar {", + " method(/** string */ arg) {}", + "}", + "class Foo extends Bar {", + " method(arg) {", + " METHOD:;", + " }", + "}")); + TypedScope methodBlockScope = getLabeledStatement("METHOD").enclosingScope; + TypedScope methodScope = methodBlockScope.getParentScope(); + assertScope(methodScope).declares("arg").directly().withTypeThat().isString(); + + FunctionType method = (FunctionType) methodScope.getRootNode().getJSType(); + assertType(method).toStringIsEqualTo("function(this:Foo, string): undefined"); + FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); + assertType(foo.getInstanceType()).withTypeOfProp("method").isEqualTo(method); + } + + public void testClassDeclarationWithStaticMethod() { + testSame( + lines( + "class Foo {", + " static method(/** string */ arg) {", + " METHOD:;", + " var /** number */ foo;", + " }", + "}")); + TypedScope methodBlockScope = getLabeledStatement("METHOD").enclosingScope; + TypedScope methodScope = methodBlockScope.getParentScope(); + assertScope(methodBlockScope).declares("foo").directly().withTypeThat().isNumber(); + assertScope(methodScope).declares("arg").directly().withTypeThat().isString(); + + FunctionType method = (FunctionType) methodScope.getRootNode().getJSType(); + // TODO(sdh): Probably want function(this:function(new:Foo), string): undefined + assertType(method).toStringIsEqualTo("function(string): undefined"); + FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); + assertType(foo).withTypeOfProp("method").isEqualTo(method); + } + + public void testClassDeclarationWithOverriddenStaticMethod() { + testSame( + lines( + "class Foo {", + " /** @param {number} arg */", + " static method(arg) {}", + "}", + "class Bar extends Foo {", + " static method(arg) {", + " METHOD:;", + " }", + "}")); + TypedScope methodBlockScope = getLabeledStatement("METHOD").enclosingScope; + TypedScope methodScope = methodBlockScope.getParentScope(); + assertScope(methodScope).declares("arg").directly().withTypeThat().isNumber(); + + FunctionType method = (FunctionType) methodScope.getRootNode().getJSType(); + // TODO(sdh): Probably want function(this:function(new:Foo), string): undefined + assertType(method).toStringIsEqualTo("function(number): undefined"); + FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); + assertType(foo).withTypeOfProp("method").isEqualTo(method); + } + + public void testClassDeclarationWithGeneratorMethod() { + testSame( + lines( + "class Foo {", + " /** @param {string} arg */", + " * method(arg) {", + " METHOD:;", + " }", + "}")); + TypedScope methodBlockScope = getLabeledStatement("METHOD").enclosingScope; + TypedScope methodScope = methodBlockScope.getParentScope(); + assertScope(methodScope).declares("arg").directly().withTypeThat().isString(); + + FunctionType method = (FunctionType) methodScope.getRootNode().getJSType(); + assertType(method).toStringIsEqualTo("function(this:Foo, string): Generator"); + FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); + assertType(foo.getInstanceType()).withTypeOfProp("method").isEqualTo(method); + } + + public void testClassExpressionAssignment() { testSame("var Foo = class Bar {}"); FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); assertTrue(foo.isConstructor()); @@ -1436,7 +1561,7 @@ public void testClassExpressionBleedingNameScope() { " constructor() {", " CTOR:;", " }", - "}")); + "};")); FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); TypedScope ctorBlockScope = getLabeledStatement("CTOR").enclosingScope; TypedScope ctorScope = ctorBlockScope.getParentScope(); @@ -1446,6 +1571,67 @@ public void testClassExpressionBleedingNameScope() { assertScope(globalScope).doesNotDeclare("Bar"); } + public void testClassExpressionWithMethod() { + testSame( + lines( + "var Foo = class {", + " /** @param {string} arg */", + " method(arg) {", + " METHOD:;", + " }", + "};")); + TypedScope methodBlockScope = getLabeledStatement("METHOD").enclosingScope; + TypedScope methodScope = methodBlockScope.getParentScope(); + assertScope(methodScope).declares("arg").directly().withTypeThat().isString(); + + FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); + assertType(foo.getInstanceType()) + .withTypeOfProp("method") + .toStringIsEqualTo("function(this:Foo, string): undefined"); + } + + public void testClassExpressionWithStaticMethod() { + testSame( + lines( + "var Foo = class {", + " static method(/** string */ arg) {", + " METHOD:;", + " var /** number */ foo;", + " }", + "}")); + TypedScope methodBlockScope = getLabeledStatement("METHOD").enclosingScope; + TypedScope methodScope = methodBlockScope.getParentScope(); + assertScope(methodBlockScope).declares("foo").directly().withTypeThat().isNumber(); + assertScope(methodScope).declares("arg").directly().withTypeThat().isString(); + + FunctionType method = (FunctionType) methodScope.getRootNode().getJSType(); + // TODO(sdh): Probably want function(this:function(new:Foo), string): undefined + assertType(method).toStringIsEqualTo("function(string): undefined"); + FunctionType foo = (FunctionType) (findNameType("Foo", globalScope)); + assertType(foo).withTypeOfProp("method").isEqualTo(method); + } + + public void testClassExpressionInCallback() { + testSame( + lines( + "function use(arg) {}", + "use(class Bar {", + " constructor() {", + " CTOR:;", + " }", + "});")); + TypedScope ctorBlockScope = getLabeledStatement("CTOR").enclosingScope; + TypedScope ctorScope = ctorBlockScope.getParentScope(); + TypedScope classScope = ctorScope.getParentScope(); + assertScope(classScope) + .declares("Bar") + .directly() + .withTypeThat() + // TODO(sdh): Print a better name (https://github.com/google/closure-compiler/issues/2982) + .toStringIsEqualTo("function(new:): undefined"); + assertScope(globalScope).doesNotDeclare("Bar"); + } + public void testForLoopIntegration() { testSame("var y = 3; for (var x = true; x; y = x) {}");