Intl messages with no parameters and no name use contents for the name.

This is the first part of getting rid of the Intl transformer. With this, it's only necessary to provide explicit names for messages with arguments. Zero-argument messages are 75+% of the total.

Many of the changes relate to characters not valid in Dart identifiers in the names, and in generating method names for them. It also has a small amount of restructuring to make room for a possible alternate syntax for messages with parameters.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=130164706
diff --git a/bin/generate_from_arb.dart b/bin/generate_from_arb.dart
index fd46857..13d3680 100644
--- a/bin/generate_from_arb.dart
+++ b/bin/generate_from_arb.dart
@@ -66,13 +66,18 @@
     exit(0);
   }
 
-  // We're re-parsing the original files to find the corresponding messages,
-  // so if there are warnings extracting the messages, suppress them, and
-  // always pretend the transformer was in use so we don't fail for missing
-  // names/args.
+  // TODO(alanknight): There is a possible regression here. If a project is
+  // using the transformer and expecting it to provide names for messages with
+  // parameters, we may report those names as missing. We now have two distinct
+  // mechanisms for providing names: the transformer and just using the message
+  // text if there are no parameters. Previously this was always acting as if
+  // the transformer was in use, but that breaks the case of using the message
+  // text. The intent is to deprecate the transformer, but if this is an issue
+  // for real projects we could provide a command-line flag to indicate which
+  // sort of automated name we're using.
   extraction.suppressWarnings = true;
   var allMessages =
-      dartFiles.map((each) => extraction.parseFile(new File(each), true));
+      dartFiles.map((each) => extraction.parseFile(new File(each), false));
 
   messages = new Map();
   for (var eachMap in allMessages) {
diff --git a/lib/extract_messages.dart b/lib/extract_messages.dart
index 33a234e..e9c13b8 100644
--- a/lib/extract_messages.dart
+++ b/lib/extract_messages.dart
@@ -241,17 +241,14 @@
   /// by calling [setAttribute]. This is the common parts between
   /// [messageFromIntlMessageCall] and [messageFromDirectPluralOrGenderCall].
   MainMessage _messageFromNode(
-      MethodInvocation node, Function extract, Function setAttribute) {
+      MethodInvocation node,
+      MainMessage extract(MainMessage message, List<AstNode> arguments),
+      void setAttribute(
+          MainMessage message, String fieldName, Object fieldValue)) {
     var message = new MainMessage();
     message.sourcePosition = node.offset;
     message.endPosition = node.end;
-    if (generateNameAndArgs) {
-      // Always try for class_method if this is a class method and transforming.
-      // It will be overwritten below if the message specifies it explicitly.
-      message.name = Message.classPlusMethodName(node, name) ?? name;
-    } else {
-      message.name = name;
-    }
+
     message.arguments =
         parameters.parameters.map((x) => x.identifier.name).toList();
     var arguments = node.argumentList.arguments;
@@ -269,27 +266,49 @@
           : basicValue;
       setAttribute(message, name, value);
     }
+    if (message.name == "") {
+      if (generateNameAndArgs) {
+        // Always try for class_method if this is a class method and
+        // transforming.
+        message.name = Message.classPlusMethodName(node, name) ?? name;
+      } else if (arguments.first is SimpleStringLiteral ||
+          arguments.first is AdjacentStrings) {
+        // If there's no name, and the message text is a single string, use it
+        // as the name
+        message.name = (arguments.first as StringLiteral).stringValue;
+      }
+    }
     return message;
   }
 
+  /// Find the message pieces from a Dart interpolated string.
+  List _extractFromIntlCallWithInterpolation(
+      MainMessage message, List<AstNode> arguments) {
+    var interpolation = new InterpolationVisitor(message, extraction);
+    arguments.first.accept(interpolation);
+    if (interpolation.pieces.any((x) => x is Plural || x is Gender) &&
+        !extraction.allowEmbeddedPluralsAndGenders) {
+      if (interpolation.pieces.any((x) => x is String && x.isNotEmpty)) {
+        throw new IntlMessageExtractionException(
+            "Plural and gender expressions must be at the top level, "
+            "they cannot be embedded in larger string literals.\n");
+      }
+    }
+    return interpolation.pieces;
+  }
+
   /// Create a MainMessage from [node] using the name and
   /// parameters of the last function/method declaration we encountered
   /// and the parameters to the Intl.message call.
   MainMessage messageFromIntlMessageCall(MethodInvocation node) {
-    MainMessage extractFromIntlCall(MainMessage message, List arguments) {
+    MainMessage extractFromIntlCall(
+        MainMessage message, List<AstNode> arguments) {
       try {
-        var interpolation = new InterpolationVisitor(message, extraction);
-        arguments.first.accept(interpolation);
-        if (interpolation.pieces.any((x) => x is Plural || x is Gender) &&
-            !extraction.allowEmbeddedPluralsAndGenders) {
-          if (interpolation.pieces.any((x) => x is String && x.isNotEmpty)) {
-            throw new IntlMessageExtractionException(
-                "Plural and gender expressions must be at the top level, "
-                "they cannot be embedded in larger string literals.\n"
-                "Error at $node");
-          }
-        }
-        message.messagePieces.addAll(interpolation.pieces as List<Message>);
+        // The pieces of the message, either literal strings, or integers
+        // representing the index of the argument to be substituted.
+        List extracted;
+        extracted = _extractFromIntlCallWithInterpolation(message, arguments);
+        message.addPieces(extracted);
       } on IntlMessageExtractionException catch (e) {
         message = null;
         var err = new StringBuffer()
@@ -299,7 +318,7 @@
         extraction.onMessage(errString);
         extraction.warnings.add(errString);
       }
-      return message; // Because we may have set it to null on an error.
+      return message;
     }
 
     void setValue(MainMessage message, String fieldName, Object fieldValue) {
@@ -470,7 +489,8 @@
         break;
       default:
         throw new IntlMessageExtractionException(
-            "Invalid plural/gender/select message");
+            "Invalid plural/gender/select message ${node.methodName.name} "
+            "in $node");
     }
     message.parent = parent;
 
diff --git a/lib/generate_localized.dart b/lib/generate_localized.dart
index f6f3c44..f443ad7 100644
--- a/lib/generate_localized.dart
+++ b/lib/generate_localized.dart
@@ -72,7 +72,8 @@
       for (var original in messagesThatNeedMethods) {
         result
           ..write("  ")
-          ..write(original.toCodeForLocale(locale))
+          ..write(
+              original.toCodeForLocale(locale, _methodNameFor(original.name)))
           ..write("\n\n");
       }
     }
@@ -85,11 +86,12 @@
     var entries = usableTranslations
         .expand((translation) => translation.originalMessages)
         .map((original) =>
-            '    "${original.name}" : ${_mapReference(original, locale)}');
+            '    "${original.escapeAndValidateString(original.name)}" '
+            ': ${_mapReference(original, locale)}');
     result..write(entries.join(",\n"))..write("\n  };\n}\n");
 
-    // To preserve compatibility, we don't use the canonical version of the locale
-    // in the file name.
+    // To preserve compatibility, we don't use the canonical version of the
+    // locale in the file name.
     var filename = path.join(
         targetDir, "${generatedFilePrefix}messages_$basicLocale.dart");
     new File(filename).writeAsStringSync(result.toString());
@@ -254,6 +256,19 @@
     return 'MessageLookupByLibrary.simpleMessage("'
         '${original.translations[locale]}")';
   } else {
-    return original.name;
+    return _methodNameFor(original.name);
   }
 }
+
+/// Generated method counter for use in [_methodNameFor].
+int _methodNameCounter = 0;
+
+/// A map from Intl message names to the generated method names
+/// for their translated versions.
+Map<String, String> _internalMethodNames = {};
+
+/// Generate a Dart method name of the form "m<number>".
+String _methodNameFor(String name) {
+  return _internalMethodNames.putIfAbsent(
+      name, () => "m${_methodNameCounter++}");
+}
diff --git a/lib/message_lookup_by_library.dart b/lib/message_lookup_by_library.dart
index bf8adf3..a3d89ac 100644
--- a/lib/message_lookup_by_library.dart
+++ b/lib/message_lookup_by_library.dart
@@ -38,15 +38,22 @@
   /// If nothing is found, return [message_str]. The [desc] and [examples]
   /// parameters are ignored
   String lookupMessage(
-      String message_str, String locale, String name, List args) {
+      String message_str, String locale, String name, List args,
+      {MessageIfAbsent ifAbsent: _useOriginal}) {
     // If passed null, use the default.
     var knownLocale = locale ?? Intl.getCurrentLocale();
     var messages = (knownLocale == _lastLocale)
         ? _lastLookup
         : _lookupMessageCatalog(knownLocale);
-    // If we didn't find any messages for this locale, use the original string.
-    if (messages == null) return message_str;
-    return messages.lookupMessage(message_str, locale, name, args);
+    // If we didn't find any messages for this locale, use the original string,
+    // faking interpolations if necessary.
+    if (messages == null) {
+      return ifAbsent(message_str, args);
+    }
+    // If the name is blank, use the message as a key
+    return messages.lookupMessage(
+        message_str, locale, name ?? message_str, args,
+        ifAbsent: ifAbsent);
   }
 
   /// Find the right message lookup for [locale].
@@ -77,6 +84,9 @@
   }
 }
 
+/// The default ifAbsent method, just returns the message string.
+String _useOriginal(String message_str, List args) => message_str;
+
 /// This provides an abstract class for messages looked up in generated code.
 /// Each locale will have a separate subclass of this class with its set of
 /// messages. See generate_localized.dart.
@@ -102,10 +112,17 @@
   /// will be extracted automatically but for the time being it must be passed
   /// explicitly in the [name] and [args] arguments.
   String lookupMessage(
-      String message_str, String locale, String name, List args) {
-    if (name == null) return message_str;
+      String message_str, String locale, String name, List args,
+      {MessageIfAbsent ifAbsent}) {
+    var notFound = false;
+    if (name == null) notFound = true;
     var function = this[name];
-    return function == null ? message_str : Function.apply(function, args);
+    notFound = notFound || (function == null);
+    if (notFound) {
+      return ifAbsent == null ? message_str : ifAbsent(message_str, args);
+    } else {
+      return Function.apply(function, args);
+    }
   }
 
   /// Return our message with the given name
diff --git a/lib/src/intl_helpers.dart b/lib/src/intl_helpers.dart
index 2362277..5556b7a 100644
--- a/lib/src/intl_helpers.dart
+++ b/lib/src/intl_helpers.dart
@@ -10,6 +10,9 @@
 import 'dart:async';
 import 'package:intl/intl.dart';
 
+/// Type for the callback action when a message translation is not found.
+typedef MessageIfAbsent(String message_str, List args);
+
 /// This is used as a marker for a locale data map that hasn't been initialized,
 /// and will throw an exception on any usage that isn't the fallback
 /// patterns/symbols provided.
@@ -22,7 +25,8 @@
       (key == 'en_US') ? fallbackData : _throwException();
 
   String lookupMessage(
-          String message_str, String locale, String name, List args) =>
+          String message_str, String locale, String name, List args,
+          {MessageIfAbsent ifAbsent}) =>
       message_str;
 
   /// Given an initial locale or null, returns the locale that will be used
@@ -43,7 +47,8 @@
 
 abstract class MessageLookup {
   String lookupMessage(
-      String message_str, String locale, String name, List args);
+      String message_str, String locale, String name, List args,
+      {MessageIfAbsent ifAbsent});
   void addLocale(String localeName, Function findLocale);
 }
 
diff --git a/lib/src/intl_message.dart b/lib/src/intl_message.dart
index fea00fa..b242b15 100644
--- a/lib/src/intl_message.dart
+++ b/lib/src/intl_message.dart
@@ -82,19 +82,26 @@
       return "The 'args' argument for Intl.message must be specified";
     }
 
+    bool useMessageAsName = false;
     var messageName = arguments.firstWhere(
         (eachArg) =>
             eachArg is NamedExpression && eachArg.name.label.name == 'name',
         orElse: () => null);
-    if (!nameAndArgsGenerated && messageName == null) {
-      return "The 'name' argument for Intl.message must be specified";
+    messageName = messageName?.expression;
+    //TODO(alanknight): If we generalize this to messages with parameters
+    // this check will need to change.
+    if (!nameAndArgsGenerated && messageName == null && !hasParameters) {
+      messageName = arguments[0];
+      useMessageAsName = true;
     }
 
-    var givenName =
-        messageName == null ? null : _evaluateAsString(messageName.expression);
+    var givenName = messageName == null ? null : _evaluateAsString(messageName);
     if (messageName != null && givenName == null) {
       return "The 'name' argument for Intl.message must be a string literal";
     }
+    if (useMessageAsName) {
+      outerName = givenName;
+    }
     var hasOuterName = outerName != null;
     var simpleMatch = outerName == givenName || givenName == null;
 
@@ -106,7 +113,8 @@
           "was '$givenName' but must be '$outerName'  or '$classPlusMethod')";
     }
 
-    var simpleArguments = arguments.where((each) => each is NamedExpression &&
+    var simpleArguments = arguments.where((each) =>
+        each is NamedExpression &&
         ["desc", "name"].contains(each.name.label.name));
     var values = simpleArguments.map((each) => each.expression).toList();
     for (var arg in values) {
@@ -171,7 +179,7 @@
       "\t": r"\t",
       "\v": r"\v",
       "'": r"\'",
-      r"$" : r"\$"
+      r"$": r"\$"
     };
 
     String _escape(String s) => escapes[s] ?? s;
@@ -316,7 +324,7 @@
         nameAndArgsGenerated: nameAndArgsGenerated);
   }
 
-  void addPieces(List<Message> messages) {
+  void addPieces(List<Object> messages) {
     for (var each in messages) {
       messagePieces.add(Message.from(each, this));
     }
@@ -342,7 +350,7 @@
   String id;
 
   /// The arguments list from the Intl.message call.
-  List arguments;
+  List<String> arguments;
 
   /// The locale argument from the Intl.message call
   String locale;
@@ -380,7 +388,7 @@
 
   /// Generate code for this message, expecting it to be part of a map
   /// keyed by name with values the function that calls Intl.message.
-  String toCodeForLocale(String locale) {
+  String toCodeForLocale(String locale, String name) {
     var out = new StringBuffer()
       ..write('static $name(')
       ..write(arguments.join(", "))
diff --git a/test/message_extraction/make_hardcoded_translation.dart b/test/message_extraction/make_hardcoded_translation.dart
index ff9d7d2..0c3af44 100644
--- a/test/message_extraction/make_hardcoded_translation.dart
+++ b/test/message_extraction/make_hardcoded_translation.dart
@@ -16,11 +16,12 @@
 /// A list of the French translations that we will produce.
 var french = {
   "types": r"{a}, {b}, {c}",
-  "multiLine": "Cette message prend plusiers lignes.",
+  "This string extends across multiple lines.":
+      "Cette message prend plusiers lignes.",
   "message2": r"Un autre message avec un seul paramètre {x}",
   "alwaysTranslated": "Cette chaîne est toujours traduit",
   "message1": "Il s'agit d'un message",
-  "leadingQuotes": "\"Soi-disant\"",
+  "\"So-called\"": "\"Soi-disant\"",
   "trickyInterpolation": r"L'interpolation est délicate "
       r"quand elle se termine une phrase comme {s}.",
   "message3": "Caractères qui doivent être échapper, par exemple barres \\ "
@@ -32,7 +33,8 @@
   "staticMessage": "Cela vient d'une méthode statique",
   "notAlwaysTranslated": "Ce manque certaines traductions",
   "thisNameIsNotInTheOriginal": "Could this lead to something malicious?",
-  "originalNotInBMP": "Anciens caractères grecs jeux du pendu: 𐅆𐅇.",
+  "Ancient Greek hangman characters: 𐅆𐅇.":
+      "Anciens caractères grecs jeux du pendu: 𐅆𐅇.",
   "escapable": "Escapes: \n\r\f\b\t\v.",
   "sameContentsDifferentName": "Bonjour tout le monde",
   "differentNameSameContents": "Bonjour tout le monde",
@@ -71,18 +73,20 @@
       "=1{{amount} dollar Canadien}"
       "other{{amount} dollars Canadiens}}}"
       "other{N'importe quoi}"
-  "}}",
-  "literalDollar": "Cinq sous est US\$0.05"
+      "}}",
+  "literalDollar": "Cinq sous est US\$0.05",
+  r"'<>{}= +-_$()&^%$#@!~`'": r"interessant (fr): '<>{}= +-_$()&^%$#@!~`'"
 };
 
 /// A list of the German translations that we will produce.
 var german = {
   "types": r"{a}, {b}, {c}",
-  "multiLine": "Dieser String erstreckt sich über mehrere Zeilen erstrecken.",
+  "This string extends across multiple lines.":
+      "Dieser String erstreckt sich über mehrere Zeilen erstrecken.",
   "message2": r"Eine weitere Meldung mit dem Parameter {x}",
   "alwaysTranslated": "Diese Zeichenkette wird immer übersetzt",
   "message1": "Dies ist eine Nachricht",
-  "leadingQuotes": "\"Sogenannt\"",
+  "\"So-called\"": "\"Sogenannt\"",
   "trickyInterpolation": r"Interpolation ist schwierig, wenn es einen Satz "
       "wie dieser endet {s}.",
   "message3": "Zeichen, die Flucht benötigen, zB Schrägstriche \\ Dollar "
@@ -92,7 +96,8 @@
   "nonLambda": "Diese Methode ist nicht eine Lambda",
   "staticMessage": "Dies ergibt sich aus einer statischen Methode",
   "thisNameIsNotInTheOriginal": "Could this lead to something malicious?",
-  "originalNotInBMP": "Antike griechische Galgenmännchen Zeichen: 𐅆𐅇",
+  "Ancient Greek hangman characters: 𐅆𐅇.":
+      "Antike griechische Galgenmännchen Zeichen: 𐅆𐅇",
   "escapable": "Escapes: \n\r\f\b\t\v.",
   "sameContentsDifferentName": "Hallo Welt",
   "differentNameSameContents": "Hallo Welt",
@@ -132,7 +137,8 @@
       "other{{amount} Kanadischen dollar}}}"
       "other{whatever}"
       "}",
-  "literalDollar": "Fünf Cent US \$ 0.05"
+  "literalDollar": "Fünf Cent US \$ 0.05",
+  r"'<>{}= +-_$()&^%$#@!~`'": r"interessant (de): '<>{}= +-_$()&^%$#@!~`'"
 };
 
 /// The output directory for translated files.
diff --git a/test/message_extraction/sample_with_messages.dart b/test/message_extraction/sample_with_messages.dart
index 590daea..c22380f 100644
--- a/test/message_extraction/sample_with_messages.dart
+++ b/test/message_extraction/sample_with_messages.dart
@@ -25,14 +25,15 @@
 
 // A string with multiple adjacent strings concatenated together, verify
 // that the parser handles this properly.
-multiLine() => Intl.message(
-    "This "
+multiLine() => Intl.message("This "
     "string "
     "extends "
     "across "
     "multiple "
-    "lines.",
-    name: "multiLine");
+    "lines.");
+
+get interestingCharactersNoName =>
+    Intl.message("'<>{}= +-_\$()&^%\$#@!~`'", desc: "interesting characters");
 
 // Have types on the enclosing function's arguments.
 types(int a, String b, List c) =>
@@ -49,11 +50,10 @@
     Intl.message("Interpolation is tricky when it ends a sentence like ${s}.",
         name: 'trickyInterpolation', args: [s]);
 
-get leadingQuotes => Intl.message("\"So-called\"", name: 'leadingQuotes');
+get leadingQuotes => Intl.message("\"So-called\"");
 
 // A message with characters not in the basic multilingual plane.
-originalNotInBMP() => Intl.message("Ancient Greek hangman characters: 𐅆𐅇.",
-    name: "originalNotInBMP");
+originalNotInBMP() => Intl.message("Ancient Greek hangman characters: 𐅆𐅇.");
 
 // A string for which we don't provide all translations.
 notAlwaysTranslated() => Intl.message("This is missing some translations",
@@ -232,6 +232,7 @@
     printOut(rentAsVerb());
     printOut(rentToBePaid());
     printOut(literalDollar());
+    printOut(interestingCharactersNoName);
   });
 }
 
diff --git a/test/message_extraction/verify_messages.dart b/test/message_extraction/verify_messages.dart
index f9bacc1..b68abcd 100644
--- a/test/message_extraction/verify_messages.dart
+++ b/test/message_extraction/verify_messages.dart
@@ -6,6 +6,7 @@
 verifyResult(ignored) {
   test("Verify message translation output", actuallyVerifyResult);
 }
+
 actuallyVerifyResult() {
   var lineIterator;
   verify(String s) {
@@ -74,6 +75,7 @@
   verify('rent');
   verify('rent');
   verify('Five cents is US\$0.05');
+  verify(r"'<>{}= +-_$()&^%$#@!~`'");
 
   var fr_lines =
       expanded.skip(1).skipWhile((line) => !line.contains('----')).toList();
@@ -140,6 +142,7 @@
   verify('loyer');
   // Using a non-French format for the currency to test interpolation.
   verify('Cinq sous est US\$0.05');
+  verify(r"interessant (fr): '<>{}= +-_$()&^%$#@!~`'");
 
   var de_lines =
       fr_lines.skip(1).skipWhile((line) => !line.contains('----')).toList();
@@ -206,4 +209,5 @@
   verify('mieten');
   verify('Miete');
   verify('Fünf Cent US \$ 0.05');
+  verify(r"interessant (de): '<>{}= +-_$()&^%$#@!~`'");
 }