From 233ee19cbcec3774aed6a8de55173c581ddaa30c Mon Sep 17 00:00:00 2001 From: johnlenz Date: Thu, 6 Sep 2018 12:21:21 -0700 Subject: [PATCH] Introduce the `typeof` operator in JSDoc types. This allows explicitly referring to namespace, constructor, enum, and module types and other anonymous types which were otherwise impossible to reference in type expressions. This is based on the change by sdh@ and has only minor changes from the original. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=211848888 --- .../jscomp/parsing/JsDocInfoParser.java | 33 ++-- .../rhino/jstype/JSTypeRegistry.java | 16 ++ .../javascript/rhino/jstype/NamedType.java | 6 + .../javascript/jscomp/TypeCheckTest.java | 145 ++++++++++++++++++ .../jscomp/parsing/JsDocInfoParserTest.java | 30 ++++ 5 files changed, 221 insertions(+), 9 deletions(-) diff --git a/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java b/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java index cced23a2b2b..fd2f31327cf 100644 --- a/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java +++ b/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java @@ -2141,6 +2141,8 @@ private Node parseBasicTypeExpression(JsDocToken token) { case "null": case "undefined": return newStringNode(string); + case "typeof": + return parseTypeofType(next()); default: return parseTypeName(token); } @@ -2150,11 +2152,7 @@ private Node parseBasicTypeExpression(JsDocToken token) { return reportGenericTypeSyntaxWarning(); } - /** - * TypeName := NameExpression | NameExpression TypeApplication - * TypeApplication := '.<' TypeExpressionList '>' - */ - private Node parseTypeName(JsDocToken token) { + private Node parseNameExpression(JsDocToken token) { if (token != JsDocToken.STRING) { return reportGenericTypeSyntaxWarning(); } @@ -2162,8 +2160,7 @@ private Node parseTypeName(JsDocToken token) { String typeName = stream.getString(); int lineno = stream.getLineno(); int charno = stream.getCharno(); - while (match(JsDocToken.EOL) && - typeName.charAt(typeName.length() - 1) == '.') { + while (match(JsDocToken.EOL) && typeName.charAt(typeName.length() - 1) == '.') { skipEOLs(); if (match(JsDocToken.STRING)) { next(); @@ -2171,12 +2168,20 @@ private Node parseTypeName(JsDocToken token) { } } - Node typeNameNode = newStringNode(typeName, lineno, charno); + return newStringNode(typeName, lineno, charno); + } + + /** + * TypeName := NameExpression | NameExpression TypeApplication TypeApplication := '.'? '<' + * TypeExpressionList '>' + */ + private Node parseTypeName(JsDocToken token) { + Node typeNameNode = parseNameExpression(token); if (match(JsDocToken.LEFT_ANGLE)) { next(); skipEOLs(); - Node memberType = parseTypeExpressionList(typeName, next()); + Node memberType = parseTypeExpressionList(typeNameNode.getString(), next()); if (memberType != null) { typeNameNode.addChildToFront(memberType); @@ -2191,6 +2196,16 @@ private Node parseTypeName(JsDocToken token) { return typeNameNode; } + /** TypeofType := 'typeof' NameExpression | 'typeof' '(' NameExpression ')' */ + private Node parseTypeofType(JsDocToken token) { + Node typeofType = newNode(Token.TYPEOF); + skipEOLs(); + Node name = parseNameExpression(token); + skipEOLs(); + typeofType.addChildToFront(name); + return typeofType; + } + /** * FunctionType := 'function' FunctionSignatureType * FunctionSignatureType := diff --git a/src/com/google/javascript/rhino/jstype/JSTypeRegistry.java b/src/com/google/javascript/rhino/jstype/JSTypeRegistry.java index 67c1b8c710d..db613f6dfc3 100644 --- a/src/com/google/javascript/rhino/jstype/JSTypeRegistry.java +++ b/src/com/google/javascript/rhino/jstype/JSTypeRegistry.java @@ -1939,6 +1939,22 @@ private JSType createFromTypeNodesInternal( case VOID: // Only allowed in the return value of a function. return getNativeType(VOID_TYPE); + case TYPEOF: { + String name = n.getFirstChild().getString(); + StaticTypedSlot slot = scope.getSlot(name); + if (slot == null) { + reporter.warning("Not in scope: " + name, sourceName, n.getLineno(), n.getCharno()); + return getNativeType(UNKNOWN_TYPE); + } + // TODO(sdh): require var to be const? + JSType type = slot.getType(); + if (type.isLiteralObject()) { + type = createNamedType(scope, "typeof " + name, sourceName, n.getLineno(), n.getCharno()); + ((NamedType) type).setReferencedType(slot.getType()); + } + return type; + } + case STRING: // TODO(martinprobst): The new type syntax resolution should be separate. // Remove the NAME case then. diff --git a/src/com/google/javascript/rhino/jstype/NamedType.java b/src/com/google/javascript/rhino/jstype/NamedType.java index 2b45a790ac1..4d06a8d5414 100644 --- a/src/com/google/javascript/rhino/jstype/NamedType.java +++ b/src/com/google/javascript/rhino/jstype/NamedType.java @@ -227,6 +227,12 @@ int recursionUnsafeHashCode() { */ @Override JSType resolveInternal(ErrorReporter reporter) { + if (!getReferencedType().isUnknownType()) { + // In some cases (e.g. typeof(ns) when the actual type is just a literal object), a NamedType + // is created solely for the purpose of naming an already-known type. When that happens, + // there's nothing to look up, so just resolve the referenced type. + return super.resolveInternal(reporter); + } // TODO(user): Investigate whether it is really necessary to keep two // different mechanisms for resolving named types, and if so, which order diff --git a/test/com/google/javascript/jscomp/TypeCheckTest.java b/test/com/google/javascript/jscomp/TypeCheckTest.java index 3c0f7e314f8..585a29b6456 100644 --- a/test/com/google/javascript/jscomp/TypeCheckTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckTest.java @@ -20213,6 +20213,151 @@ public void testRefinedTypeInNestedShortCircuitingAndOr() { "required: null")); } + public void testTypeofType_notInScope() { + testTypes("var /** typeof ns */ x;", "Parse error. Not in scope: ns"); + } + + public void testTypeofType_namespace() { + testTypes( + lines( + "/** @const */ var ns = {};", // + "var /** typeof ns */ x = ns;")); + } + + public void testTypeofType_namespaceMismatch() { + testTypes( + lines( + "/** @const */ var ns = {};", // + "var /** typeof ns */ x = {};"), + lines( + "initializing variable", // + "found : {}", + "required: {}")); + } + + public void testTypeofType_constructor() { + testTypesWithExterns( + new TestExternsBuilder().addArray().build(), + lines( + "/** @constructor */ function Foo() {}", // + "var /** !Array */ x = [];", + "x.push(Foo);")); + } + + public void testTypeofType_constructorMismatch1() { + testTypesWithExterns( + new TestExternsBuilder().addArray().build(), + lines( + "/** @constructor */ function Foo() {}", // + "var /** !Array<(typeof Foo)> */ x = [];", + "x.push(new Foo());"), + lines( + "actual parameter 1 of Array.prototype.push does not match formal parameter", + "found : Foo", + "required: function(new:Foo): undefined")); + } + + public void testTypeofType_constructorMismatch2() { + testTypesWithExterns( + new TestExternsBuilder().addArray().build(), + lines( + "/** @constructor */ function Foo() {}", + "var /** !Array */ x = [];", + "var /** typeof Foo */ y = Foo;", + "x.push(y);"), + lines( + "actual parameter 1 of Array.prototype.push does not match formal parameter", + "found : function(new:Foo): undefined", + "required: Foo")); + } + + public void testTypeofType_enum() { + testTypes( + lines( + "/** @enum */ var Foo = {A: 1}", // + "var /** typeof Foo */ x = Foo;")); + } + + public void testTypeofType_enumMismatch1() { + testTypes( + lines("/** @enum */ var Foo = {A: 1}", "var /** typeof Foo */ x = Foo.A;"), + lines( + "initializing variable", // + "found : Foo", + "required: enum{Foo}")); + } + + public void testTypeofType_enumMismatch2() { + testTypes( + lines( + "/** @enum */ var Foo = {A: 1}", + "/** @enum */ var Bar = {A: 1}", + "var /** typeof Foo */ x = Bar;"), + lines( + "initializing variable", // + "found : enum{Bar}", + "required: enum{Foo}")); + } + + public void testTypeofType_castNamespaceIncludesPropertiesFromTypeofType() { + testTypes( + lines( + "/** @const */ var ns1 = {};", + "/** @type {string} */ ns1.foo;", + "/** @const */ var ns2 = {};", + "/** @type {number} */ ns2.bar;", + "", + "/** @const {typeof ns2} */ var ns = /** @type {?} */ (ns1);", + "/** @type {null} */ var x = ns.bar;"), + lines( + "initializing variable", // + "found : number", + "required: null")); + } + + public void testTypeofType_castNamespaceDoesNotIncludeOwnProperties() { + testTypes( + lines( + "/** @const */ var ns1 = {};", + "/** @type {string} */ ns1.foo;", + "/** @const */ var ns2 = {};", + "/** @type {number} */ ns2.bar;", + "", + "/** @const {typeof ns2} */ var ns = /** @type {?} */ (ns1);", + "/** @type {null} */ var x = ns.foo;"), + "Property foo never defined on ns"); + } + + public void testTypeofType_namespaceTypeIsAnAliasNotACopy() { + testTypes( + lines( + "/** @const */ var ns1 = {};", + "/** @type {string} */ ns1.foo;", + "", + "/** @const {typeof ns1} */ var ns = /** @type {?} */ (x);", + "", + "/** @type {string} */ ns1.bar;", + "", + "/** @type {null} */ var x = ns.bar;"), + lines( + "initializing variable", // + "found : string", + "required: null")); + } + + public void testTypeofType_namespacedTypeNameResolves() { + testTypes( + lines( + "/** @const */ var ns1 = {};", + "/** @constructor */ ns1.Foo = function() {};", + "/** @const {typeof ns1} */ var ns = /** @type {?} */ (x);", + "/** @type {!ns.Foo} */ var x = null;"), + lines( + "initializing variable", // + "found : null", + "required: ns1.Foo")); + } + public void testAssignOp() { testTypes( lines( diff --git a/test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java b/test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java index 33112fbbf78..de8d5232854 100644 --- a/test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java +++ b/test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java @@ -688,6 +688,36 @@ public void testParseArrayTypeError5() { "Bad type annotation. type not recognized due to syntax error." + BAD_TYPE_WIKI_LINK); } + public void testParseTypeofType1() { + Node node = parse("@type {typeof Foo}*/").getType().getRoot(); + assertEquals(Token.TYPEOF, node.getToken()); + assertEquals(1, node.getChildCount()); + assertEquals(Token.STRING, node.getFirstChild().getToken()); + assertEquals("Foo", node.getFirstChild().getString()); + } + + public void testParseTypeofType2() { + Node node = parse("@type {(typeof Foo)}*/").getType().getRoot(); + assertEquals(Token.TYPEOF, node.getToken()); + assertEquals(1, node.getChildCount()); + assertEquals(Token.STRING, node.getFirstChild().getToken()); + assertEquals("Foo", node.getFirstChild().getString()); + } + + public void testParseTypeofType3() { + Node node = parse("@type {typeof Foo|Bar}*/").getType().getRoot(); + assertEquals(Token.PIPE, node.getToken()); + assertEquals(Token.TYPEOF, node.getFirstChild().getToken()); + assertEquals(Token.STRING, node.getFirstFirstChild().getToken()); + assertEquals("Foo", node.getFirstFirstChild().getString()); + assertEquals(Token.STRING, node.getLastChild().getToken()); + assertEquals("Bar", node.getLastChild().getString()); + assertEquals(Token.BLOCK, node.getLastChild().getFirstChild().getToken()); + assertEquals(Token.TYPEOF, node.getLastChild().getFirstFirstChild().getToken()); + assertEquals(Token.STRING, node.getLastChild().getFirstFirstChild().getFirstChild().getToken()); + assertEquals("Baz", node.getLastChild().getFirstFirstChild().getFirstChild().getString()); + } + private JSType testParseType(String type) { return testParseType(type, type); }