Skip to content

Commit

Permalink
ReplaceMessages: check for "example" and "original_code" goog.getMsg(…
Browse files Browse the repository at this point in the history
…) options

Support was previously added for these options when generating XMB files,
but the error checking code in `ReplaceMessages` was overlooked.

PiperOrigin-RevId: 441613321
  • Loading branch information
brad4d authored and Copybara-Service committed Apr 13, 2022
1 parent 9fd0b80 commit 43ddc24
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 8 deletions.
56 changes: 50 additions & 6 deletions src/com/google/javascript/jscomp/ReplaceMessages.java
Original file line number Diff line number Diff line change
Expand Up @@ -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__({<msg properties>}, {<substitutions>})`
Expand Down Expand Up @@ -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<String, Node> placeholderMap = createPlaceholderNodeMap(objLitNode);
final ImmutableSet<String> placeholderNames = message.placeholders();
Expand Down Expand Up @@ -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;
Expand All @@ -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<String> 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);
}
Expand Down
4 changes: 2 additions & 2 deletions test/com/google/javascript/jscomp/JsMessageExtractorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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',",
" },",
" },",
Expand Down
146 changes: 146 additions & 0 deletions test/com/google/javascript/jscomp/ReplaceMessagesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 43ddc24

Please sign in to comment.