Skip to content

Commit

Permalink
Validate dynamic extends clauses during TypeCheck
Browse files Browse the repository at this point in the history
Allows extending things other than qualified names, provided `@extends` is specified in the JSDoc.  Emits a warning if no such annotation is given, or else checks that the extended type is in fact compatible with any declared supertype.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=203523284
  • Loading branch information
shicks authored and lauraharker committed Jul 6, 2018
1 parent ad9860e commit 407cb25
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 33 deletions.
4 changes: 3 additions & 1 deletion src/com/google/javascript/jscomp/TypeCheck.java
Expand Up @@ -2001,7 +2001,9 @@ private void visitClass(NodeTraversal t, Node n) {
// Ensure that the `extends` clause is actually a constructor or interface. If it is, but // Ensure that the `extends` clause is actually a constructor or interface. If it is, but
// it's the wrong one then checkConstructor or checkInterface will warn. // it's the wrong one then checkConstructor or checkInterface will warn.
JSType superType = extendsClause.getJSType(); JSType superType = extendsClause.getJSType();
if (!(superType.isConstructor() || superType.isInterface())) { if (superType.isConstructor() || superType.isInterface()) {
validator.expectExtends(n, functionType, superType.toMaybeFunctionType());
} else {
compiler.report( compiler.report(
t.makeError( t.makeError(
n, n,
Expand Down
38 changes: 36 additions & 2 deletions src/com/google/javascript/jscomp/TypeValidator.java
Expand Up @@ -678,8 +678,7 @@ void expectSuperType(NodeTraversal t, Node n, ObjectType superObject,
superObject, declaredSuper, superObject, declaredSuper,
report(t.makeError(n, MISSING_EXTENDS_TAG_WARNING, subObject.toString()))); report(t.makeError(n, MISSING_EXTENDS_TAG_WARNING, subObject.toString())));
} else { } else {
mismatch(n, "mismatch in declaration of superclass type", mismatch(n, "mismatch in declaration of superclass type", superObject, declaredSuper);
superObject, declaredSuper);
} }


// Correct the super type. // Correct the super type.
Expand All @@ -689,6 +688,41 @@ void expectSuperType(NodeTraversal t, Node n, ObjectType superObject,
} }
} }


/**
* Expect that an ES6 class's extends clause is actually a supertype of the given class.
*
* @param n The node where warnings should point to.
* @param subCtor The sub constructor type.
* @param superCtor The expected super constructor.
*/
void expectExtends(Node n, FunctionType subCtor, FunctionType superCtor) {
if (superCtor == null || (!superCtor.isConstructor() && !superCtor.isInterface())) {
// toMaybeFunctionType failed, or we've got a loose type. Let it go for now.
return;
}
if (superCtor.isConstructor() != subCtor.isConstructor()) {
// Don't bother looking if one is a constructor and the other is an interface.
// We'll report an error elsewhere.
return;
}
ObjectType superInstance = superCtor.getInstanceType();
if (subCtor.isConstructor()) {
// There should be exactly one superclass, and it needs to have this constructor.
ObjectType declaredSuper = subCtor.getSuperClassConstructor().getInstanceType();
if (!superInstance.isEquivalentTo(declaredSuper)) {
mismatch(n, "mismatch in declaration of superclass type", superInstance, declaredSuper);
}
} else if (subCtor.isInterface()) {
// Find an equivalent constructor in the superinterfaces. There may have been multiple
// super-interfaces marked, but we can't know which was intended so just give the error
// on the first one.
if (!subCtor.explicitlyImplOrExtInterface(superCtor)) {
ObjectType extended = subCtor.getExtendedInterfaces().get(0);
mismatch(n, "mismatch in declaration of superclass type", superInstance, extended);
}
}
}

/** /**
* Expect that it's valid to assign something to a given type's prototype. * Expect that it's valid to assign something to a given type's prototype.
* *
Expand Down
24 changes: 17 additions & 7 deletions src/com/google/javascript/jscomp/TypedScopeCreator.java
Expand Up @@ -155,6 +155,12 @@ final class TypedScopeCreator implements ScopeCreator, StaticSymbolTable<TypedVa
"JSC_INCOMPATIBLE_ALIAS_ANNOTATION", "JSC_INCOMPATIBLE_ALIAS_ANNOTATION",
"Annotation {0} on {1} incompatible with aliased type."); "Annotation {0} on {1} incompatible with aliased type.");


static final DiagnosticType DYNAMIC_EXTENDS_WITHOUT_JSDOC =
DiagnosticType.warning(
"JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC",
"The right-hand side of an extends clause must be a qualified name, or else @extends must"
+ " be specified in JSDoc");

static final DiagnosticGroup ALL_DIAGNOSTICS = new DiagnosticGroup( static final DiagnosticGroup ALL_DIAGNOSTICS = new DiagnosticGroup(
DELEGATE_PROXY_SUFFIX, DELEGATE_PROXY_SUFFIX,
MALFORMED_TYPEDEF, MALFORMED_TYPEDEF,
Expand All @@ -164,7 +170,8 @@ final class TypedScopeCreator implements ScopeCreator, StaticSymbolTable<TypedVa
CONSTRUCTOR_EXPECTED, CONSTRUCTOR_EXPECTED,
UNKNOWN_LENDS, UNKNOWN_LENDS,
LENDS_ON_NON_OBJECT, LENDS_ON_NON_OBJECT,
INCOMPATIBLE_ALIAS_ANNOTATION); INCOMPATIBLE_ALIAS_ANNOTATION,
DYNAMIC_EXTENDS_WITHOUT_JSDOC);


private final AbstractCompiler compiler; private final AbstractCompiler compiler;
private final ErrorReporter typeParsingErrorReporter; private final ErrorReporter typeParsingErrorReporter;
Expand Down Expand Up @@ -966,7 +973,7 @@ private FunctionType createClassTypeFromNodes(


// Look at the extends clause and/or JSDoc info to find a super class. Use generics from the // Look at the extends clause and/or JSDoc info to find a super class. Use generics from the
// JSDoc to supplement the extends type when available. // JSDoc to supplement the extends type when available.
ObjectType baseType = findSuperClassFromNodes(extendsClause); ObjectType baseType = findSuperClassFromNodes(extendsClause, info);
builder.inferInheritance(info, baseType); builder.inferInheritance(info, baseType);


// Look for an explicit constructor. // Look for an explicit constructor.
Expand Down Expand Up @@ -1012,7 +1019,7 @@ private FunctionType createClassTypeFromNodes(
* be determined. * be determined.
*/ */
@Nullable @Nullable
private ObjectType findSuperClassFromNodes(Node extendsNode) { private ObjectType findSuperClassFromNodes(Node extendsNode, @Nullable JSDocInfo info) {
if (extendsNode.isEmpty()) { if (extendsNode.isEmpty()) {
// No extends clause: return null. // No extends clause: return null.
return null; return null;
Expand All @@ -1025,10 +1032,13 @@ private ObjectType findSuperClassFromNodes(Node extendsNode) {
if (var != null) { if (var != null) {
ctorType = var.getType(); ctorType = var.getType();
} }
} else if (extendsNode.isCall()) { } else {
// TODO(sdh): Do some limited type inference to get the return type for a mixin? // Anything TypedScopeCreator can infer has already been read off the AST. This is likely
// The difficulty is that mixin functions are likely to be generic, so we need to be at // a CALL or GETELEM, which are unknown until TypeInference. Instead, ignore it for now,
// least a little sophisticated here. // require an @extends tag in the JSDoc, and verify correctness in TypeCheck.
if (info == null || !info.hasBaseType()) {
report(JSError.make(extendsNode, DYNAMIC_EXTENDS_WITHOUT_JSDOC));
}
} }
} }
if (ctorType != null && (ctorType.isConstructor() || ctorType.isInterface())) { if (ctorType != null && (ctorType.isConstructor() || ctorType.isInterface())) {
Expand Down
90 changes: 67 additions & 23 deletions test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java
Expand Up @@ -2211,13 +2211,16 @@ public void testClassSyntaxRecordWithPropertyMismatch() {
} }


public void testClassJSDocExtendsInconsistentWithExtendsClause() { public void testClassJSDocExtendsInconsistentWithExtendsClause() {
// TODO(sdh): Should be an error.
testTypes( testTypes(
lines( lines(
"class Bar {}", // "class Bar {}", //
"class Baz {}", "class Baz {}",
"/** @extends {Bar} */", "/** @extends {Bar} */",
"class Foo extends Baz {}")); "class Foo extends Baz {}"),
lines(
"mismatch in declaration of superclass type",
"found : Baz",
"required: Bar"));
} }


public void testClassJSDocExtendsWithMissingExtendsClause() { public void testClassJSDocExtendsWithMissingExtendsClause() {
Expand All @@ -2237,11 +2240,16 @@ public void testClassExtendsGetElem() {
"var obj = {};", "var obj = {};",
"class Bar extends obj['abc'] {}", "class Bar extends obj['abc'] {}",
"var /** !Foo */ foo = new Bar();"), "var /** !Foo */ foo = new Bar();"),
// TODO(sdh): It would be good to recognize that Bar actually *is* a Foo. new String[] {
lines( "The right-hand side of an extends clause must be a qualified name, or else @extends must"
"initializing variable", + " be specified in JSDoc",
"found : Bar", // TODO(sdh): This is a little confusing, but there doesn't seem to be a way to suppress
"required: Foo")); // this additional error.
lines(
"initializing variable",
"found : Bar",
"required: Foo"),
});
} }


public void testClassExtendsFunctionCall() { public void testClassExtendsFunctionCall() {
Expand All @@ -2252,27 +2260,61 @@ public void testClassExtendsFunctionCall() {
"function mixin() {}", "function mixin() {}",
"class Bar extends mixin() {}", "class Bar extends mixin() {}",
"var /** !Foo */ foo = new Bar();"), "var /** !Foo */ foo = new Bar();"),
// TODO(sdh): It would be good to recognize that Bar actually *is* a Foo. new String[] {
"The right-hand side of an extends clause must be a qualified name, or else @extends must"
+ " be specified in JSDoc",
// TODO(sdh): This is a little confusing, but there doesn't seem to be a way to suppress
// this additional error.
lines(
"initializing variable",
"found : Bar",
"required: Foo"),
});
}

public void testClassInterfaceExtendsFunctionCall() {
testTypes(
lines( lines(
"initializing variable", "/** @interface */",
"found : Bar", "class Foo {}",
"required: Foo")); "/** @return {function(new:Foo)} */",
"function mixin() {}",
"/** @interface */",
"class Bar extends mixin() {}"),
"The right-hand side of an extends clause must be a qualified name, or else @extends must"
+ " be specified in JSDoc");
} }


public void testClassExtendsFunctionCallOverridesMethod() { public void testClassExtendsFunctionCallWithJSDoc() {
testTypes( testTypes(
lines( lines(
"class Foo {", "class Foo {",
" /** @return {number} */ foo() {}", " constructor() { /** @type {number} */ this.foo; }",
"}", "}",
"/** @return {function(new:Foo)} */", "/** @return {function(new:Foo)} */",
"function mixin() {}", "function mixin() {}",
"class Bar extends mixin() {", "/** @extends {Foo} */",
" /** @override */", "class Bar extends mixin() {}",
" foo() {}", "var /** null */ x = new Bar().foo;"),
"}"), lines(
// TODO(sdh): We should respect the type of the function return if possible. "initializing variable", //
"property foo not defined on any superclass of Bar"); "found : number",
"required: null"));
}

public void testClassExtendsFunctionCallWithIncompatibleJSDoc() {
testTypes(
lines(
"class Foo {}",
"class Baz {}",
"/** @return {function(new:Foo)} */",
"function mixin() {}",
"/** @extends {Baz} */",
"class Bar extends mixin() {}"),
lines(
"mismatch in declaration of superclass type", //
"found : Foo",
"required: Baz"));
} }


public void testClassImplementsInterface() { public void testClassImplementsInterface() {
Expand All @@ -2297,7 +2339,7 @@ public void testClassMissingInterfaceMethod() {
"property foo on interface Foo is not implemented by type Bar"); "property foo on interface Foo is not implemented by type Bar");
} }


public void testClassAbstractClassNeedNonExplicitlyOverrideUnimplementedInterfaceMethods() { public void testClassAbstractClassNeedNotExplicitlyOverrideUnimplementedInterfaceMethods() {
testTypes( testTypes(
lines( lines(
"/** @interface */", "/** @interface */",
Expand Down Expand Up @@ -2363,6 +2405,7 @@ public void testClassInheritedInterfaceMethod() {
} }


public void testClassMixinAllowsNonOverriddenInterfaceMethods() { public void testClassMixinAllowsNonOverriddenInterfaceMethods() {
// See cl/188076790 and b/74120976
testTypes( testTypes(
lines( lines(
"/** @interface */", "/** @interface */",
Expand All @@ -2373,7 +2416,7 @@ public void testClassMixinAllowsNonOverriddenInterfaceMethods() {
// TODO(sdh): Intersection types would allow annotating this correctly. // TODO(sdh): Intersection types would allow annotating this correctly.
"/** @return {function(new:Bar)} */", "/** @return {function(new:Bar)} */",
"function mixin() {}", "function mixin() {}",
"/** @implements {Foo} */", "/** @extends {Bar} @implements {Foo} */",
"class Baz extends mixin() {}"), "class Baz extends mixin() {}"),
// TODO(sdh): This is supposed to be allowed. // TODO(sdh): This is supposed to be allowed.
"property foo on interface Foo is not implemented by type Baz"); "property foo on interface Foo is not implemented by type Baz");
Expand Down Expand Up @@ -2644,7 +2687,7 @@ public void testClassStaticMethodCalledOnInstance() {
"}", "}",
"new C().m();"), "new C().m();"),
// TODO(sdh): This error message should be different from the converse case. // TODO(sdh): This error message should be different from the converse case.
// Probably should say "Instance property m never defined on C". // Probably should say "Property m never defined on instances of C".
"Property m never defined on C"); "Property m never defined on C");
} }


Expand All @@ -2656,7 +2699,8 @@ public void testClassInstanceMethodCalledOnClass() {
"}", "}",
"C.m();"), "C.m();"),
// TODO(sdh): This error message should be different from the converse case. // TODO(sdh): This error message should be different from the converse case.
// Probably should say "Static property m never defined on C". // Maybe should say "Property m never defined on namespace C". (but we need to think
// about union types, etc)
"Property m never defined on C"); "Property m never defined on C");
} }


Expand Down

0 comments on commit 407cb25

Please sign in to comment.