Intl messages with no parameters and no name use contents for the name (take 2)

Cloned from CL 130164706 by 'g4 patch'.
Original change by alanknight@alanknight:noArgMessageNames:665:citc on 2016/08/12 19:27:00.

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=130571915
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): '<>{}= +-_$()&^%$#@!~`'");
 }