Skip to content

Commit

Permalink
Introduce the typeof operator in JSDoc types.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
concavelenz authored and lauraharker committed Sep 7, 2018
1 parent de799ce commit 233ee19
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 9 deletions.
33 changes: 24 additions & 9 deletions src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java
Expand Up @@ -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);
}
Expand All @@ -2150,33 +2152,36 @@ 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();
}

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();
typeName += stream.getString();
}
}

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);

Expand All @@ -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 :=
Expand Down
16 changes: 16 additions & 0 deletions src/com/google/javascript/rhino/jstype/JSTypeRegistry.java
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/com/google/javascript/rhino/jstype/NamedType.java
Expand Up @@ -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
Expand Down
145 changes: 145 additions & 0 deletions test/com/google/javascript/jscomp/TypeCheckTest.java
Expand Up @@ -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<typeof Foo> */ 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<!Foo> */ 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<number>",
"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(
Expand Down
30 changes: 30 additions & 0 deletions test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java
Expand Up @@ -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<typeof Baz>}*/").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);
}
Expand Down

0 comments on commit 233ee19

Please sign in to comment.