diff --git a/src/com/google/javascript/jscomp/AstValidator.java b/src/com/google/javascript/jscomp/AstValidator.java index 4392dca8388..988941c8235 100644 --- a/src/com/google/javascript/jscomp/AstValidator.java +++ b/src/com/google/javascript/jscomp/AstValidator.java @@ -764,9 +764,6 @@ private void validateParameters(Node n) { validateNodeType(Token.PARAM_LIST, n); for (Node c = n.getFirstChild(); c != null; c = c.getNext()) { if (c.isRest()) { - if (c.getNext() != null) { - violation("Rest parameters must come after all other parameters.", c); - } validateRest(Token.PARAM_LIST, c); } else if (c.isDefaultValue()) { validateDefaultValue(Token.PARAM_LIST, c); @@ -807,6 +804,9 @@ private void validateRest(Token contextType, Node n) { validateNodeType(Token.REST, n); validateChildCount(n); validateLHS(contextType, n.getFirstChild()); + if (n.getNext() != null) { + violation("Rest parameters must come after all other parameters.", n); + } } private void validateSpread(Node n) { @@ -821,6 +821,7 @@ private void validateSpread(Node n) { } break; case ARRAYLIT: + case OBJECTLIT: break; default: violation("SPREAD node should not be the child of a " + parent.getToken() + " node.", n); @@ -957,6 +958,9 @@ private void validateObjectPattern(Token type, Node n) { case DEFAULT_VALUE: validateDefaultValue(type, c); break; + case REST: + validateRest(type, c); + break; case COMPUTED_PROP: validateObjectPatternComputedPropKey(type, c); break; @@ -1270,6 +1274,9 @@ private void validateObjectLitKey(Node n) { case COMPUTED_PROP: validateObjectLitComputedPropKey(n); return; + case SPREAD: + validateSpread(n); + return; default: violation("Expected object literal key expression but was " + n.getToken(), n); } diff --git a/src/com/google/javascript/jscomp/CodeGenerator.java b/src/com/google/javascript/jscomp/CodeGenerator.java index 1c4fd88e98c..8bf383483bc 100644 --- a/src/com/google/javascript/jscomp/CodeGenerator.java +++ b/src/com/google/javascript/jscomp/CodeGenerator.java @@ -1001,7 +1001,7 @@ protected void add(Node n, Context context) { cc.listSeparator(); } - checkState(NodeUtil.isObjLitProperty(c)); + checkState(NodeUtil.isObjLitProperty(c) || c.isSpread(), c); add(c); } add("}"); diff --git a/src/com/google/javascript/jscomp/NodeUtil.java b/src/com/google/javascript/jscomp/NodeUtil.java index 17e9634fd72..193b61ce2bf 100644 --- a/src/com/google/javascript/jscomp/NodeUtil.java +++ b/src/com/google/javascript/jscomp/NodeUtil.java @@ -721,6 +721,10 @@ public static boolean isLiteralValue(Node n, boolean includeFunctions) { || !isLiteralValue(child.getLastChild(), includeFunctions)) { return false; } + } else if (child.isSpread()) { + if (!isLiteralValue(child.getOnlyChild(), includeFunctions)) { + return false; + } } else { // { key: value, ... } // { "quoted_key": value, ... } diff --git a/src/com/google/javascript/jscomp/parsing/IRFactory.java b/src/com/google/javascript/jscomp/parsing/IRFactory.java index 876fc9e269c..d00b92094cb 100644 --- a/src/com/google/javascript/jscomp/parsing/IRFactory.java +++ b/src/com/google/javascript/jscomp/parsing/IRFactory.java @@ -1047,8 +1047,12 @@ Node processObjectPattern(ObjectPatternTree tree) { maybeWarnForFeature(tree, Feature.DESTRUCTURING); Node node = newNode(Token.OBJECT_PATTERN); - for (ParseTree child : tree.fields) { - node.addChildToBack(transformNodeWithInlineJsDoc(child)); + for (ParseTree c : tree.fields) { + Node child = transformNodeWithInlineJsDoc(c); + if (child.isRest()) { + maybeWarnForFeature(c, Feature.OBJECT_PATTERN_REST); + } + node.addChildToBack(child); } return node; } @@ -1613,9 +1617,15 @@ Node processObjectLiteral(ObjectLiteralExpressionTree objTree) { } Node key = transform(el); - if (!key.isComputedProp() && !key.isQuotedString() && !currentFileIsExterns) { + if (!key.isComputedProp() + && !key.isQuotedString() + && !key.isSpread() + && !currentFileIsExterns) { maybeWarnKeywordProperty(key); } + if (key.isSpread()) { + maybeWarnForFeature(el, Feature.OBJECT_LITERALS_WITH_SPREAD); + } if (!key.hasChildren()) { maybeWarn = true; } diff --git a/src/com/google/javascript/jscomp/parsing/parser/Parser.java b/src/com/google/javascript/jscomp/parsing/parser/Parser.java index 3875634d829..cf04e32dd78 100644 --- a/src/com/google/javascript/jscomp/parsing/parser/Parser.java +++ b/src/com/google/javascript/jscomp/parsing/parser/Parser.java @@ -2395,6 +2395,7 @@ private ParseTree parseArrayLiteral() { elements.add(new NullTree(getTreeLocation(getTreeStartLocation()))); } else { if (peek(TokenType.SPREAD)) { + features = features.with(Feature.SPREAD_EXPRESSIONS); elements.add(parseSpreadExpression()); } else { elements.add(parseAssignmentExpression()); @@ -2419,7 +2420,9 @@ private ParseTree parseObjectLiteral() { eat(TokenType.OPEN_CURLY); Token commaToken = null; - while (peekPropertyNameOrComputedProp(0) || peek(TokenType.STAR) + while (peek(TokenType.SPREAD) + || peekPropertyNameOrComputedProp(0) + || peek(TokenType.STAR) || peekAccessibilityModifier()) { commaToken = null; result.add(parsePropertyAssignment()); @@ -2468,6 +2471,9 @@ private ParseTree parsePropertyAssignment() { TokenType type = peekType(); if (type == TokenType.STAR) { return parsePropertyAssignmentGenerator(); + } else if (peek(TokenType.SPREAD)) { + features = features.with(Feature.OBJECT_LITERALS_WITH_SPREAD); + return parseSpreadExpression(); } else if (type == TokenType.STRING || type == TokenType.NUMBER || type == TokenType.IDENTIFIER @@ -3570,21 +3576,10 @@ private ParseTree parsePattern(PatternKind kind) { } private boolean peekArrayPatternElement() { - return peekExpression() || peek(TokenType.SPREAD); - } - - private ParseTree parseArrayPatternElement(PatternKind patternKind) { - ParseTree patternElement; - - if (peek(TokenType.SPREAD)) { - patternElement = parseArrayPatternRest(patternKind); - } else { - patternElement = parsePatternAssignmentTarget(patternKind); - } - return patternElement; + return peekExpression(); } - private ParseTree parseArrayPatternRest(PatternKind patternKind) { + private ParseTree parsePatternRest(PatternKind patternKind) { SourcePosition start = getTreeStartLocation(); eat(TokenType.SPREAD); ParseTree patternAssignmentTarget = parseRestAssignmentTarget(patternKind); @@ -3609,13 +3604,9 @@ private ParseTree parseArrayPattern(PatternKind kind) { eat(TokenType.COMMA); elements.add(new NullTree(getTreeLocation(getTreeStartLocation()))); } else { - ParseTree element = parseArrayPatternElement(kind); - elements.add(element); + elements.add(parsePatternAssignmentTarget(kind)); - if (element.isAssignmentRestElement()) { - // Rest can only appear in the posterior, so we must be done - break; - } else if (peek(TokenType.COMMA)) { + if (peek(TokenType.COMMA)) { // Consume the comma separator eat(TokenType.COMMA); } else { @@ -3624,6 +3615,10 @@ private ParseTree parseArrayPattern(PatternKind kind) { } } } + if (peek(TokenType.SPREAD)) { + features = features.with(Feature.ARRAY_PATTERN_REST); + elements.add(parsePatternRest(kind)); + } eat(TokenType.CLOSE_SQUARE); return new ArrayPatternTree(getTreeLocation(start), elements.build()); } @@ -3635,6 +3630,7 @@ private ParseTree parseObjectPattern(PatternKind kind) { eat(TokenType.OPEN_CURLY); while (peekObjectPatternField()) { fields.add(parseObjectPatternField(kind)); + if (peek(TokenType.COMMA)) { // Consume the comma separator eat(TokenType.COMMA); @@ -3643,6 +3639,10 @@ private ParseTree parseObjectPattern(PatternKind kind) { break; } } + if (peek(TokenType.SPREAD)) { + features = features.with(Feature.OBJECT_PATTERN_REST); + fields.add(parsePatternRest(kind)); + } eat(TokenType.CLOSE_CURLY); return new ObjectPatternTree(getTreeLocation(start), fields.build()); } diff --git a/test/com/google/javascript/jscomp/AstValidatorTest.java b/test/com/google/javascript/jscomp/AstValidatorTest.java index c221cd2aa14..d0a3a8eb38f 100644 --- a/test/com/google/javascript/jscomp/AstValidatorTest.java +++ b/test/com/google/javascript/jscomp/AstValidatorTest.java @@ -312,6 +312,20 @@ public void testValidDestructuringAssignment2() { valid("({['a']:b()['c'] = 1} = obj);"); } + public void testObjectRestAssignment() { + setAcceptedLanguage(LanguageMode.ECMASCRIPT_NEXT); + valid("var {a, ...rest} = obj;"); + valid("({a:b, ...rest} = obj);"); + valid("({a:b.c, ...rest} = obj);"); + valid("({a:b().c, ...rest} = obj);"); + valid("({a:b['c'], ...rest} = obj);"); + valid("({a:b()['c'], ...rest} = obj);"); + valid("({a:b.c = 1, ...rest} = obj);"); + valid("({a:b().c = 1, ...rest} = obj);"); + valid("({a:b['c'] = 1, ...rest} = obj);"); + valid("({a:b()['c'] = 1, ...rest} = obj);"); + } + public void testInvalidDestructuringAssignment() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); diff --git a/test/com/google/javascript/jscomp/CodePrinterTest.java b/test/com/google/javascript/jscomp/CodePrinterTest.java index 3e9c80d994a..f43c87685ea 100644 --- a/test/com/google/javascript/jscomp/CodePrinterTest.java +++ b/test/com/google/javascript/jscomp/CodePrinterTest.java @@ -53,6 +53,18 @@ public void testExponentiationAssignmentOperator() { assertPrintSame("x**=y"); } + public void testObjectLiteralWithSpread() { + languageMode = LanguageMode.ECMASCRIPT_NEXT; + assertPrintSame("({...{}})"); + assertPrintSame("({...x})"); + assertPrintSame("({...x,a:1})"); + assertPrintSame("({a:1,...x})"); + assertPrintSame("({a:1,...x,b:1})"); + assertPrintSame("({...x,...y})"); + assertPrintSame("({...x,...f()})"); + assertPrintSame("({...{...{}}})"); + } + public void testPrint() { assertPrint("10 + a + b", "10+a+b"); assertPrint("10 + (30*50)", "10+30*50"); @@ -448,6 +460,16 @@ public void testPrintObjectPatternInitializer() { assertPrintSame("({a:{b=2},c}=foo())"); } + public void testPrintObjectPatternWithRest() { + languageMode = LanguageMode.ECMASCRIPT_NEXT; + assertPrintSame("const {a,...rest}=foo()"); + assertPrintSame("var {a,...rest}=foo()"); + assertPrintSame("let {a,...rest}=foo()"); + assertPrintSame("({a,...rest}=foo())"); + assertPrintSame("({a=2,...rest}=foo())"); + assertPrintSame("({a:b=2,...rest}=foo())"); + } + public void testPrettyPrintObjectPattern() { languageMode = LanguageMode.ECMASCRIPT_2015; assertPrettyPrint("const {a,b,c}=foo();", "const {a, b, c} = foo();\n"); diff --git a/test/com/google/javascript/jscomp/NodeUtilTest.java b/test/com/google/javascript/jscomp/NodeUtilTest.java index f930ebd593b..191b8fc0e0c 100644 --- a/test/com/google/javascript/jscomp/NodeUtilTest.java +++ b/test/com/google/javascript/jscomp/NodeUtilTest.java @@ -529,6 +529,12 @@ public void testMayHaveSideEffects() { assertSideEffect(false, "[...[1]]"); assertSideEffect(true, "[...[i++]]"); assertSideEffect(true, "[...f()]"); + assertSideEffect(false, "({...x})"); + assertSideEffect(false, "({...{}})"); + assertSideEffect(false, "({...{a:1}})"); + assertSideEffect(true, "({...{a:i++}})"); + assertSideEffect(true, "({...{a:f()}})"); + assertSideEffect(true, "({...f()})"); assertSideEffect(true, "i++"); assertSideEffect(true, "[b, [a, i++]]"); assertSideEffect(true, "i=3"); @@ -1699,6 +1705,10 @@ public void testLocalValueSpread() { assertTrue(NodeUtil.evaluatesToLocalValue(getNode("[...x]"))); assertFalse(NodeUtil.isLiteralValue(getNode("[...x]"), false)); assertFalse(NodeUtil.isImmutableValue(getNode("[...x]"))); + + assertTrue(NodeUtil.evaluatesToLocalValue(getNode("{...x}"))); + assertFalse(NodeUtil.isLiteralValue(getNode("{...x}"), false)); + assertFalse(NodeUtil.isImmutableValue(getNode("{...x}"))); } public void testLocalValueAwait() { diff --git a/test/com/google/javascript/jscomp/parsing/ParserTest.java b/test/com/google/javascript/jscomp/parsing/ParserTest.java index 657ce148d0c..77e75c99923 100644 --- a/test/com/google/javascript/jscomp/parsing/ParserTest.java +++ b/test/com/google/javascript/jscomp/parsing/ParserTest.java @@ -1466,15 +1466,10 @@ public void testArrayDestructuringDeclarationRest() { expectFeatures(Feature.DESTRUCTURING, Feature.ARRAY_PATTERN_REST); parse("var [first, ...rest] = foo();"); - - expectFeatures(Feature.DESTRUCTURING, Feature.ARRAY_PATTERN_REST, Feature.LET_DECLARATIONS); parse("let [first, ...rest] = foo();"); - - expectFeatures(Feature.DESTRUCTURING, Feature.ARRAY_PATTERN_REST, Feature.CONST_DECLARATIONS); parse("const [first, ...rest] = foo();"); // nested destructuring in regular parameters and rest parameters - expectFeatures(Feature.DESTRUCTURING, Feature.ARRAY_PATTERN_REST); parse("var [first, {a, b}, ...[re, st, ...{length}]] = foo();"); expectFeatures(); @@ -1491,6 +1486,59 @@ public void testArrayDestructuringDeclarationRest() { getRequiresEs6Message(Feature.ARRAY_PATTERN_REST)); } + public void testObjectDestructuringDeclarationRest() { + mode = LanguageMode.ES_NEXT; + strictMode = SLOPPY; + + expectFeatures(Feature.DESTRUCTURING, Feature.OBJECT_PATTERN_REST); + parse("var {first, ...rest} = foo();"); + parse("let {first, ...rest} = foo();"); + parse("const {first, ...rest} = foo();"); + + expectFeatures(); + parseError( + "var {first, ...more = 'default'} = foo();", + "A default value cannot be specified after '...'"); + parseError("var {first, ...more, last} = foo();", "'}' expected"); + + mode = LanguageMode.ECMASCRIPT6; + parseWarning( + "var {first, ...rest} = foo();", + getRequiresEsNextMessage(Feature.OBJECT_PATTERN_REST)); + } + + public void testArrayLiteralDeclarationSpread() { + mode = LanguageMode.ECMASCRIPT6; + strictMode = SLOPPY; + + expectFeatures(Feature.SPREAD_EXPRESSIONS); + parse("var o = [first, ...spread];"); + + mode = LanguageMode.ECMASCRIPT5; + parseWarning( + "var o = [first, ...spread];", + getRequiresEs6Message(Feature.SPREAD_EXPRESSIONS)); + } + + public void testObjectLiteralDeclarationSpread() { + mode = LanguageMode.ES_NEXT; + strictMode = SLOPPY; + + expectFeatures(Feature.OBJECT_LITERALS_WITH_SPREAD); + parse("var o = {first: 1, ...spread};"); + + mode = LanguageMode.ECMASCRIPT5; + parseWarning( + "var o = {first: 1, ...spread};", + getRequiresEs6Message(Feature.SPREAD_EXPRESSIONS), + getRequiresEsNextMessage(Feature.OBJECT_LITERALS_WITH_SPREAD)); + + mode = LanguageMode.ECMASCRIPT6; + parseWarning( + "var o = {first: 1, ...spread};", + getRequiresEsNextMessage(Feature.OBJECT_LITERALS_WITH_SPREAD)); + } + public void testArrayDestructuringAssignRest() { mode = LanguageMode.ECMASCRIPT6; strictMode = SLOPPY; @@ -1507,6 +1555,17 @@ public void testArrayDestructuringAssignRest() { getRequiresEs6Message(Feature.ARRAY_PATTERN_REST)); } + public void testObjectDestructuringAssignRest() { + mode = LanguageMode.ES_NEXT; + strictMode = SLOPPY; + expectFeatures(Feature.DESTRUCTURING, Feature.OBJECT_PATTERN_REST); + parse("const {first, ...rest} = foo();"); + + mode = LanguageMode.ECMASCRIPT6; + parseWarning("var {first, ...rest} = foo();", + getRequiresEsNextMessage(Feature.OBJECT_PATTERN_REST)); + } + public void testArrayDestructuringAssignRestInvalid() { // arbitrary LHS assignment target not allowed parseError( @@ -3740,6 +3799,10 @@ private static String getRequiresEs6Message(Feature feature) { return requiresLanguageModeMessage(LanguageMode.ECMASCRIPT6, feature); } + private static String getRequiresEsNextMessage(Feature feature) { + return requiresLanguageModeMessage(LanguageMode.ES_NEXT, feature); + } + private static String requiresLanguageModeMessage(LanguageMode languageMode, Feature feature) { return String.format( "this language feature is only supported for %s mode or better: %s",