Skip to content

Commit

Permalink
Correct property (dis)ambiguation for dynamically created classes.
Browse files Browse the repository at this point in the history
This change is necessary for type checking and property (dis)ambiguation to
work correctly when class transpilation is moved after type checking.

The core of the change is to expand the logic in ObjectType.isAmbiguousObject()
to return true for cases where the implementation of the object's class or one
of its superclasses isn't fully known.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=215136357
  • Loading branch information
brad4d authored and lauraharker committed Oct 2, 2018
1 parent eb9b3b8 commit cfc9a19
Show file tree
Hide file tree
Showing 8 changed files with 416 additions and 23 deletions.
55 changes: 42 additions & 13 deletions src/com/google/javascript/jscomp/FunctionTypeBuilder.java
Expand Up @@ -408,24 +408,46 @@ private boolean maybeUseNativeClassTemplateNames(JSDocInfo info) {
return false; return false;
} }


/** Infer any supertypes from the JSDocInfo or the passed-in base type. */ /**
FunctionTypeBuilder inferInheritance(@Nullable JSDocInfo info, @Nullable ObjectType baseType) { * Infer any supertypes from the JSDocInfo or the passed-in base type.
*
* @param info JSDoc info that is attached to the type declaration, if any
* @param classExtendsType The type of the extends clause in `class C extends SuperClass {}`, if
* present.
* @return this object
*/
FunctionTypeBuilder inferInheritance(
@Nullable JSDocInfo info, @Nullable ObjectType classExtendsType) {


// base type
if (info != null && info.hasBaseType()) { if (info != null && info.hasBaseType()) {
if (isConstructor) { if (isConstructor) {
ObjectType infoBaseType = ObjectType infoBaseType =
info.getBaseType().evaluate(templateScope, typeRegistry).toMaybeObjectType(); info.getBaseType().evaluate(templateScope, typeRegistry).toMaybeObjectType();
// TODO(sdh): ensure JSDoc's baseType and AST's baseType are compatible if both are set // TODO(sdh): ensure JSDoc's baseType and AST's baseType are compatible if both are set
baseType = infoBaseType; if (infoBaseType.setValidator(new ExtendedTypeValidator())) {
baseType = infoBaseType;
}
} else { } else {
reportWarning(EXTENDS_WITHOUT_TYPEDEF, formatFnName()); reportWarning(EXTENDS_WITHOUT_TYPEDEF, formatFnName());
} }
} } else if (classExtendsType != null && isConstructor) {
if (baseType != null && isConstructor) { // This case is:
if (baseType.setValidator(new ExtendedTypeValidator())) { // // no JSDoc here
this.baseType = baseType; // class extends astBaseType {...}
} //
// It may well be that astBaseType is something dynamically created, like a value passed into
// a function. A common pattern is:
//
// function mixinX(superClass) {
// return class extends superClass {
// ...
// };
// }
// The ExtendedTypeValidator() used in the JSDocInfo case above will report errors for these
// cases, and we don't want that.
// Since astBaseType is an actual value in code rather than an annotation, we can
// rely on validation elsewhere to ensure it is actually defined.
baseType = classExtendsType;
} }


// Implemented interfaces (for constructors only). // Implemented interfaces (for constructors only).
Expand Down Expand Up @@ -471,13 +493,20 @@ FunctionTypeBuilder inferInheritance(@Nullable JSDocInfo info, @Nullable ObjectT
extendedInterfaces.add((ObjectType) maybeInterfaceType); extendedInterfaces.add((ObjectType) maybeInterfaceType);
} }
// de-dupe baseType (from extends keyword) if it's also in @extends jsdoc. // de-dupe baseType (from extends keyword) if it's also in @extends jsdoc.
if (baseType != null && maybeInterfaceType.isSubtypeOf(baseType)) { if (classExtendsType != null && maybeInterfaceType.isSubtypeOf(classExtendsType)) {
baseType = null; classExtendsType = null;
} }
} }
} }
if (baseType != null && baseType.setValidator(new ExtendedTypeValidator())) { if (classExtendsType != null && classExtendsType.setValidator(new ExtendedTypeValidator())) {
extendedInterfaces.add(baseType); // case is:
// /**
// * @interface
// * @extends {OtherInterface}
// */
// class SomeInterface extends astBaseType {}
// Add the explicit extends type to the extended interfaces listed in JSDoc.
extendedInterfaces.add(classExtendsType);
} }
} }


Expand Down
15 changes: 13 additions & 2 deletions src/com/google/javascript/jscomp/TypedScopeCreator.java
Expand Up @@ -1187,8 +1187,19 @@ private ObjectType findSuperClassFromNodes(Node extendsNode, @Nullable JSDocInfo
} }
} }


if (ctorType != null && (ctorType.isConstructor() || ctorType.isInterface())) { if (ctorType != null) {
return ctorType.toMaybeFunctionType().getInstanceType(); if (ctorType.isConstructor() || ctorType.isInterface()) {
return ctorType.toMaybeFunctionType().getInstanceType();
} else if (ctorType.isUnknownType()) {
// The constructor could have an unknown type for cases where it is dynamically
// created or passed in from elsewhere.
// e.g. with a mixin pattern
// function mixinSomething(ctor) {
// return class extends ctor { ... };
// }
// In that case consider the super class instance type to be unknown.
return ctorType.toMaybeObjectType();
}
} }


// We couldn't determine the type, so for TypedScope creation purposes we will treat it as if // We couldn't determine the type, so for TypedScope creation purposes we will treat it as if
Expand Down
66 changes: 66 additions & 0 deletions src/com/google/javascript/rhino/jstype/FunctionType.java
Expand Up @@ -89,6 +89,14 @@ private enum PropAccess {
DICT DICT
} }


enum ConstructorAmbiguity {
UNKNOWN,
CONSTRUCTS_AMBIGUOUS_OBJECTS,
CONSTRUCTS_UNAMBIGUOUS_OBJECTS
}

private ConstructorAmbiguity constructorAmbiguity = ConstructorAmbiguity.UNKNOWN;

/** {@code [[Call]]} property. */ /** {@code [[Call]]} property. */
private ArrowType call; private ArrowType call;


Expand Down Expand Up @@ -1447,4 +1455,62 @@ public final ImmutableList<TemplateType> getConstructorOnlyTemplateParameters()
} }
return ctorKeys.build(); return ctorKeys.build();
} }

boolean createsAmbiguousObjects() {
if (this.constructorAmbiguity == ConstructorAmbiguity.UNKNOWN) {
constructorAmbiguity = calculateConstructorAmbiguity();
}
return constructorAmbiguity == ConstructorAmbiguity.CONSTRUCTS_AMBIGUOUS_OBJECTS;
}

private ConstructorAmbiguity calculateConstructorAmbiguity() {
final ConstructorAmbiguity constructorAmbiguity;
if (isUnknownType()) {
constructorAmbiguity = ConstructorAmbiguity.CONSTRUCTS_AMBIGUOUS_OBJECTS;
} else if (isNativeObjectType()) {
// native types other than unknown are never ambiguous
constructorAmbiguity = ConstructorAmbiguity.CONSTRUCTS_UNAMBIGUOUS_OBJECTS;
} else {
FunctionType superConstructor = getSuperClassConstructor();
if (superConstructor == null) {
// TODO(bradfordcsmith): Why is superConstructor ever null here?
constructorAmbiguity = ConstructorAmbiguity.CONSTRUCTS_AMBIGUOUS_OBJECTS;
} else if (superConstructor.createsAmbiguousObjects()) {
// Subclasses of ambiguous objects are also ambiguous
constructorAmbiguity = ConstructorAmbiguity.CONSTRUCTS_AMBIGUOUS_OBJECTS;
} else if (source != null) {
// We can see the definition of the class, so we know all properties it directly declares
// or references.
// The same is true for its superclass (previous condition).
constructorAmbiguity = ConstructorAmbiguity.CONSTRUCTS_UNAMBIGUOUS_OBJECTS;
} else if (isDelegateProxy()) {
// Type was created by the compiler as a proxy that inherits from the real type that was in
// the code.
// Since we've made it this far, we know the real type creates unambiguous objects.
// Therefore, the proxy does, too.
constructorAmbiguity = ConstructorAmbiguity.CONSTRUCTS_UNAMBIGUOUS_OBJECTS;
} else {
// Type was created directly from JSDoc without a function or class literal.
// e.g.
// /**
// * @constructor
// * @param {string} x
// * @implements {SomeInterface}
// */
// const MyImpl = createMyImpl();
// The actual properties on this class are hidden from us, so we must consider it ambiguous.
constructorAmbiguity = ConstructorAmbiguity.CONSTRUCTS_AMBIGUOUS_OBJECTS;
}
}
return constructorAmbiguity;
}

// See also TypedScopeCreator.DELEGATE_PROXY_SUFFIX
// Unfortunately we cannot use that constant here.
private static final String DELEGATE_SUFFIX = ObjectType.createDelegateSuffix("Proxy");

private boolean isDelegateProxy() {
// TODO(bradfordcsmith): There should be a better way to determine that we have a proxy type.
return hasReferenceName() && getReferenceName().endsWith(DELEGATE_SUFFIX);
}
} }
Expand Up @@ -188,6 +188,11 @@ public Iterable<ObjectType> getCtorExtendedInterfaces() {
return getConstructor().getExtendedInterfaces(); return getConstructor().getExtendedInterfaces();
} }


@Override
public boolean isAmbiguousObject() {
return getConstructor().createsAmbiguousObjects();
}

// The owner will always be a resolved type, so there's no need to set // The owner will always be a resolved type, so there's no need to set
// the constructor in resolveInternal. // the constructor in resolveInternal.
// (it would lead to infinite loops if we did). // (it would lead to infinite loops if we did).
Expand Down
2 changes: 1 addition & 1 deletion src/com/google/javascript/rhino/jstype/ObjectType.java
Expand Up @@ -252,7 +252,7 @@ public static String createDelegateSuffix(String suffix) {
return "(" + suffix + ")"; return "(" + suffix + ")";
} }


public final boolean isAmbiguousObject() { public boolean isAmbiguousObject() {
return !hasReferenceName(); return !hasReferenceName();
} }


Expand Down
3 changes: 1 addition & 2 deletions test/com/google/javascript/jscomp/Es6RewriteClassTest.java
Expand Up @@ -835,7 +835,6 @@ public void testExtendsInExterns() {


@Test @Test
public void testExtendForwardDeclaredClass() { public void testExtendForwardDeclaredClass() {
ignoreWarnings(TypeCheck.POSSIBLE_INEXISTENT_PROPERTY);
enableClosurePass(); enableClosurePass();
test( test(
srcs( srcs(
Expand All @@ -851,7 +850,7 @@ public void testExtendForwardDeclaredClass() {
" return ns.D.apply(this, arguments) || this;", " return ns.D.apply(this, arguments) || this;",
"};", "};",
"$jscomp.inherits(C, ns.D);")), "$jscomp.inherits(C, ns.D);")),
warning(FunctionTypeBuilder.RESOLVED_TAG_EMPTY)); warning(TypeCheck.POSSIBLE_INEXISTENT_PROPERTY));
} }


@Test @Test
Expand Down

0 comments on commit cfc9a19

Please sign in to comment.