diff --git a/src/com/google/javascript/jscomp/ReplaceMessages.java b/src/com/google/javascript/jscomp/ReplaceMessages.java index 38d93fa4828..e34d7604552 100644 --- a/src/com/google/javascript/jscomp/ReplaceMessages.java +++ b/src/com/google/javascript/jscomp/ReplaceMessages.java @@ -142,7 +142,7 @@ private void protectGetMsgCall(Node callNode, JsMessage message) throws Malforme final Node originalMessageString = checkNotNull(googGetMsg.getNext()); final Node placeholdersNode = originalMessageString.getNext(); final Node optionsNode = placeholdersNode == null ? null : placeholdersNode.getNext(); - final MsgOptions msgOptions = getOptions(optionsNode); + final MsgOptions msgOptions = getOptions(message, optionsNode); // Construct // `__jscomp_define_msg__({}, {})` @@ -728,7 +728,7 @@ private Node replaceCallNode(JsMessage message, Node callNode) throws MalformedE // optional `{ key1: value, key2: value2 }` replacements Node objLitNode = stringExprNode.getNext(); // optional replacement options, e.g. `{ html: true }` - MsgOptions options = getOptions(objLitNode != null ? objLitNode.getNext() : null); + MsgOptions options = getOptions(message, objLitNode != null ? objLitNode.getNext() : null); Map placeholderMap = createPlaceholderNodeMap(objLitNode); final ImmutableSet placeholderNames = message.placeholders(); @@ -820,7 +820,8 @@ private Node createNodeForMsgPart( return partNode; } - private static MsgOptions getOptions(@Nullable Node optionsNode) throws MalformedException { + private static MsgOptions getOptions(JsMessage message, @Nullable Node optionsNode) + throws MalformedException { MsgOptions options = new MsgOptions(); if (optionsNode == null) { return options; @@ -834,16 +835,59 @@ private static MsgOptions getOptions(@Nullable Node optionsNode) throws Malforme } String optName = aNode.getString(); Node value = aNode.getFirstChild(); - if (!value.isTrue() && !value.isFalse()) { - throw new MalformedException("Literal true or false expected", value); - } switch (optName) { case "html": options.escapeLessThan = value.isTrue(); + if (!value.isTrue() && !value.isFalse()) { + throw new MalformedException("html: Literal true or false expected", value); + } break; case "unescapeHtmlEntities": options.unescapeHtmlEntities = value.isTrue(); + if (!value.isTrue() && !value.isFalse()) { + throw new MalformedException( + "unescapeHtmlEntities: Literal true or false expected", value); + } break; + case "example": + case "original_code": + { + // Verify this format to inform users ASAP if they're supplying something unexpected. + // These options are only used when generating the XMB file, but normal compilations + // happen much more frequently than that, so we want to report these errors now. + // ``` + // { + // example: { + // 'name': 'George' + // }, + // original_code: { + // 'name': 'getName()' + // } + // } + // ``` + if (!value.isObjectLit()) { + throw new MalformedException(optName + ": object literal required", value); + } + final ImmutableSet placeholders = message.placeholders(); + Node stringKeyNode; + for (stringKeyNode = value.getFirstChild(); + stringKeyNode != null; + stringKeyNode = stringKeyNode.getNext()) { + if (!stringKeyNode.isStringKey()) { + throw new MalformedException("placeholder name required", stringKeyNode); + } + String placeholderName = stringKeyNode.getString(); + if (!placeholders.contains(placeholderName)) { + throw new MalformedException("unknown placeholder name", stringKeyNode); + } + Node placeholderValue = stringKeyNode.getOnlyChild(); + if (!placeholderValue.isStringLit()) { + throw new MalformedException("string literal required", placeholderValue); + } + } + break; + } + default: throw new MalformedException("Unexpected option", aNode); } diff --git a/test/com/google/javascript/jscomp/JsMessageExtractorTest.java b/test/com/google/javascript/jscomp/JsMessageExtractorTest.java index 625baf4141b..3aa39ea7d38 100644 --- a/test/com/google/javascript/jscomp/JsMessageExtractorTest.java +++ b/test/com/google/javascript/jscomp/JsMessageExtractorTest.java @@ -104,7 +104,7 @@ public void testOriginalCodeAndExampleMaps() { "interpolation_1", "bar.getProductName()")) .setPlaceholderNameToExampleMap( ImmutableMap.of( - "interpolation_0", "Jenny Weasley", + "interpolation_0", "Ginny Weasley", "interpolation_1", "Google Muggle Finder")) .setDesc("The welcome message.") .build(), @@ -122,7 +122,7 @@ public void testOriginalCodeAndExampleMaps() { " 'interpolation_1': 'bar.getProductName()',", " },", " example: {", - " 'interpolation_0': 'Jenny Weasley',", + " 'interpolation_0': 'Ginny Weasley',", " 'interpolation_1': 'Google Muggle Finder',", " },", " },", diff --git a/test/com/google/javascript/jscomp/ReplaceMessagesTest.java b/test/com/google/javascript/jscomp/ReplaceMessagesTest.java index f3e317d4c43..3024e62e4c9 100644 --- a/test/com/google/javascript/jscomp/ReplaceMessagesTest.java +++ b/test/com/google/javascript/jscomp/ReplaceMessagesTest.java @@ -610,6 +610,53 @@ public void testNameReplacement() { "var MSG_B = 'One ' + x + ' ph';")); } + @Test + public void testNameReplacementWithFullOptionsBag() { + registerMessage( + new JsMessage.Builder("MSG_B") + .appendStringPart("One ") + .appendPlaceholderReference("measly") + .appendStringPart(" ph") + .build()); + + multiPhaseTest( + lines( + "/** @desc d */", + "var MSG_B =", + " goog.getMsg(", + " 'asdf {$measly}',", + " {measly: x},", + " {", + // use all allowed options + " html: true,", + " unescapeHtmlEntities: true,", + // original_code and example get dropped, because they're only used + // when generating the XMB file. + " original_code: {", + " 'measly': 'getMeasley()'", + " },", + " example: {", + " 'measly': 'very little'", + " },", + " });"), + lines( + "/**", + " * @desc d", + " */", + "var MSG_B =", + " __jscomp_define_msg__(", + " {", + " \"key\":\"MSG_B\",", + " \"msg_text\":\"asdf {$measly}\",", + " \"escapeLessThan\":\"\",", + " \"unescapeHtmlEntities\":\"\"", + " },", + " {'measly': x});"), + lines( + "/** @desc d */", // + "var MSG_B = 'One ' + x + ' ph';")); + } + @Test public void testGetPropReplacement() { registerMessage(new JsMessage.Builder("MSG_C").appendPlaceholderReference("amount").build()); @@ -1167,6 +1214,105 @@ public void testTranslatedPlaceHolderMissMatch() { multiPhaseTestPreLookupError("var MSG_A = goog.getMsg('{$a}');", MESSAGE_TREE_MALFORMED); } + @Test + public void testTranslatedBadBooleanOptionValue() { + registerMessage( + new JsMessage.Builder("MSG_A") + .appendPlaceholderReference("a") + .appendStringPart("!") + .build()); + + multiPhaseTestPreLookupError( + // used an object when a boolean is required + "var MSG_A = goog.getMsg('{$a}', {'a': 'something'}, { html: {} });", + MESSAGE_TREE_MALFORMED); + multiPhaseTestPreLookupError( + // used an object when a boolean is required + "var MSG_A = goog.getMsg('{$a}', {'a': 'something'}, { unescapeHtmlEntities: {} });", + MESSAGE_TREE_MALFORMED); + } + + @Test + public void testTranslatedMisspelledExamples() { + registerMessage( + new JsMessage.Builder("MSG_A") + .appendPlaceholderReference("a") + .appendStringPart("!") + .build()); + + multiPhaseTestPreLookupError( + // mistakenly used "examples" instead of "example" + "var MSG_A = goog.getMsg('{$a}', {'a': 'something'}, { examples: { 'a': 'example a' } });", + MESSAGE_TREE_MALFORMED); + } + + @Test + public void testTranslatedMisspelledOriginalCode() { + registerMessage( + new JsMessage.Builder("MSG_A") + .appendPlaceholderReference("a") + .appendStringPart("!") + .build()); + + multiPhaseTestPreLookupError( + // mistakenly used "original" instead of "original_code" + "var MSG_A = goog.getMsg('{$a}', {'a': 'something'}, { original: { 'a': 'code' } });", + MESSAGE_TREE_MALFORMED); + } + + @Test + public void testTranslatedExampleWithUnknownPlaceholder() { + registerMessage( + new JsMessage.Builder("MSG_A") + .appendPlaceholderReference("a") + .appendStringPart("!") + .build()); + + multiPhaseTestPreLookupError( + "var MSG_A = goog.getMsg('{$a}', {'a': 'something'}, { example: { 'b': 'example a' } });", + MESSAGE_TREE_MALFORMED); + } + + @Test + public void testTranslatedExampleWithNonStringPlaceholderValue() { + registerMessage( + new JsMessage.Builder("MSG_A") + .appendPlaceholderReference("a") + .appendStringPart("!") + .build()); + + multiPhaseTestPreLookupError( + "var MSG_A = goog.getMsg('{$a}', {'a': 'something'}, { example: { 'a': 1 } });", + MESSAGE_TREE_MALFORMED); + } + + @Test + public void testTranslatedExampleWithBadValue() { + registerMessage( + new JsMessage.Builder("MSG_A") + .appendPlaceholderReference("a") + .appendStringPart("!") + .build()); + + multiPhaseTestPreLookupError( + "var MSG_A = goog.getMsg('{$a}', {'a': 'something'}, { example: 'bad value' });", + MESSAGE_TREE_MALFORMED); + } + + @Test + public void testTranslatedExampleWithComputedProperty() { + registerMessage( + new JsMessage.Builder("MSG_A") + .appendPlaceholderReference("a") + .appendStringPart("!") + .build()); + + multiPhaseTestPreLookupError( + // computed property is not allowed for examples + "var MSG_A = goog.getMsg('{$a}', {'a': 'something'}, { example: { ['a']: 'wrong' } });", + MESSAGE_TREE_MALFORMED); + } + @Test public void testBadFallbackSyntax1() { multiPhaseTestPreLookupError(