/* * Copyright 2008 The Closure Compiler Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.javascript.jscomp; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import static com.google.javascript.jscomp.JsMessage.Style; import static com.google.javascript.jscomp.JsMessage.Style.CLOSURE; import static com.google.javascript.jscomp.JsMessage.Style.LEGACY; import static com.google.javascript.jscomp.JsMessage.Style.RELAX; import static com.google.javascript.jscomp.JsMessageVisitor.isLowerCamelCaseWithNumericSuffixes; import static com.google.javascript.jscomp.JsMessageVisitor.toLowerCamelCaseWithNumericSuffixes; import com.google.javascript.rhino.Node; import junit.framework.TestCase; import java.util.List; /** * Test for {@link JsMessageVisitor}. * * @author anatol@google.com (Anatol Pomazau) */ public class JsMessageVisitorTest extends TestCase { private Compiler compiler; private List messages; private boolean allowLegacyMessages; @Override protected void setUp() throws Exception { messages = Lists.newLinkedList(); allowLegacyMessages = true; } public void testJsMessageOnVar() { extractMessagesSafely( "/** @desc Hello */ var MSG_HELLO = goog.getMsg('a')"); assertEquals(0, compiler.getWarningCount()); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_HELLO", msg.getKey()); assertEquals("Hello", msg.getDesc()); } public void testJsMessageOnProperty() { extractMessagesSafely("/** @desc a */ " + "pint.sub.MSG_MENU_MARK_AS_UNREAD = goog.getMsg('a')"); assertEquals(0, compiler.getWarningCount()); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_MENU_MARK_AS_UNREAD", msg.getKey()); assertEquals("a", msg.getDesc()); } public void testOrphanedJsMessage() { extractMessagesSafely("goog.getMsg('a')"); assertEquals(1, compiler.getWarningCount()); assertEquals(0, messages.size()); JSError warn = compiler.getWarnings()[0]; assertEquals(JsMessageVisitor.MESSAGE_NODE_IS_ORPHANED, warn.getType()); } public void testMessageWithoutDescription() { extractMessagesSafely("var MSG_HELLO = goog.getMsg('a')"); assertEquals(1, compiler.getWarningCount()); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_HELLO", msg.getKey()); assertEquals(JsMessageVisitor.MESSAGE_HAS_NO_DESCRIPTION, compiler.getWarnings()[0].getType()); } public void testIncorrectMessageReporting() { extractMessages("var MSG_HELLO = goog.getMsg('a' + + 'b')"); assertEquals(1, compiler.getErrorCount()); assertEquals(0, compiler.getWarningCount()); assertEquals(0, messages.size()); JSError mailformedTreeError = compiler.getErrors()[0]; assertEquals(JsMessageVisitor.MESSAGE_TREE_MALFORMED, mailformedTreeError.getType()); assertEquals("Message parse tree malformed. " + "STRING or ADD node expected; found: POS", mailformedTreeError.description); } public void testEmptyMessage() { // This is an edge case. Empty messages are useless, but shouldn't fail extractMessagesSafely("var MSG_EMPTY = '';"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_EMPTY", msg.getKey()); assertEquals("", msg.toString()); } public void testConcatOfStrings() { extractMessagesSafely("var MSG_NOTEMPTY = 'aa' + 'bbb' \n + ' ccc';"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_NOTEMPTY", msg.getKey()); assertEquals("aabbb ccc", msg.toString()); } public void testLegacyFormatDescription() { extractMessagesSafely("var MSG_SILLY = 'silly test message';\n" + "var MSG_SILLY_HELP = 'help text';"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_SILLY", msg.getKey()); assertEquals("help text", msg.getDesc()); assertEquals("silly test message", msg.toString()); } public void testLegacyFormatParametizedFunction() { extractMessagesSafely("var MSG_SILLY = function(one, two) {" + " return one + ', ' + two + ', buckle my shoe';" + "};"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_SILLY", msg.getKey()); assertEquals(null, msg.getDesc()); assertEquals("{$one}, {$two}, buckle my shoe", msg.toString()); } public void testLegacyMessageWithDescAnnotation() { // Well, is was better do not allow legacy messages with @desc annotations, // but people love to mix styles so we need to check @desc also. extractMessagesSafely( "/** @desc The description */ var MSG_A = 'The Message';"); assertEquals(1, messages.size()); assertEquals(1, compiler.getWarningCount()); JsMessage msg = messages.get(0); assertEquals("MSG_A", msg.getKey()); assertEquals("The Message", msg.toString()); assertEquals("The description", msg.getDesc()); } public void testLegacyMessageWithDescAnnotationAndHelpVar() { // Well, is was better do not allow legacy messages with @desc annotations, // but people love to mix styles so we need to check @desc also. extractMessagesSafely( "var MSG_A_HELP = 'This is a help var';\n" + "/** @desc The description in @desc*/ var MSG_A = 'The Message';"); assertEquals(1, messages.size()); assertEquals(1, compiler.getWarningCount()); JsMessage msg = messages.get(0); assertEquals("MSG_A", msg.getKey()); assertEquals("The Message", msg.toString()); assertEquals("The description in @desc", msg.getDesc()); } public void testClosureMessageWithHelpPostfix() { extractMessagesSafely("/** @desc help text */\n" + "var MSG_FOO_HELP = goog.getMsg('Help!');"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_FOO_HELP", msg.getKey()); assertEquals("help text", msg.getDesc()); assertEquals("Help!", msg.toString()); } public void testClosureMessageWithoutGoogGetmsg() { allowLegacyMessages = false; extractMessages("var MSG_FOO_HELP = 'I am a bad message';"); assertEquals(1, messages.size()); assertEquals(1, compiler.getErrors().length); JSError error = compiler.getErrors()[0]; assertEquals(JsMessageVisitor.MESSAGE_NOT_INITIALIZED_USING_NEW_SYNTAX, error.getType()); } public void testClosureFormatParametizedFunction() { extractMessagesSafely("/** @desc help text */" + "var MSG_SILLY = goog.getMsg('{$adjective} ' + 'message', " + "{'adjective': 'silly'});"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_SILLY", msg.getKey()); assertEquals("help text", msg.getDesc()); assertEquals("{$adjective} message", msg.toString()); } public void testHugeMessage() { extractMessagesSafely("/**" + " * @desc A message with lots of stuff.\n" + " * @hidden\n" + " */" + "var MSG_HUGE = goog.getMsg(" + " '{$startLink_1}Google{$endLink}' +" + " '{$startLink_2}blah{$endLink}{$boo}{$foo_001}{$boo}' +" + " '{$foo_002}{$xxx_001}{$image}{$image_001}{$xxx_002}'," + " {'startLink_1': ''," + " 'endLink': ''," + " 'startLink_2': ''," + " 'boo': opt_data.boo," + " 'foo_001': opt_data.foo," + " 'foo_002': opt_data.boo.foo," + " 'xxx_001': opt_data.boo + opt_data.foo," + " 'image': htmlTag7," + " 'image_001': opt_data.image," + " 'xxx_002': foo.callWithOnlyTopLevelKeys(" + " bogusFn, opt_data, null, 'bogusKey1'," + " opt_data.moo, 'bogusKey2', param10)});"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_HUGE", msg.getKey()); assertEquals("A message with lots of stuff.", msg.getDesc()); assertTrue(msg.isHidden()); assertEquals("{$startLink_1}Google{$endLink}{$startLink_2}blah{$endLink}" + "{$boo}{$foo_001}{$boo}{$foo_002}{$xxx_001}{$image}" + "{$image_001}{$xxx_002}", msg.toString()); } public void testUnnamedGoogleMessage() { extractMessagesSafely("var MSG_UNNAMED_2 = goog.getMsg('Hullo');"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals(null, msg.getDesc()); assertEquals("MSG_16LJMYKCXT84X", msg.getKey()); assertEquals("MSG_16LJMYKCXT84X", msg.getId()); } public void testEmptyTextMessage() { extractMessagesSafely("/** @desc text */ var MSG_FOO = goog.getMsg('');"); assertEquals(1, messages.size()); assertEquals(1, compiler.getWarningCount()); assertEquals("Message value of MSG_FOO is just an empty string. " + "Empty messages are forbidden.", compiler.getWarnings()[0].description); } public void testEmptyTextComplexMessage() { extractMessagesSafely("/** @desc text */ var MSG_BAR = goog.getMsg(" + "'' + '' + '' + ''\n+'');"); assertEquals(1, messages.size()); assertEquals(1, compiler.getWarningCount()); assertEquals("Message value of MSG_BAR is just an empty string. " + "Empty messages are forbidden.", compiler.getWarnings()[0].description); } public void testMessageIsNoUnnamed() { extractMessagesSafely("var MSG_UNNAMED_ITEM = goog.getMsg('Hullo');"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_UNNAMED_ITEM", msg.getKey()); assertFalse(msg.isHidden()); } public void testMsgVarWithoutAssignment() { extractMessages("var MSG_SILLY;"); assertEquals(1, compiler.getErrors().length); JSError error = compiler.getErrors()[0]; assertEquals(JsMessageVisitor.MESSAGE_HAS_NO_VALUE, error.getType()); } public void testRegularVarWithoutAssignment() { extractMessagesSafely("var SILLY;"); assertTrue(messages.isEmpty()); } public void itIsNotImplementedYet_testMsgPropertyWithoutAssignment() { extractMessages("goog.message.MSG_SILLY_PROP;"); assertEquals(1, compiler.getErrors().length); JSError error = compiler.getErrors()[0]; assertEquals("Message MSG_SILLY_PROP has no value", error.description); } public void testMsgVarWithIncorrectRightSide() { extractMessages("var MSG_SILLY = 0;"); assertEquals(1, compiler.getErrors().length); JSError error = compiler.getErrors()[0]; assertEquals("Message parse tree malformed. Cannot parse value of " + "message MSG_SILLY", error.description); } public void testIncorrectMessage() { extractMessages("DP_DatePicker.MSG_DATE_SELECTION = {};"); assertEquals(0, messages.size()); assertEquals(1, compiler.getErrors().length); JSError error = compiler.getErrors()[0]; assertEquals("Message parse tree malformed. "+ "Message must be initialized using goog.getMsg function.", error.description); } public void testUnrecognizedFunction() { allowLegacyMessages = false; extractMessages("DP_DatePicker.MSG_DATE_SELECTION = somefunc('a')"); assertEquals(0, messages.size()); assertEquals(1, compiler.getErrors().length); JSError error = compiler.getErrors()[0]; assertEquals("Message parse tree malformed. "+ "Message initialized using unrecognized function. " + "Please use goog.getMsg() instead.", error.description); } public void testExtractPropertyMessage() { extractMessagesSafely("/**" + " * @desc A message that demonstrates placeholders\n" + " * @hidden\n" + " */" + "a.b.MSG_SILLY = goog.getMsg(\n" + " '{$adjective} ' + '{$someNoun}',\n" + " {'adjective': adj, 'someNoun': noun});"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_SILLY", msg.getKey()); assertEquals("{$adjective} {$someNoun}", msg.toString()); assertEquals("A message that demonstrates placeholders", msg.getDesc()); assertTrue(msg.isHidden()); } public void testAlmostButNotExternalMessage() { extractMessagesSafely( "/** @desc External */ var MSG_EXTERNAL = goog.getMsg('External');"); assertEquals(0, compiler.getWarningCount()); assertEquals(1, messages.size()); assertFalse(messages.get(0).isExternal()); assertEquals("MSG_EXTERNAL", messages.get(0).getKey()); } public void testExternalMessage() { extractMessagesSafely("var MSG_EXTERNAL_111 = goog.getMsg('Hello World');"); assertEquals(0, compiler.getWarningCount()); assertEquals(1, messages.size()); assertTrue(messages.get(0).isExternal()); assertEquals("111", messages.get(0).getId()); } public void testIsValidMessageNameStrict() { JsMessageVisitor visitor = new DummyJsVisitor(CLOSURE); assertTrue(visitor.isMessageName("MSG_HELLO", true)); assertTrue(visitor.isMessageName("MSG_", true)); assertTrue(visitor.isMessageName("MSG_HELP", true)); assertTrue(visitor.isMessageName("MSG_FOO_HELP", true)); assertFalse(visitor.isMessageName("_FOO_HELP", true)); assertFalse(visitor.isMessageName("MSGFOOP", true)); } public void testIsValidMessageNameRelax() { JsMessageVisitor visitor = new DummyJsVisitor(RELAX); assertFalse(visitor.isMessageName("MSG_HELP", false)); assertFalse(visitor.isMessageName("MSG_FOO_HELP", false)); } public void testIsValidMessageNameLegacy() { theseAreLegacyMessageNames(new DummyJsVisitor(RELAX)); theseAreLegacyMessageNames(new DummyJsVisitor(LEGACY)); } private void theseAreLegacyMessageNames(JsMessageVisitor visitor) { assertTrue(visitor.isMessageName("MSG_HELLO", false)); assertTrue(visitor.isMessageName("MSG_", false)); assertFalse(visitor.isMessageName("MSG_HELP", false)); assertFalse(visitor.isMessageName("MSG_FOO_HELP", false)); assertFalse(visitor.isMessageName("_FOO_HELP", false)); assertFalse(visitor.isMessageName("MSGFOOP", false)); } public void testUnexistedPlaceholders() { extractMessages("var MSG_FOO = goog.getMsg('{$foo}:', {});"); assertEquals(0, messages.size()); JSError[] errors = compiler.getErrors(); assertEquals(1, errors.length); JSError error = errors[0]; assertEquals(JsMessageVisitor.MESSAGE_TREE_MALFORMED, error.getType()); assertEquals("Message parse tree malformed. Unrecognized message " + "placeholder referenced: foo", error.description); } public void testUnusedReferenesAreNotOK() { extractMessages("/** @desc AA */ " + "var MSG_FOO = goog.getMsg('lalala:', {foo:1});"); assertEquals(0, messages.size()); JSError[] errors = compiler.getErrors(); assertEquals(1, errors.length); JSError error = errors[0]; assertEquals(JsMessageVisitor.MESSAGE_TREE_MALFORMED, error.getType()); assertEquals("Message parse tree malformed. Unused message placeholder: " + "foo", error.description); } public void testDuplicatePlaceHoldersAreBad() { extractMessages("var MSG_FOO = goog.getMsg(" + "'{$foo}:', {'foo': 1, 'foo' : 2});"); assertEquals(0, messages.size()); JSError[] errors = compiler.getErrors(); assertEquals(1, errors.length); JSError error = errors[0]; assertEquals(JsMessageVisitor.MESSAGE_TREE_MALFORMED, error.getType()); assertEquals("Message parse tree malformed. Duplicate placeholder " + "name: foo", error.description); } public void testDuplicatePlaceholderReferencesAreOk() { extractMessagesSafely("var MSG_FOO = goog.getMsg(" + "'{$foo}:, {$foo}', {'foo': 1});"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("{$foo}:, {$foo}", msg.toString()); } public void testCamelcasePlaceholderNamesAreOk() { extractMessagesSafely("var MSG_WITH_CAMELCASE = goog.getMsg(" + "'Slide {$slideNumber}:', {'slideNumber': opt_index + 1});"); assertEquals(1, messages.size()); JsMessage msg = messages.get(0); assertEquals("MSG_WITH_CAMELCASE", msg.getKey()); assertEquals("Slide {$slideNumber}:", msg.toString()); List parts = msg.parts(); assertEquals(3, parts.size()); assertEquals("slideNumber", ((JsMessage.PlaceholderReference)parts.get(1)).getName()); } public void testWithNonCamelcasePlaceholderNamesAreNotOk() { extractMessages("var MSG_WITH_CAMELCASE = goog.getMsg(" + "'Slide {$slide_number}:', {'slide_number': opt_index + 1});"); assertEquals(0, messages.size()); JSError[] errors = compiler.getErrors(); assertEquals(1, errors.length); JSError error = errors[0]; assertEquals(JsMessageVisitor.MESSAGE_TREE_MALFORMED, error.getType()); assertEquals("Message parse tree malformed. Placeholder name not in " + "lowerCamelCase: slide_number", error.description); } public void testUnquotedPlaceholdersAreOk() { extractMessagesSafely("/** @desc Hello */ " + "var MSG_FOO = goog.getMsg('foo {$unquoted}:', {unquoted: 12});"); assertEquals(1, messages.size()); assertEquals(0, compiler.getWarningCount()); } public void testIsLowerCamelCaseWithNumericSuffixes() { assertTrue(isLowerCamelCaseWithNumericSuffixes("name")); assertFalse(isLowerCamelCaseWithNumericSuffixes("NAME")); assertFalse(isLowerCamelCaseWithNumericSuffixes("Name")); assertTrue(isLowerCamelCaseWithNumericSuffixes("a4Letter")); assertFalse(isLowerCamelCaseWithNumericSuffixes("A4_LETTER")); assertTrue(isLowerCamelCaseWithNumericSuffixes("startSpan_1_23")); assertFalse(isLowerCamelCaseWithNumericSuffixes("startSpan_1_23b")); assertFalse(isLowerCamelCaseWithNumericSuffixes("START_SPAN_1_23")); assertFalse(isLowerCamelCaseWithNumericSuffixes("")); } public void testToLowerCamelCaseWithNumericSuffixes() { assertEquals("name", toLowerCamelCaseWithNumericSuffixes("NAME")); assertEquals("a4Letter", toLowerCamelCaseWithNumericSuffixes("A4_LETTER")); assertEquals("startSpan_1_23", toLowerCamelCaseWithNumericSuffixes("START_SPAN_1_23")); } public void testDuplicateMessageError() { extractMessages( "(function () {/** @desc Hello */ var MSG_HELLO = goog.getMsg('a')})" + "(function () {/** @desc Hello2 */ var MSG_HELLO = goog.getMsg('a')})"); assertEquals(0, compiler.getWarningCount()); assertOneError(JsMessageVisitor.MESSAGE_DUPLICATE_KEY); } public void testNoDuplicateErrorOnExternMessage() { extractMessagesSafely( "(function () {/** @desc Hello */ " + "var MSG_EXTERNAL_2 = goog.getMsg('a')})" + "(function () {/** @desc Hello2 */ " + "var MSG_EXTERNAL_2 = goog.getMsg('a')})"); } public void testErrorWhenUsingMsgPrefixWithFallback() { extractMessages( "/** @desc Hello */ var MSG_HELLO_1 = goog.getMsg('hello');\n" + "/** @desc Hello */ var MSG_HELLO_2 = goog.getMsg('hello');\n" + "/** @desc Hello */ " + "var MSG_HELLO_3 = goog.getMsgWithFallback(MSG_HELLO_1, MSG_HELLO_2);"); assertOneError(JsMessageVisitor.MESSAGE_TREE_MALFORMED); } private void assertOneError(DiagnosticType type) { String errors = Joiner.on("\n").join(compiler.getErrors()); assertEquals("There should be one error. " + errors, 1, compiler.getErrorCount()); JSError error = compiler.getErrors()[0]; assertEquals(type, error.getType()); } private void extractMessagesSafely(String input) { extractMessages(input); JSError[] errors = compiler.getErrors(); assertEquals( "Unexpected error(s): " + Joiner.on("\n").join(compiler.getErrors()), 0, compiler.getErrorCount()); } private void extractMessages(String input) { compiler = new Compiler(); Node root = compiler.parseTestCode(input); JsMessageVisitor visitor = new CollectMessages(compiler); visitor.process(null, root); } private class CollectMessages extends JsMessageVisitor { private CollectMessages(Compiler compiler) { super(compiler, true, Style.getFromParams(true, allowLegacyMessages), null); } @Override protected void processJsMessage(JsMessage message, JsMessageDefinition definition) { messages.add(message); } } private class DummyJsVisitor extends JsMessageVisitor { private DummyJsVisitor(Style style) { super(null, true, style, null); } @Override protected void processJsMessage(JsMessage message, JsMessageDefinition definition) { // no-op } } }