From d33179debf96b318a7229117c5a9e7176106b08d Mon Sep 17 00:00:00 2001 From: nickreid Date: Mon, 13 Aug 2018 15:29:40 -0700 Subject: [PATCH] Fixes some violations of the `equals-hashCode` contract in `NamedType`. `NamedType` instances used to only be compared based on their string references. Now, if they have been successfully resolved they will be compared based on their underlying (proxied) type instances, else fall back to comparing strings. This change is not a complete fix because: - `NamedType` equality should also consider scopes - There is some ambiguity between forward declared types and `NoResolvedType` I haven't been able to unravel. Fixing these has been punted in favour of an incremental improvement. As a confirmation that this change is positive and to help prevent the contract from slipping further, `assertTypeEquals` in the "jstype" unit tests is being updated to compare `hashCode()` in addition to `equals()`. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=208553812 --- .../javascript/rhino/jstype/JSType.java | 47 +++-- .../javascript/rhino/jstype/NamedType.java | 6 +- .../javascript/rhino/testing/Asserts.java | 92 +++++++--- .../rhino/testing/BaseJSTypeTestCase.java | 4 +- .../javascript/jscomp/TypeCheckTest.java | 53 ++++-- .../javascript/rhino/jstype/JSTypeTest.java | 152 +++++++++++----- .../rhino/jstype/NamedTypeTest.java | 162 ++++++++++++++++-- 7 files changed, 417 insertions(+), 99 deletions(-) diff --git a/src/com/google/javascript/rhino/jstype/JSType.java b/src/com/google/javascript/rhino/jstype/JSType.java index 6fa6dcfea3f..0a990870966 100644 --- a/src/com/google/javascript/rhino/jstype/JSType.java +++ b/src/com/google/javascript/rhino/jstype/JSType.java @@ -690,7 +690,8 @@ boolean checkEquivalenceHelper( if (this.isNoResolvedType() && that.isNoResolvedType()) { if (this.isNamedType() && that.isNamedType()) { return Objects.equals( - ((NamedType) this).getReferenceName(), ((NamedType) that).getReferenceName()); + this.toMaybeNamedType().getReferenceName(), // + that.toMaybeNamedType().getReferenceName()); } else { return true; } @@ -738,8 +739,14 @@ boolean checkEquivalenceHelper( if (isNominalType() && that.isNominalType()) { // TODO(johnlenz): is this valid across scopes? - return getConcreteNominalTypeName(this.toObjectType()) - .equals(getConcreteNominalTypeName(that.toObjectType())); + @Nullable String nameOfThis = deepestResolvedTypeNameOf(this.toObjectType()); + @Nullable String nameOfThat = deepestResolvedTypeNameOf(that.toObjectType()); + + if ((nameOfThis == null) && (nameOfThat == null)) { + // These are two anonymous types that were masquerading as nominal, so don't compare names. + } else { + return Objects.equals(nameOfThis, nameOfThat); + } } if (isTemplateType() && that.isTemplateType()) { @@ -769,15 +776,16 @@ boolean checkEquivalenceHelper( } // Named types may be proxies of concrete types. - private String getConcreteNominalTypeName(ObjectType objType) { - if (objType instanceof ProxyObjectType) { - ObjectType internal = ((ProxyObjectType) objType) - .getReferencedObjTypeInternal(); - if (internal != null && internal.isNominalType()) { - return getConcreteNominalTypeName(internal); - } + @Nullable + private String deepestResolvedTypeNameOf(ObjectType objType) { + if (!objType.isResolved() || !(objType instanceof ProxyObjectType)) { + return objType.getReferenceName(); } - return objType.getReferenceName(); + + ObjectType internal = ((ProxyObjectType) objType).getReferencedObjTypeInternal(); + return (internal != null && internal.isNominalType()) + ? deepestResolvedTypeNameOf(internal) + : null; } /** @@ -1661,11 +1669,26 @@ void setResolvedTypeInternal(JSType type) { resolved = true; } - /** Whether the type has been resolved. */ + /** + * Returns whether the type has undergone resolution. + * + *

A value of {@code true} does not indicate that resolution was successful, only that + * it was attempted and has finished. + */ public final boolean isResolved() { return resolved; } + /** Returns whether the type has undergone resolution and resolved to a "useful" type. */ + public final boolean isSuccessfullyResolved() { + return isResolved() && !isNoResolvedType(); + } + + /** Returns whether the type has undergone resolution and resolved to a "useless" type. */ + public final boolean isUnsuccessfullyResolved() { + return isResolved() && isNoResolvedType(); + } + /** * A null-safe resolve. * @see #resolve diff --git a/src/com/google/javascript/rhino/jstype/NamedType.java b/src/com/google/javascript/rhino/jstype/NamedType.java index 4261b49b057..2b45a790ac1 100644 --- a/src/com/google/javascript/rhino/jstype/NamedType.java +++ b/src/com/google/javascript/rhino/jstype/NamedType.java @@ -217,7 +217,9 @@ public boolean isNominalType() { @Override int recursionUnsafeHashCode() { - return nominalHashCode(this); + // Recall that equality on `NamedType` uses only the name until successful resolution, then + // delegates to the resolved type. + return isSuccessfullyResolved() ? super.recursionUnsafeHashCode() : nominalHashCode(this); } /** @@ -253,7 +255,7 @@ JSType resolveInternal(ErrorReporter reporter) { JSType result = getReferencedType(); - if (isResolved() && !result.isNoResolvedType()) { + if (isSuccessfullyResolved()) { int numKeys = result.getTemplateTypeMap().numUnfilledTemplateKeys(); if (result.isObjectType() && (templateTypes != null && !templateTypes.isEmpty()) diff --git a/src/com/google/javascript/rhino/testing/Asserts.java b/src/com/google/javascript/rhino/testing/Asserts.java index 4e03243d1e3..eb914759a45 100644 --- a/src/com/google/javascript/rhino/testing/Asserts.java +++ b/src/com/google/javascript/rhino/testing/Asserts.java @@ -42,6 +42,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; +import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import com.google.javascript.rhino.ErrorReporter; import com.google.javascript.rhino.jstype.JSType; @@ -73,15 +74,26 @@ public static void assertTypeNotEquals(JSType a, JSType b) { } public static void assertTypeNotEquals(String message, JSType a, JSType b) { + if (!message.isEmpty()) { + message += "\n"; + } + String aDebug = debugStringOf(a); + String bDebug = debugStringOf(b); + Assert.assertFalse( - message + (message.isEmpty() ? "" : "\n") - + " Equals is not symmetric.\n" - + "Type: " + b + "\n", + lines( + message, + "Type should not be equal.", + "First type : " + aDebug, + "was equal to second type: " + bDebug), a.isEquivalentTo(b)); Assert.assertFalse( - message + (message.isEmpty() ? "" : "\n") - + " Equals is not symmetric.\n" - + "Type: " + b + "\n", + lines( + message, + "Inequality was found to be asymmetric.", + "Type should not be equal.", + "First type : " + aDebug, + "was equal to second type: " + bDebug), b.isEquivalentTo(a)); } @@ -89,20 +101,47 @@ public static void assertTypeEquals(JSType a, JSType b) { assertTypeEquals("", a, b); } - public static void assertTypeEquals(String message, JSType a, JSType b) { - checkNotNull(a); - checkNotNull(b); + public static void assertTypeEquals(String message, JSType expected, JSType actual) { + checkNotNull(expected); + checkNotNull(actual); + + if (!message.isEmpty()) { + message += "\n"; + } + String expectedDebug = debugStringOf(expected); + String actualDebug = debugStringOf(actual); + Assert.assertTrue( - message + (message.isEmpty() ? "" : "\n") - + "Expected: " + a + "\n" - + "Actual : " + b, - a.isEquivalentTo(b, true)); + lines( + message, // + "Types should be equal.", + "Expected: " + expectedDebug, + "Actual : " + actualDebug), + expected.isEquivalentTo(actual, true)); Assert.assertTrue( - message - + " Equals is not symmetric.\n" - + "Expected: " + b + "\n" - + "Actual : " + a, - b.isEquivalentTo(a, true)); + lines( + message, + "Equality was found to be asymmetric.", + "Types should be equal.", + "Expected: " + expectedDebug, + "Actual : " + actualDebug), + actual.isEquivalentTo(expected, true)); + + // Recall the `equals-hashCode` contract: if two objects report being equal, their hashcodes + // must also be equal. Breaking this contract breaks structures that depend on it (e.g. + // `HashMap`). + // + // The implementations of `hashCode()` and `equals()` are a bit tricky in some of the `JSType` + // classes, so we want to check specifically that this contract is fulfilled in all the unit + // tests involving them. + Assert.assertEquals( + lines( + message, + "Types violate the `equals-hashCode`: types report e `hashCode()`s do not match.", + "Expected: " + expected.hashCode() + " [on " + expectedDebug + "]", + "Actual : " + actual.hashCode() + " [on " + actualDebug + "]"), + expected.hashCode(), + actual.hashCode()); } public static void @@ -120,10 +159,9 @@ public static void assertTypeEquals(String message, JSType a, JSType b) { * should have trivial solutions (getGreatestSubtype, isEquivalentTo, etc) */ public static void assertEquivalenceOperations(JSType a, JSType b) { - Assert.assertTrue(a.isEquivalentTo(b)); - Assert.assertTrue(a.isEquivalentTo(a)); - Assert.assertTrue(b.isEquivalentTo(b)); - Assert.assertTrue(b.isEquivalentTo(a)); + assertTypeEquals(a, a); + assertTypeEquals(a, b); + assertTypeEquals(b, b); Assert.assertTrue(a.isSubtypeOf(b)); Assert.assertTrue(a.isSubtypeOf(a)); @@ -145,4 +183,14 @@ public static void assertEquivalenceOperations(JSType a, JSType b) { Assert.assertTrue(b.canCastTo(b)); Assert.assertTrue(b.canCastTo(a)); } + + private static String debugStringOf(JSType type) { + return (type == null) + ? "" + : type.toString() + " [instanceof " + type.getClass().getName() + "]"; + } + + private static final String lines(String... lines) { + return Joiner.on("\n").join(lines); + } } diff --git a/src/com/google/javascript/rhino/testing/BaseJSTypeTestCase.java b/src/com/google/javascript/rhino/testing/BaseJSTypeTestCase.java index ee3f588bc4b..46388effd18 100644 --- a/src/com/google/javascript/rhino/testing/BaseJSTypeTestCase.java +++ b/src/com/google/javascript/rhino/testing/BaseJSTypeTestCase.java @@ -55,6 +55,8 @@ import junit.framework.TestCase; public abstract class BaseJSTypeTestCase extends TestCase { + protected static final String FORWARD_DECLARED_TYPE_NAME = "forwardDeclared"; + protected static final Joiner LINE_JOINER = Joiner.on('\n'); protected JSTypeRegistry registry; @@ -107,7 +109,7 @@ public abstract class BaseJSTypeTestCase extends TestCase { protected void setUp() throws Exception { super.setUp(); errorReporter = new TestErrorReporter(null, null); - registry = new JSTypeRegistry(errorReporter, ImmutableSet.of("forwardDeclared")); + registry = new JSTypeRegistry(errorReporter, ImmutableSet.of(FORWARD_DECLARED_TYPE_NAME)); initTypes(); } diff --git a/test/com/google/javascript/jscomp/TypeCheckTest.java b/test/com/google/javascript/jscomp/TypeCheckTest.java index 290bdef55a2..502d6fcd341 100644 --- a/test/com/google/javascript/jscomp/TypeCheckTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckTest.java @@ -20137,23 +20137,52 @@ public void testShadowedForwardReferenceHoisted() { "}")); } - public void testShadowedForwardReference() { - // TODO(sdh): fix this. + public void testTypeDeclarationsShadowOneAnotherWithFunctionScoping() { + // TODO(b/110538992): Accuracy of shadowing is unclear here. b/110741413 may be confusing the + // issue. + + // `C` at (2) should refer to `C` at (3) and not `C` at (1). Otherwise the assignment at (4) + // would be invalid. testTypes( lines( - "/** @constructor */ var C = function() { /** @type {string} */ this.prop = 's'; };", + "/** @constructor */", + "var A = function() { };", + "", + "/**", + " * @constructor", + " * @extends {A}", + " */", + "var C = function() { };", // (1) + "", "var fn = function() {", - "/** @type {C} */ var x", - "/** @constructor */ var C = function() { /** @type {number} */ this.prop = 1; };", - "/** @return {C} */ function fn() { return new C(); };", - "/** @type {number} */ var n1 = x.prop;", - "/** @type {number} */ var n2 = new C().prop;", + " /** @type {?C} */ var innerC;", // (2) "", - "}"), + " /** @constructor */", + " var C = function() { };", // (3) + "", + " innerC = new C();", // (4) + "}")); + } + + public void testTypeDeclarationsShadowOneAnotherWithFunctionScopingConsideringHoisting() { + // TODO(b/110538992): Accuracy of shadowing is unclear here. b/110741413 may be confusing the + // issue. + + // `C` at (3) should refer to `C` at (2) and not `C` at (1). Otherwise return the value at (4) + // would be invalid. + testTypes( lines( - "initializing variable", // - "found : string", // should be "number" - "required: number")); + "/** @constructor */", + "var C = function() { };", // (1) + "", + "var fn = function() {", + " /** @constructor */", + " var C = function() { };", // (2) + "", + // This function will be hoisted above, and therefore type-analysed before, (2). + " /** @return {!C} */", // (3) + " function hoisted() { return new C(); };", // (4) + "}")); } public void testRefinedTypeInNestedShortCircuitingAndOr() { diff --git a/test/com/google/javascript/rhino/jstype/JSTypeTest.java b/test/com/google/javascript/rhino/jstype/JSTypeTest.java index 5a15db65ff7..31f24d69e60 100644 --- a/test/com/google/javascript/rhino/jstype/JSTypeTest.java +++ b/test/com/google/javascript/rhino/jstype/JSTypeTest.java @@ -48,6 +48,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.javascript.rhino.ErrorReporter; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.JSDocInfo.Visibility; import com.google.javascript.rhino.JSDocInfoBuilder; @@ -4607,14 +4608,20 @@ public void testSymmetryOfLeastSupertype() { JSType aOnB = typeA.getLeastSupertype(typeB); JSType bOnA = typeB.getLeastSupertype(typeA); - // Use a custom assert message instead of the normal assertTypeEquals, - // to make it more helpful. + // TODO(b/110226422): This should use `assertTypeEquals` but at least one of the cases + // doesn't have matching `hashCode()` values. assertTrue( - String.format("getLeastSupertype not symmetrical:\n" + - "typeA: %s\ntypeB: %s\n" + - "a.getLeastSupertype(b): %s\n" + - "b.getLeastSupertype(a): %s\n", - typeA, typeB, aOnB, bOnA), + String.format( + lines( + "getLeastSupertype not symmetrical:", + "typeA: %s", + "typeB: %s", + "a.getLeastSupertype(b): %s", + "b.getLeastSupertype(a): %s"), + typeA, + typeB, + aOnB, + bOnA), aOnB.isEquivalentTo(bOnA)); } } @@ -4637,14 +4644,20 @@ public void testSymmetryOfGreatestSubtype() { JSType aOnB = typeA.getGreatestSubtype(typeB); JSType bOnA = typeB.getGreatestSubtype(typeA); - // Use a custom assert message instead of the normal assertTypeEquals, - // to make it more helpful. + // TODO(b/110226422): This should use `assertTypeEquals` but at least one of the cases + // doesn't have matching `hashCode()` values. assertTrue( - String.format("getGreatestSubtype not symmetrical:\n" + - "typeA: %s\ntypeB: %s\n" + - "a.getGreatestSubtype(b): %s\n" + - "b.getGreatestSubtype(a): %s\n", - typeA, typeB, aOnB, bOnA), + String.format( + lines( + "getGreatestSubtype not symmetrical:", + "typeA: %s", + "typeB: %s", + "a.getGreatestSubtype(b): %s", + "b.getGreatestSubtype(a): %s"), + typeA, + typeB, + aOnB, + bOnA), aOnB.isEquivalentTo(bOnA)); } } @@ -4800,40 +4813,55 @@ public void testNamedTypeEquals2() { assertSame(resolvedB, realB); } - /** - * Tests the {@link NamedType#equals} function against other types - * when it's forward-declared. - */ - public void testForwardDeclaredNamedTypeEquals() { - // test == if references are equal - NamedType a = new NamedType(EMPTY_SCOPE, registry, "forwardDeclared", "source", 1, 0); - NamedType b = new NamedType(EMPTY_SCOPE, registry, "forwardDeclared", "source", 1, 0); + public void testMeaningOfUnresolved() { + // Given + JSType underTest = new UnitTestingJSType(registry); - assertTypeEquals(a, b); + // When + // No resolution. - a.resolve(null); + // Then + assertFalse(underTest.isResolved()); + assertFalse(underTest.isSuccessfullyResolved()); + assertFalse(underTest.isUnsuccessfullyResolved()); + } - assertTrue(a.isResolved()); - assertFalse(b.isResolved()); + public void testMeaningOfSuccessfullyResolved() { + // Given + JSType underTest = + new UnitTestingJSType(registry) { + @Override + public boolean isNoResolvedType() { + return false; + } + }; - assertTypeEquals(a, b); + // When + underTest.resolve(ErrorReporter.NULL_INSTANCE); - assertFalse(a.isEquivalentTo(UNKNOWN_TYPE)); - assertFalse(b.isEquivalentTo(UNKNOWN_TYPE)); - assertTrue(a.isEmptyType()); - assertFalse(a.isNoType()); - assertTrue(a.isNoResolvedType()); + // Then + assertTrue(underTest.isResolved()); + assertTrue(underTest.isSuccessfullyResolved()); + assertFalse(underTest.isUnsuccessfullyResolved()); } - public void testForwardDeclaredNamedType() { - NamedType a = new NamedType(EMPTY_SCOPE, registry, "forwardDeclared", "source", 1, 0); + public void testMeaningOfUnsuccessfullyResolved() { + // Given + JSType underTest = + new UnitTestingJSType(registry) { + @Override + public boolean isNoResolvedType() { + return true; + } + }; - assertTypeEquals(UNKNOWN_TYPE, a.getLeastSupertype(UNKNOWN_TYPE)); - assertTypeEquals(CHECKED_UNKNOWN_TYPE, - a.getLeastSupertype(CHECKED_UNKNOWN_TYPE)); - assertTypeEquals(UNKNOWN_TYPE, UNKNOWN_TYPE.getLeastSupertype(a)); - assertTypeEquals(CHECKED_UNKNOWN_TYPE, - CHECKED_UNKNOWN_TYPE.getLeastSupertype(a)); + // When + underTest.resolve(ErrorReporter.NULL_INSTANCE); + + // Then + assertTrue(underTest.isResolved()); + assertFalse(underTest.isSuccessfullyResolved()); + assertTrue(underTest.isUnsuccessfullyResolved()); } /** @@ -6181,4 +6209,48 @@ public void testCanCastTo() { // We currently allow any function to be cast to any other function type assertTrue(ARRAY_FUNCTION_TYPE.canCastTo(BOOLEAN_OBJECT_FUNCTION_TYPE)); } + + /** + * A minimal implementation of {@link JSType} for unit tests and nothing else. + * + *

This class has no innate behaviour. It is intended as a stand-in for testing behaviours on + * {@link JSType} that require a concrete instance. Test cases are responsible for any + * configuration. + */ + private static class UnitTestingJSType extends JSType { + + UnitTestingJSType(JSTypeRegistry registry) { + super(registry); + } + + @Override + int recursionUnsafeHashCode() { + throw new UnsupportedOperationException(); + } + + @Override + public BooleanLiteralSet getPossibleToBooleanOutcomes() { + throw new UnsupportedOperationException(); + } + + @Override + public T visit(Visitor visitor) { + throw new UnsupportedOperationException(); + } + + @Override + T visit(RelationshipVisitor visitor, JSType that) { + throw new UnsupportedOperationException(); + } + + @Override + JSType resolveInternal(ErrorReporter reporter) { + return this; + } + + @Override + StringBuilder appendTo(StringBuilder builder, boolean forAnnotation) { + throw new UnsupportedOperationException(); + } + } } diff --git a/test/com/google/javascript/rhino/jstype/NamedTypeTest.java b/test/com/google/javascript/rhino/jstype/NamedTypeTest.java index bba07659e19..6fcf07ddc1c 100644 --- a/test/com/google/javascript/rhino/jstype/NamedTypeTest.java +++ b/test/com/google/javascript/rhino/jstype/NamedTypeTest.java @@ -38,32 +38,174 @@ package com.google.javascript.rhino.jstype; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.javascript.rhino.testing.MapBasedScope.emptyScope; + import com.google.common.collect.ImmutableMap; +import com.google.common.testing.EqualsTester; +import com.google.javascript.rhino.ErrorReporter; import com.google.javascript.rhino.testing.BaseJSTypeTestCase; import com.google.javascript.rhino.testing.MapBasedScope; +import javax.annotation.Nullable; /** * @author nicksantos@google.com (Nick Santos) */ public class NamedTypeTest extends BaseJSTypeTestCase { - public void testNamedTypeProperties() { - FunctionType ctorA = registry.createConstructorType("TypeA", null, null, null, null, false); - MapBasedScope scope = new MapBasedScope(ImmutableMap.of("TypeA", ctorA)); - ObjectType typeA = ctorA.getInstanceType(); - NamedType namedA = new NamedType(scope, registry, "TypeA", "source", 1, 0); - - namedA.defineDeclaredProperty("foo", NUMBER_TYPE, null); - namedA.resolve(null); - assertTypeEquals(NUMBER_TYPE, typeA.getPropertyType("foo")); + + private FunctionType fooCtorType; // The type of the constructor of "Foo". + private ObjectType fooType; // A realized type with the canonical name "Foo". + + @Override + public void setUp() throws Exception { + super.setUp(); + + fooCtorType = + forceResolutionOf(new FunctionBuilder(registry).forConstructor().withName("Foo").build()); + fooType = forceResolutionOf(fooCtorType.getInstanceType()); + } + + public void testResolutionPropagatesNamedTypePropertiesToResolvedType() { + // Given + StaticTypedScope fooToFooScope = new MapBasedScope(ImmutableMap.of("Foo", fooCtorType)); + NamedType namedFooType = new NamedTypeBuilder().setScope(fooToFooScope).setName("Foo").build(); + + namedFooType.defineDeclaredProperty("myProperty", NUMBER_TYPE, null); + + // The property should not be typed yet. + assertTypeNotEquals(NUMBER_TYPE, fooType.getPropertyType("myProperty")); + + // When + namedFooType.resolve(ErrorReporter.NULL_INSTANCE); + + // Then + assertTypeEquals(NUMBER_TYPE, fooType.getPropertyType("myProperty")); + } + + public void testStateOfForwardDeclaredType_Unresolved() { + // Given + NamedType type = new NamedTypeBuilder().setName(FORWARD_DECLARED_TYPE_NAME).build(); + + // Then + assertFalse(type.isResolved()); + assertFalse(type.isEmptyType()); + assertTypeNotEquals(UNKNOWN_TYPE, type); + assertTypeEquals(UNKNOWN_TYPE, type.getReferencedType()); + } + + public void testStateOfForwardDeclaredType_UnsuccesfullyResolved() { + // Given + NamedType type = new NamedTypeBuilder().setName(FORWARD_DECLARED_TYPE_NAME).build(); + + // When + type.resolve(ErrorReporter.NULL_INSTANCE); + + // Then + assertTrue(type.isUnsuccessfullyResolved()); + assertFalse(type.isUnknownType()); + } + + public void testStateOfForwardDeclaredType_SuccessfullyResolved() { + // Given + StaticTypedScope fooToFooScope = new MapBasedScope(ImmutableMap.of("Foo", fooCtorType)); + NamedType namedFooType = new NamedTypeBuilder().setScope(fooToFooScope).setName("Foo").build(); + + // When + namedFooType.resolve(ErrorReporter.NULL_INSTANCE); + + // Then + assertTrue(namedFooType.isSuccessfullyResolved()); + assertTypeEquals(namedFooType, fooType); + } + + public void testEquality() { + // Given + ObjectType barType = createNominalType("Bar"); + FunctionType anonType = forceResolutionOf(new FunctionBuilder(registry).build()); + + NamedTypeBuilder namedFooBuilder = new NamedTypeBuilder().setName("Foo"); + NamedType namedFooUnresolved = namedFooBuilder.build(); + NamedType namedFooUnsuccessfullyResolved = + forceResolutionWith(NO_RESOLVED_TYPE, namedFooBuilder.build()); + NamedType namedFooResolvedToFoo = forceResolutionWith(fooType, namedFooBuilder.build()); + NamedType namedFooResolvedToAnon = forceResolutionWith(anonType, namedFooBuilder.build()); + NamedType namedFooResolvedToBar = forceResolutionWith(barType, namedFooBuilder.build()); + + NamedTypeBuilder namedBarBuilder = new NamedTypeBuilder().setName("Bar"); + NamedType namedBarUnresolved = namedBarBuilder.build(); + NamedType namedBarUnsuccessfullyResolved = + forceResolutionWith(NO_RESOLVED_TYPE, namedBarBuilder.build()); + NamedType namedBarResolvedToFoo = forceResolutionWith(fooType, namedBarBuilder.build()); + NamedType namedBarResolvedToAnon = forceResolutionWith(anonType, namedBarBuilder.build()); + NamedType namedBarResolvedToBar = forceResolutionWith(barType, namedBarBuilder.build()); + + // Then + new EqualsTester() + .addEqualityGroup(fooType, namedFooUnresolved, namedFooResolvedToFoo, namedBarResolvedToFoo) + .addEqualityGroup(barType, namedFooResolvedToBar, namedBarUnresolved, namedBarResolvedToBar) + .addEqualityGroup(anonType, namedFooResolvedToAnon, namedBarResolvedToAnon) + .addEqualityGroup(namedFooUnsuccessfullyResolved) + .addEqualityGroup(namedBarUnsuccessfullyResolved) + // TODO(b/112425334): Either re-enable this equality group or remove the NO_RESOLVED_TYPE + // from the typesystem. + // .addEqualityGroup(NO_RESOLVED_TYPE) + .testEquals(); + } + + public void testForwardDeclaredNamedType() { + NamedType a = new NamedTypeBuilder().setName("Unresolvable").build(); + + assertTypeEquals(UNKNOWN_TYPE, a.getLeastSupertype(UNKNOWN_TYPE)); + assertTypeEquals(CHECKED_UNKNOWN_TYPE, a.getLeastSupertype(CHECKED_UNKNOWN_TYPE)); + assertTypeEquals(UNKNOWN_TYPE, UNKNOWN_TYPE.getLeastSupertype(a)); + assertTypeEquals(CHECKED_UNKNOWN_TYPE, CHECKED_UNKNOWN_TYPE.getLeastSupertype(a)); } public void testActiveXObjectResolve() { MapBasedScope scope = new MapBasedScope(ImmutableMap.of("ActiveXObject", NO_OBJECT_TYPE)); + NamedType activeXObject = + new NamedTypeBuilder().setScope(scope).setName("ActiveXObject").build(); - NamedType activeXObject = new NamedType(scope, registry, "ActiveXObject", "source", 1, 0); assertEquals("ActiveXObject", activeXObject.toString()); activeXObject.resolve(null); assertEquals("NoObject", activeXObject.toString()); assertTypeEquals(NO_OBJECT_TYPE, activeXObject.getReferencedType()); } + + private static NamedType forceResolutionWith(JSType type, NamedType proxy) { + proxy.setResolvedTypeInternal(type); + proxy.setReferencedType(type); + return proxy; + } + + private static T forceResolutionOf(T type) { + type.setResolvedTypeInternal(type); + return type; + } + + private ObjectType createNominalType(String name) { + FunctionType ctorType = + forceResolutionOf(new FunctionBuilder(registry).forConstructor().withName(name).build()); + return forceResolutionOf(ctorType.getInstanceType()); + } + + private class NamedTypeBuilder { + private StaticTypedScope scope = emptyScope(); + @Nullable private String name; + + public NamedTypeBuilder setScope(StaticTypedScope scope) { + this.scope = scope; + return this; + } + + public NamedTypeBuilder setName(String name) { + this.name = name; + return this; + } + + public NamedType build() { + checkNotNull(name, "NamedType requires a name"); + return new NamedType(scope, registry, name, "source", 1, 0); + } + } }