diff --git a/src/com/google/javascript/jscomp/TypeCheck.java b/src/com/google/javascript/jscomp/TypeCheck.java index 0aa16c28e7f..b170d6577c0 100644 --- a/src/com/google/javascript/jscomp/TypeCheck.java +++ b/src/com/google/javascript/jscomp/TypeCheck.java @@ -1006,6 +1006,11 @@ public void visit(NodeTraversal t, Node n, Node parent) { } private void checkSpread(NodeTraversal t, Node spreadNode) { + if (spreadNode.getParent().isObjectLit()) { + // Nothing to check for object spread, anything can be spread. + return; + } + Node iterableNode = spreadNode.getOnlyChild(); ensureTyped(iterableNode); JSType iterableType = getJSType(iterableNode); @@ -1328,7 +1333,7 @@ private void visitObjectPattern(NodeTraversal t, Node pattern) { * we change the schema of the object type it is referring to. * * @param t the traversal - * @param key the ASSIGN, STRING_KEY, MEMBER_FUNCTION_DEF, or COMPUTED_PROPERTY node + * @param key the ASSIGN, STRING_KEY, MEMBER_FUNCTION_DEF, SPREAD, or COMPUTED_PROPERTY node * @param owner the parent node, either OBJECTLIT or CLASS_MEMBERS * @param ownerType the instance type of the enclosing object/class */ @@ -1371,6 +1376,11 @@ private void visitObjectOrClassLiteralKey( } } + if (key.isSpread()) { + // Rely on type inference to figure out what the key/types this adds to this object. + return; + } + // TODO(johnlenz): Validate get and set function declarations are valid // as is the functions can have "extraneous" bits. diff --git a/src/com/google/javascript/jscomp/TypeInference.java b/src/com/google/javascript/jscomp/TypeInference.java index 841ceec1a8a..496cc8fc88a 100644 --- a/src/com/google/javascript/jscomp/TypeInference.java +++ b/src/com/google/javascript/jscomp/TypeInference.java @@ -1333,26 +1333,52 @@ private FlowScope traverseObjectLiteral(Node n, FlowScope scope) { return scope; } - String qObjName = NodeUtil.getBestLValueName( - NodeUtil.getBestLValue(n)); - for (Node name = n.getFirstChild(); name != null; - name = name.getNext()) { - if (name.isComputedProp()) { + String qObjName = NodeUtil.getBestLValueName(NodeUtil.getBestLValue(n)); + for (Node key = n.getFirstChild(); key != null; key = key.getNext()) { + if (key.isComputedProp()) { // Don't define computed properties as inferred properties on the object continue; } - String memberName = NodeUtil.getObjectLitKeyName(name); + + if (key.isSpread()) { + Node name = key.getFirstChild(); + JSType nameType = name.getJSType(); + + if (nameType == null) { + continue; + } + + ObjectType spreadType = nameType.toMaybeObjectType(); + + while (spreadType != null) { + Set spreadPropertyNames = spreadType.getOwnPropertyNames(); + for (String propertyName : spreadPropertyNames) { + objectType.defineInferredProperty( + propertyName, spreadType.getPropertyType(propertyName), key); + } + if ((!spreadType.isConstructor() + && !spreadType.isInterface() + && !spreadType.isInstanceType()) + || spreadType.getSuperClassConstructor() == null) { + break; + } + spreadType = spreadType.getSuperClassConstructor().getInstanceType(); + } + + continue; + } + + String memberName = NodeUtil.getObjectLitKeyName(key); if (memberName != null) { - JSType rawValueType = name.getFirstChild().getJSType(); - JSType valueType = - TypeCheck.getObjectLitKeyTypeFromValueType(name, rawValueType); + JSType rawValueType = key.getFirstChild().getJSType(); + JSType valueType = TypeCheck.getObjectLitKeyTypeFromValueType(key, rawValueType); if (valueType == null) { valueType = unknownType; } - objectType.defineInferredProperty(memberName, valueType, name); + objectType.defineInferredProperty(memberName, valueType, key); // Do normal flow inference if this is a direct property assignment. - if (qObjName != null && name.isStringKey()) { + if (qObjName != null && key.isStringKey()) { String qKeyName = qObjName + "." + memberName; TypedVar var = getDeclaredVar(scope, qKeyName); JSType oldType = var == null ? null : var.getType(); @@ -1362,7 +1388,7 @@ private FlowScope traverseObjectLiteral(Node n, FlowScope scope) { scope = scope.inferQualifiedSlot( - name, qKeyName, oldType == null ? unknownType : oldType, valueType, false); + key, qKeyName, oldType == null ? unknownType : oldType, valueType, false); } } else { n.setJSType(unknownType); diff --git a/src/com/google/javascript/jscomp/TypedScopeCreator.java b/src/com/google/javascript/jscomp/TypedScopeCreator.java index 8272768d7d6..16bc84d8cea 100644 --- a/src/com/google/javascript/jscomp/TypedScopeCreator.java +++ b/src/com/google/javascript/jscomp/TypedScopeCreator.java @@ -750,8 +750,10 @@ void processObjectLitProperties( Node objLit, ObjectType objLitType, boolean declareOnOwner) { for (Node keyNode = objLit.getFirstChild(); keyNode != null; keyNode = keyNode.getNext()) { - if (keyNode.isComputedProp()) { - // Don't try defining computed properties on an object. + if (keyNode.isComputedProp() || keyNode.isSpread()) { + // Don't try defining computed or spread properties on an object. Note that for spread + // type inference will try to determine the properties and types. We cannot do it here as + // we don't have all the type information of the spread object. continue; } Node value = keyNode.getFirstChild(); diff --git a/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java b/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java index 326d6595a4d..4d03b444546 100644 --- a/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java @@ -6109,4 +6109,373 @@ public void testAsyncGeneratorReturnPromiseMismatch() { "found : Promise", "required: (IThenable|number)")); } + + @Test + public void testObjectSpread() { + testTypes( + lines( + "let /** number */ qux = 0;", + "let obj = {a: qux, b: 'str'};", + "let /** !{a: number, b: string} */ copy = {...obj};")); + } + + @Test + public void testObjectSpreadBadExplicitType() { + testTypes( + lines( + "let /** number */ qux = 0;", + "let obj = {a: qux, b: 'str'};", + "let /** !{a: string, b: string, c: boolean} */ copy = {...obj};"), + lines( + "initializing variable", + "found : {a: number, b: string}", + "required: {", + " a: string,", + " b: string,", + " c: boolean", + "}", + "missing : [c]", + "mismatch: [a]")); + } + + @Test + public void testMultipleObjectSpread() { + testTypes( + lines( + "let objAB = {a: 0, b: 'b'};", + "let objCD = {c: 'c', d: false};", + "let /** !{a: number, b: string, c: string, d: boolean} */ copy = ", + " {...objAB, ...objCD};")); + } + + @Test + public void testMultipleObjectSpreadBadExplicitType() { + testTypes( + lines( + "let objAB = {a: 0, b: 'b'};", + "let objCD = {c: 'c', d: false};", + "let /** !{", + " a: string,", + " b: string,", + " c: number,", + " d: boolean,", + " e: boolean,", + "}*/", + "copy = {...objAB, ...objCD};"), + lines( + "initializing variable", + "found : {", + " a: number,", + " b: string,", + " c: string,", + " d: boolean", + "}", + "required: {", + " a: string,", + " b: string,", + " c: number,", + " d: boolean,", + " e: boolean", + "}", + "missing : [e]", + "mismatch: [a,c]")); + } + + @Test + public void testObjectSpreadSameKeysUnions() { + testTypes( + lines( + "let objAB = {a: 0, b: 'b'};", + "let objCD = {c: 'c', d: false, a: 'str'};", + "let /** !{", + " a: (string|number),", + " b: string,", + " c: string,", + " d: boolean,", + "}*/", + "copy = {...objAB, ...objCD};")); + + testTypes( + lines( + "let objAB = {a: 0, b: 'b'};", + "let objCD = {c: 'c', d: false, a: 'str'};", + "let /** !{", + " a: (string|number),", + " b: string,", + " c: string,", + " d: boolean,", + "}*/", + "copy = {...objCD, ...objAB};")); + + testTypes( + lines( + "let objAB = {a: 0, b: 'b'};", + "let objCD = {c: 'c', d: false, a: 'str'};", + "let /** !{", + " a: (string|number|boolean),", + " b: string,", + " c: string,", + " d: boolean,", + "}*/", + "copy = {...objAB, ...objCD, a: false};")); + + testTypes( + lines( + "let objAB = {a: 0, b: 'b'};", + "let objCD = {c: 'c', d: false, a: 'str'};", + "let /** !{", + " a: (string|number|boolean),", + " b: string,", + " c: string,", + " d: boolean,", + "}*/", + "copy = {a: false, ...objAB, ...objCD};")); + } + + @Test + public void testObjectSpreadWithExplicitProperties() { + testTypes( + lines( + "let /** number */ qux = 0;", + "let obj = {a: qux, b: 'str'};", + "let /** !{a: number, b: string, c: boolean} */ copy = {...obj, c: false};")); + } + + @Test + public void testSpreadClassInstance() { + testTypes( + lines( + "class Qux {", + " constructor() {", + " /** @const {number} */", + " this.a = 0;", + " /** @const {string} */", + " this.b = 'y';", + " }", + "}", + "let q = new Qux();", + "let /** !{a: number, b: string} */ copy = {...q};")); + } + + @Test + public void testSpreadClassInstanceBadExplicitType() { + testTypes( + lines( + "class Qux {", + " constructor() {", + " /** @const {number} */", + " this.a = 0;", + " /** @const {string} */", + " this.b = 'y';", + " }", + "}", + "let /** !{a: string, b: string, c: boolean} */ copy = {...new Qux()};"), + lines( + "initializing variable", + "found : {a: number, b: string}", + "required: {", + " a: string,", + " b: string,", + " c: boolean", + "}", + "missing : [c]", + "mismatch: [a]")); + } + + @Test + public void testSpreadClassInstanceIsAnonymousType() { + testTypes( + lines( + "class Qux {", + " constructor() {", + " /** @const {number} */", + " this.a = 0;", + " /** @const {string} */", + " this.b = 'y';", + " }", + "}", + "let /** !Qux */ copy = {...new Qux()};"), + lines( + "initializing variable", // + "found : {a: number, b: string}", + "required: Qux")); + } + + @Test + public void testSpreadClassInstanceDoesNotSpreadMethods() { + testTypes( + lines( + "class Qux {", + " constructor() {", + " /** @const {number} */", + " this.a = 0;", + " /** @const {string} */", + " this.b = 'y';", + " }", + "", + " /** @return {number} */", + " baz() { return 0; }", + "}", + "let q = new Qux();", + "let /** !{a: number, b: string} */ copy = {...q};")); + } + + @Test + public void testSpreadSubclassInstanceIncludesSupertypeProperties() { + testTypes( + lines( + "class Super {", + " constructor() {", + " /** @const {boolean} */", + " this.duper = true;", + " }", + "}", + "", + "class Qux extends Super {", + " constructor() {", + " super();", + " /** @const {number} */", + " this.a = 0;", + " /** @const {string} */", + " this.b = 'y';", + " }", + "}", + "let q = new Qux();", + "let /** !{duper: boolean, a: number, b: string} */ copy = {...q};")); + } + + @Test + public void testSpreadSubclassInstanceHasUnionedProperties() { + testTypes( + lines( + "class Super {", + " constructor() {", + " /** @type {number} */", + " this.duper;", + " }", + "}", + "", + "class Qux extends Super {", + " constructor() {", + " super();", + " /** @const {string} */", + " this.duper;", + " /** @const {number} */", + " this.a = 0;", + " /** @const {string} */", + " this.b = 'y';", + " }", + "}", + "let q = new Qux();", + "let /** !{duper: (number|string), a: number, b: string} */ copy = {...q};")); + } + + @Test + public void testSpreadInterface() { + testTypes( + lines( + "/** @interface */", + "class Interface {", + " constructor() {", + " /** @const {boolean} */", + " this.i = true;", + " }", + "}", + "let /** !Interface */ i;", + "let /** !{i: boolean} */ copy = {...i};")); + } + + @Test + public void testSpreadInterfaceMismatch() { + testTypes( + lines( + "/** @interface */", + "class Interface {", + " constructor() {", + " /** @const {number} */", + " this.i = 0;", + " }", + "}", + "let /** !Interface */ i;", + "let /** !{i: boolean} */ copy = {...i};"), + lines( + "initializing variable", + "found : {i: number}", + "required: {i: boolean}", + "missing : []", + "mismatch: [i]")); + } + + @Test + public void testSpreadTypeDef() { + testTypes( + lines( + "/** @typedef {{a: number, b: string}} */", + "let TypeDef;", + "let /** !TypeDef */ t;", + "let /** !{a: number, b: string} */ copy = {...t};")); + } + + @Test + public void testSpreadTypeDefMismatch() { + testTypes( + lines( + "/** @typedef {{a: string, b: number}} */", + "let TypeDef;", + "let /** !TypeDef */ t;", + "let /** !{a: number, b: string} */ copy = {...t};"), + lines( + "initializing variable", + "found : {a: string, b: number}", + "required: {a: number, b: string}", + "missing : []", + "mismatch: [a,b]")); + } + + @Test + public void testSpreadUnknown() { + testTypes( + lines( + "/** @type {?} */", // + "let value;", + "let /** !Object */ copy = {...value};")); + + testTypes( + lines( + "/** @type {?} */", + "let value0;", + "let /** !{a: number, b: string} */ copy0 = {a: 0, b: '', ...value0};")); + } + + @Test + public void testSpreadUnknownTypeMismatch() { + // Spreading unknown should have no effect on the type, rather than making the new type unknown. + testTypes( + lines( + "/** @type {?} */", + "let value;", + "let /** !{a: number, b: string} */ copy = {...value};"), + lines( + "initializing variable", + "found : {}", + "required: {a: number, b: string}", + "missing : [a,b]", + "mismatch: []")); + } + + @Test + public void testSpreadUnknownTypeMismatchExtraProperties() { + // Spreading unknown should have no effect on the type, rather than making the new type unknown. + testTypes( + lines( + "/** @type {?} */", + "let value;", + "let /** !{a: number, b: string} */ copy = {a: '', b: 0, ...value0};"), + lines( + "initializing variable", + "found : {a: string, b: number}", + "required: {a: number, b: string}", + "missing : []", + "mismatch: [a,b]")); + } } diff --git a/test/com/google/javascript/jscomp/TypeInferenceTest.java b/test/com/google/javascript/jscomp/TypeInferenceTest.java index 6c920e0847b..e863bfc520c 100644 --- a/test/com/google/javascript/jscomp/TypeInferenceTest.java +++ b/test/com/google/javascript/jscomp/TypeInferenceTest.java @@ -1367,6 +1367,147 @@ public void testObjectLit() { verify("out", NUMBER_TYPE); } + @Test + public void testObjectSpread() { + JSType recordType = + registry.createRecordType( + ImmutableMap.of( + "x", getNativeType(STRING_TYPE), + "y", getNativeType(NUMBER_TYPE))); + assuming("obj", recordType); + + inFunction( + lines( + "let copy = {...obj}; ", // preserve newline + "X: copy.x;", + "Y: copy.y;")); + assertTypeOfExpression("X").toStringIsEqualTo("string"); + assertTypeOfExpression("Y").toStringIsEqualTo("number"); + + assertScopeEnclosing("X") + .declares("copy") + .withTypeThat() + .toStringIsEqualTo("{x: string, y: number}"); + } + + @Test + public void testObjectSpreadWithAdditionalProperties() { + JSType recordType = + registry.createRecordType( + ImmutableMap.of( + "x", getNativeType(STRING_TYPE), + "y", getNativeType(NUMBER_TYPE))); + assuming("obj", recordType); + assuming("a", NUMBER_TYPE); + assuming("b", STRING_TYPE); + + inFunction( + lines( + "let copy = {a, c: 0, ...obj, b}; ", // preserve newline + "A: copy.a", + "B: copy.b", + "C: copy.c", + "X: copy.x;", + "Y: copy.y;")); + assertTypeOfExpression("A").toStringIsEqualTo("number"); + assertTypeOfExpression("B").toStringIsEqualTo("string"); + assertTypeOfExpression("C").toStringIsEqualTo("number"); + assertTypeOfExpression("X").toStringIsEqualTo("string"); + assertTypeOfExpression("Y").toStringIsEqualTo("number"); + + assertScopeEnclosing("X") + .declares("copy") + .withTypeThat() + .toStringIsEqualTo( + lines( + "{", + " a: number,", + " b: string,", + " c: number,", + " x: string,", + " y: number", + "}")); + } + + @Test + public void testObjectSpreadMergeObjects() { + JSType recordType = + registry.createRecordType( + ImmutableMap.of( + "x", getNativeType(STRING_TYPE), + "y", getNativeType(NUMBER_TYPE))); + assuming("xy", recordType); + recordType = + registry.createRecordType( + ImmutableMap.of( + "a", getNativeType(NUMBER_TYPE), + "b", getNativeType(STRING_TYPE))); + assuming("ab", recordType); + + inFunction( + lines( + "let copy = {...ab, c: 0, ...xy}; ", // preserve newline + "A: copy.a", + "B: copy.b", + "C: copy.c", + "X: copy.x;", + "Y: copy.y;")); + assertTypeOfExpression("A").toStringIsEqualTo("number"); + assertTypeOfExpression("B").toStringIsEqualTo("string"); + assertTypeOfExpression("C").toStringIsEqualTo("number"); + assertTypeOfExpression("X").toStringIsEqualTo("string"); + assertTypeOfExpression("Y").toStringIsEqualTo("number"); + + assertScopeEnclosing("X") + .declares("copy") + .withTypeThat() + .toStringIsEqualTo( + lines( + "{", + " a: number,", + " b: string,", + " c: number,", + " x: string,", + " y: number", + "}")); + } + + @Test + public void testObjectSpreadPrimitivesAddsNoProperties() { + assuming("num", NUMBER_TYPE); + + inFunction( + lines( + "let obj = {...0, ...'str', ...[0], ...num, ...function() {}}; ", // preserve newline + "OBJ: obj")); + + assertTypeOfExpression("OBJ").toStringIsEqualTo("{}"); + } + + @Test + public void testObjectSpreadUnknown() { + assuming("what", UNKNOWN_TYPE); + + inFunction( + lines( + "let obj = {...what}; ", // preserve newline + "OBJ: obj")); + + assertTypeOfExpression("OBJ").toStringIsEqualTo("{}"); + } + + @Test + public void testObjectSpreadUnknownExtraProperties() { + assuming("what", UNKNOWN_TYPE); + + inFunction( + lines( + "let obj = {a: 0, ...{b: ''}, ...what}; ", // preserve newline + "OBJ: obj")); + + assertTypeOfExpression("OBJ").toStringIsEqualTo("{a: number, b: string}"); + } + @Test public void testCast1() { inFunction("var x = /** @type {Object} */ (this);");