[front_end] Use shared logic for interpreting messages.yaml file.

Reworks the logic in `pkg/front_end/tool/generate_messages_lib.dart`
to take advantage of shared functionality in
`pkg/analyzer_utilities/lib/messages.dart`. This reduces code
duplication between the CFE and analyzer, and should help pave the way
for sharing more diagnostic message generation logic over time.

The following declarations are moved into
`pkg/analyzer_utilities/lib/messages.dart` to avoid introducing a
circular dependency between `pkg/analyzer_utilities` and
`pkg/front_end`:

- `severityEnumNames`

- `_templateParameterNameToType`

- `Conversion`, `LabelerConversion`, `NumericConversion`, and
  `SimpleConversion`

- `ParsedPlaceholder`

Also, the enum `_TemplateParameterType` is removed in favor of the
shared enum `ErrorCodeParameterType`. To accommodate parameter types
that haven't yet been fully harmonized between the analyzer and CFE
representations, `ErrorCodeParameterType` is now capable of
representing CFE-only and analyzer-only types. In the long run I
expect to update the analyzer so that it fully supports all diagnostic
parameter types.

Change-Id: I6a6a6964928631a6f73a354f9f2d7275197f84b2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/448249
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/messages/severity.dart b/pkg/_fe_analyzer_shared/lib/src/messages/severity.dart
index b3547e9..8d97a6e 100644
--- a/pkg/_fe_analyzer_shared/lib/src/messages/severity.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/messages/severity.dart
@@ -6,15 +6,6 @@
 
 enum CfeSeverity { context, error, ignored, internalProblem, warning, info }
 
-const Map<String, String> severityEnumNames = const <String, String>{
-  'CONTEXT': 'context',
-  'ERROR': 'error',
-  'IGNORED': 'ignored',
-  'INTERNAL_PROBLEM': 'internalProblem',
-  'WARNING': 'warning',
-  'INFO': 'info',
-};
-
 const Map<String, CfeSeverity> severityEnumValues = const <String, CfeSeverity>{
   'CONTEXT': CfeSeverity.context,
   'ERROR': CfeSeverity.error,
diff --git a/pkg/analyzer_utilities/lib/messages.dart b/pkg/analyzer_utilities/lib/messages.dart
index 3a4a72a..159f5aa 100644
--- a/pkg/analyzer_utilities/lib/messages.dart
+++ b/pkg/analyzer_utilities/lib/messages.dart
@@ -11,6 +11,52 @@
 import 'package:path/path.dart';
 import 'package:yaml/yaml.dart' show loadYaml;
 
+const Map<String, String> severityEnumNames = <String, String>{
+  'CONTEXT': 'context',
+  'ERROR': 'error',
+  'IGNORED': 'ignored',
+  'INTERNAL_PROBLEM': 'internalProblem',
+  'WARNING': 'warning',
+  'INFO': 'info',
+};
+
+/// Map assigning each possible template parameter a [ErrorCodeParameterType].
+///
+// TODO(paulberry): Change the format of `messages.yaml` so that the template
+// parameters, and their types, are stated explicitly, as they are in the
+// analyzer's `messages.yaml` file. Then this constant will not be needed.
+const _templateParameterNameToType = {
+  'character': ErrorCodeParameterType.character,
+  'unicode': ErrorCodeParameterType.unicode,
+  'name': ErrorCodeParameterType.name,
+  'name2': ErrorCodeParameterType.name,
+  'name3': ErrorCodeParameterType.name,
+  'name4': ErrorCodeParameterType.name,
+  'nameOKEmpty': ErrorCodeParameterType.nameOKEmpty,
+  'names': ErrorCodeParameterType.names,
+  'lexeme': ErrorCodeParameterType.token,
+  'lexeme2': ErrorCodeParameterType.token,
+  'string': ErrorCodeParameterType.string,
+  'string2': ErrorCodeParameterType.string,
+  'string3': ErrorCodeParameterType.string,
+  'stringOKEmpty': ErrorCodeParameterType.stringOKEmpty,
+  'type': ErrorCodeParameterType.type,
+  'type2': ErrorCodeParameterType.type,
+  'type3': ErrorCodeParameterType.type,
+  'type4': ErrorCodeParameterType.type,
+  'uri': ErrorCodeParameterType.uri,
+  'uri2': ErrorCodeParameterType.uri,
+  'uri3': ErrorCodeParameterType.uri,
+  'count': ErrorCodeParameterType.int,
+  'count2': ErrorCodeParameterType.int,
+  'count3': ErrorCodeParameterType.int,
+  'count4': ErrorCodeParameterType.int,
+  'constant': ErrorCodeParameterType.constant,
+  'num1': ErrorCodeParameterType.num,
+  'num2': ErrorCodeParameterType.num,
+  'num3': ErrorCodeParameterType.num,
+};
+
 /// Decoded messages from the front end's `messages.yaml` file.
 final Map<String, FrontEndErrorCodeInfo> frontEndMessages =
     _loadFrontEndMessages();
@@ -28,8 +74,6 @@
 final RegExp oldPlaceholderPattern = RegExp(r'\{\d+\}');
 
 /// Pattern for placeholders in error message strings.
-// TODO(paulberry): share this regexp (and the code for interpreting
-// it) between the CFE and analyzer.
 final RegExp placeholderPattern = RegExp(
   '#([-a-zA-Z0-9_]+)(?:%([0-9]*).([0-9]+))?',
 );
@@ -121,6 +165,19 @@
   return lines;
 }
 
+/// Information about how to convert the CFE's internal representation of a
+/// template parameter to a string.
+///
+/// Instances of this class should implement [==] and [hashCode] so that they
+/// can be used as keys in a [Map].
+sealed class Conversion {
+  /// Returns Dart code that applies the conversion to a template parameter
+  /// having the given [name] and [type].
+  ///
+  /// If no conversion is needed, returns `null`.
+  String? toCode({required String name, required ErrorCodeParameterType type});
+}
+
 /// Information about a code generated class derived from `ErrorCode`.
 class ErrorClassInfo {
   /// The generated file containing this class.
@@ -357,7 +414,8 @@
         'Error code declares parameters using a `parameters` entry, but '
         "doesn't use them",
       );
-    } else if (parameters == null) {
+    } else if (parameters == null ||
+        parameters.any((p) => !p.type.isSupportedByAnalyzer)) {
       // Do not generate literate API yet.
       className = errorClassInfo.name;
     } else if (parameters.isNotEmpty) {
@@ -564,12 +622,73 @@
 /// In-memory representation of the type of a single diagnostic code's
 /// parameter.
 enum ErrorCodeParameterType {
+  character(
+    messagesYamlName: 'Character',
+    cfeName: 'String',
+    cfeConversion: SimpleConversion('validateCharacter'),
+  ),
+  constant(
+    messagesYamlName: 'Constant',
+    cfeName: 'Constant',
+    cfeConversion: LabelerConversion('labelConstant'),
+  ),
   element(messagesYamlName: 'Element', analyzerName: 'Element'),
-  int(messagesYamlName: 'int', analyzerName: 'int'),
+  int(messagesYamlName: 'int', analyzerName: 'int', cfeName: 'int'),
+  name(
+    messagesYamlName: 'Name',
+    cfeName: 'String',
+    cfeConversion: SimpleConversion('validateAndDemangleName'),
+  ),
+  nameOKEmpty(
+    messagesYamlName: 'NameOKEmpty',
+    cfeName: 'String',
+    cfeConversion: SimpleConversion('nameOrUnnamed'),
+  ),
+  names(
+    messagesYamlName: 'Names',
+    cfeName: 'List<String>',
+    cfeConversion: SimpleConversion('validateAndItemizeNames'),
+  ),
+  num(
+    messagesYamlName: 'num',
+    cfeName: 'num',
+    cfeConversion: SimpleConversion('formatNumber'),
+  ),
   object(messagesYamlName: 'Object', analyzerName: 'Object'),
-  string(messagesYamlName: 'String', analyzerName: 'String'),
-  type(messagesYamlName: 'Type', analyzerName: 'DartType'),
-  uri(messagesYamlName: 'Uri', analyzerName: 'Uri');
+  string(
+    messagesYamlName: 'String',
+    analyzerName: 'String',
+    cfeName: 'String',
+    cfeConversion: SimpleConversion('validateString'),
+  ),
+  stringOKEmpty(
+    messagesYamlName: 'StringOKEmpty',
+    analyzerName: 'String',
+    cfeName: 'String',
+    cfeConversion: SimpleConversion('stringOrEmpty'),
+  ),
+  token(
+    messagesYamlName: 'Token',
+    cfeName: 'Token',
+    cfeConversion: SimpleConversion('tokenToLexeme'),
+  ),
+  type(
+    messagesYamlName: 'Type',
+    analyzerName: 'DartType',
+    cfeName: 'DartType',
+    cfeConversion: LabelerConversion('labelType'),
+  ),
+  unicode(
+    messagesYamlName: 'Unicode',
+    cfeName: 'int',
+    cfeConversion: SimpleConversion('codePointToUnicode'),
+  ),
+  uri(
+    messagesYamlName: 'Uri',
+    analyzerName: 'Uri',
+    cfeName: 'Uri',
+    cfeConversion: SimpleConversion('relativizeUri'),
+  );
 
   /// Map from [messagesYamlName] to the enum constant.
   ///
@@ -581,18 +700,47 @@
   /// Name of this type as it appears in `messages.yaml`.
   final String messagesYamlName;
 
-  /// Name of this type as it appears in Dart source code.
-  final String analyzerName;
+  /// Name of this type as it appears in analyzer source code.
+  ///
+  /// If `null`, diagnostic messages using parameters of this type are not yet
+  /// supported by the analyzer (see [isSupportedByAnalyzer])
+  final String? _analyzerName;
+
+  /// Name of this type as it appears in CFE source code.
+  ///
+  /// If `null`, diagnostic messages using parameters of this type are not
+  /// supported by the CFE.
+  final String? cfeName;
+
+  /// How to convert the CFE's internal representation of a template parameter
+  /// to a string.
+  ///
+  /// This field will be `null` if either:
+  /// - Diagnostic messages using parameters of this type are not supported by
+  ///   the CFE (and hence no CFE conversion is needed), or
+  /// - No CFE conversion is needed because the type's `toString` method is
+  ///   sufficient.
+  final Conversion? cfeConversion;
 
   const ErrorCodeParameterType({
     required this.messagesYamlName,
-    required this.analyzerName,
-  });
+    String? analyzerName,
+    this.cfeName,
+    this.cfeConversion,
+  }) : _analyzerName = analyzerName;
 
   /// Decodes a type name from `messages.yaml` into an [ErrorCodeParameterName].
   factory ErrorCodeParameterType.fromMessagesYamlName(String name) =>
       _messagesYamlNameToValue[name] ??
       (throw StateError('Unknown type name: $name'));
+
+  String get analyzerName =>
+      _analyzerName ??
+      (throw 'No analyzer support for type ${json.encode(messagesYamlName)}');
+
+  /// Whether giatnostic messages using parameters of this type are supported by
+  /// the analyzer.
+  bool get isSupportedByAnalyzer => _analyzerName != null;
 }
 
 /// In-memory representation of error code information obtained from the front
@@ -605,10 +753,19 @@
   /// The index of the error in the analyzer's `fastaAnalyzerErrorCodes` table.
   final int? index;
 
-  FrontEndErrorCodeInfo.fromYaml(super.yaml)
+  /// The name of the [CfeSeverity] constant describing this error code's CFE
+  /// severity.
+  final String? cfeSeverity;
+
+  FrontEndErrorCodeInfo.fromYaml(Map<Object?, Object?> yaml)
     : analyzerCode = _decodeAnalyzerCode(yaml['analyzerCode']),
-      index = yaml['index'] as int?,
-      super.fromYaml();
+      index = _decodeIndex(yaml['index']),
+      cfeSeverity = _decodeSeverity(yaml['severity']),
+      super.fromYaml(yaml) {
+    if (yaml['problemMessage'] == null) {
+      throw 'Missing problemMessage';
+    }
+  }
 
   @override
   Map<Object?, Object?> toYaml() => {
@@ -630,6 +787,30 @@
     }
   }
 
+  static int? _decodeIndex(Object? value) {
+    switch (value) {
+      case null:
+        return null;
+      case int():
+        if (value >= 1) {
+          return value;
+        }
+    }
+    throw 'Expected positive int for "index:", but found $value';
+  }
+
+  static String? _decodeSeverity(Object? yamlEntry) {
+    switch (yamlEntry) {
+      case null:
+        return null;
+      case String():
+        return severityEnumNames[yamlEntry] ??
+            (throw "Unknown severity '$yamlEntry'");
+      default:
+        throw 'Bad severity type: ${yamlEntry.runtimeType}';
+    }
+  }
+
   static Object _encodeAnalyzerCode(List<String> analyzerCode) {
     if (analyzerCode.length == 1) {
       return analyzerCode.single;
@@ -661,3 +842,167 @@
     this.shouldIgnorePreferSingleQuotes = false,
   });
 }
+
+/// A [Conversion] that makes use of the [TypeLabeler] class.
+class LabelerConversion implements Conversion {
+  /// The name of the [TypeLabeler] method to call.
+  final String methodName;
+
+  const LabelerConversion(this.methodName);
+
+  @override
+  int get hashCode => Object.hash(runtimeType, methodName.hashCode);
+
+  @override
+  bool operator ==(Object other) =>
+      other is LabelerConversion && other.methodName == methodName;
+
+  @override
+  String toCode({required String name, required ErrorCodeParameterType type}) =>
+      'labeler.$methodName($name)';
+}
+
+/// A [Conversion] that acts on [num], applying formatting parameters specified
+/// in the template.
+class NumericConversion implements Conversion {
+  /// If non-null, the number of digits to show after the decimal point.
+  final int? fractionDigits;
+
+  /// The minimum number of characters of output to be generated.
+  ///
+  /// If the number does not require this many characters to display, extra
+  /// padding characters are inserted to the left.
+  final int padWidth;
+
+  /// If `true`, '0' is used for padding (see [padWidth]); otherwise ' ' is
+  /// used.
+  final bool padWithZeros;
+
+  NumericConversion({
+    required this.fractionDigits,
+    required this.padWidth,
+    required this.padWithZeros,
+  });
+
+  @override
+  int get hashCode => Object.hash(
+    runtimeType,
+    fractionDigits.hashCode,
+    padWidth.hashCode,
+    padWithZeros.hashCode,
+  );
+
+  @override
+  bool operator ==(Object other) =>
+      other is NumericConversion &&
+      other.fractionDigits == fractionDigits &&
+      other.padWidth == padWidth &&
+      other.padWithZeros == padWithZeros;
+
+  @override
+  String? toCode({required String name, required ErrorCodeParameterType type}) {
+    if (type != ErrorCodeParameterType.num) {
+      throw 'format suffix may only be applied to parameters of type num';
+    }
+    return 'conversions.formatNumber($name, fractionDigits: $fractionDigits, '
+        'padWidth: $padWidth, padWithZeros: $padWithZeros)';
+  }
+
+  /// Creates a [NumericConversion] from the given regular expression [match].
+  ///
+  /// [match] should be the result of matching [placeholderPattern] to the
+  /// template string.
+  ///
+  /// Returns `null` if no special numeric conversion is needed.
+  static NumericConversion? from(Match match) {
+    String? padding = match[2];
+    String? fractionDigitsStr = match[3];
+
+    int? fractionDigits = fractionDigitsStr == null
+        ? null
+        : int.parse(fractionDigitsStr);
+    if (padding != null && padding.isNotEmpty) {
+      return NumericConversion(
+        fractionDigits: fractionDigits,
+        padWidth: int.parse(padding),
+        padWithZeros: padding.startsWith('0'),
+      );
+    } else if (fractionDigits != null) {
+      return NumericConversion(
+        fractionDigits: fractionDigits,
+        padWidth: 0,
+        padWithZeros: false,
+      );
+    } else {
+      return null;
+    }
+  }
+}
+
+/// The result of parsing a [placeholderPattern] match in a template string.
+class ParsedPlaceholder {
+  /// The name of the template parameter.
+  ///
+  /// This is the identifier that immediately follows the `#`.
+  final String name;
+
+  /// The type of the corresponding template parameter.
+  final ErrorCodeParameterType templateParameterType;
+
+  /// The conversion that should be applied to the template parameter.
+  final Conversion? conversion;
+
+  /// Builds a [ParsedPlaceholder] from the given [match] of
+  /// [placeholderPattern].
+  factory ParsedPlaceholder.fromMatch(Match match) {
+    String name = match[1]!;
+
+    var templateParameterType = _templateParameterNameToType[name];
+    if (templateParameterType == null) {
+      throw "Unhandled placeholder in template: '$name'";
+    }
+
+    return ParsedPlaceholder._(
+      name: name,
+      templateParameterType: templateParameterType,
+      conversion:
+          NumericConversion.from(match) ?? templateParameterType.cfeConversion,
+    );
+  }
+
+  ParsedPlaceholder._({
+    required this.name,
+    required this.templateParameterType,
+    required this.conversion,
+  });
+
+  @override
+  int get hashCode => Object.hash(name, templateParameterType, conversion);
+
+  @override
+  bool operator ==(Object other) =>
+      other is ParsedPlaceholder &&
+      other.name == name &&
+      other.templateParameterType == templateParameterType &&
+      other.conversion == conversion;
+}
+
+/// A [Conversion] that invokes a top level function via the `conversions`
+/// import prefix.
+class SimpleConversion implements Conversion {
+  /// The name of the function to be invoked.
+  final String functionName;
+
+  const SimpleConversion(this.functionName);
+
+  @override
+  int get hashCode => Object.hash(runtimeType, functionName.hashCode);
+
+  @override
+  bool operator ==(Object other) =>
+      other is SimpleConversion && other.functionName == functionName;
+
+  @override
+  String toCode({required String name, required ErrorCodeParameterType type}) =>
+      'conversions.$functionName($name)';
+}
diff --git a/pkg/front_end/tool/generate_messages_lib.dart b/pkg/front_end/tool/generate_messages_lib.dart
index 23c391a..1f06c67 100644
--- a/pkg/front_end/tool/generate_messages_lib.dart
+++ b/pkg/front_end/tool/generate_messages_lib.dart
@@ -5,48 +5,9 @@
 /// @docImport 'package:front_end/src/codes/type_labeler.dart';
 library;
 
-import 'dart:io' show File, exitCode;
+import 'dart:io' show exitCode;
 
-import "package:_fe_analyzer_shared/src/messages/severity.dart"
-    show severityEnumNames;
-import 'package:yaml/yaml.dart' show loadYaml;
-
-/// Map assigning each possible template parameter a [_TemplateParameterType].
-///
-/// TODO(paulberry): Change the format of `messages.yaml` so that the template
-/// parameters, and their types, are stated explicitly, as they are in the
-/// analyzer's `messages.yaml` file. Then this constant will not be needed.
-const _templateParameterNameToType = {
-  'character': _TemplateParameterType.character,
-  'unicode': _TemplateParameterType.unicode,
-  'name': _TemplateParameterType.name,
-  'name2': _TemplateParameterType.name,
-  'name3': _TemplateParameterType.name,
-  'name4': _TemplateParameterType.name,
-  'nameOKEmpty': _TemplateParameterType.nameOKEmpty,
-  'names': _TemplateParameterType.names,
-  'lexeme': _TemplateParameterType.token,
-  'lexeme2': _TemplateParameterType.token,
-  'string': _TemplateParameterType.string,
-  'string2': _TemplateParameterType.string,
-  'string3': _TemplateParameterType.string,
-  'stringOKEmpty': _TemplateParameterType.stringOKEmpty,
-  'type': _TemplateParameterType.type,
-  'type2': _TemplateParameterType.type,
-  'type3': _TemplateParameterType.type,
-  'type4': _TemplateParameterType.type,
-  'uri': _TemplateParameterType.uri,
-  'uri2': _TemplateParameterType.uri,
-  'uri3': _TemplateParameterType.uri,
-  'count': _TemplateParameterType.int,
-  'count2': _TemplateParameterType.int,
-  'count3': _TemplateParameterType.int,
-  'count4': _TemplateParameterType.int,
-  'constant': _TemplateParameterType.constant,
-  'num1': _TemplateParameterType.num,
-  'num2': _TemplateParameterType.num,
-  'num3': _TemplateParameterType.num,
-};
+import 'package:analyzer_utilities/messages.dart';
 
 Uri computeSharedGeneratedFile(Uri repoDir) {
   return repoDir.resolve(
@@ -68,10 +29,6 @@
 }
 
 Messages generateMessagesFilesRaw(Uri repoDir) {
-  Uri messagesFile = repoDir.resolve("pkg/front_end/messages.yaml");
-  Map<dynamic, dynamic> yaml = loadYaml(
-    new File.fromUri(messagesFile).readAsStringSync(),
-  );
   StringBuffer sharedMessages = new StringBuffer();
   StringBuffer cfeMessages = new StringBuffer();
 
@@ -111,40 +68,23 @@
   int largestIndex = 0;
   final indexNameMap = new Map<int, String>();
 
-  List<String> keys = yaml.keys.cast<String>().toList()..sort();
+  List<String> keys = frontEndMessages.keys.toList()..sort();
   for (String name in keys) {
-    var description = yaml[name];
-    while (description is String) {
-      description = yaml[description];
-    }
-    Map<dynamic, dynamic>? map = description;
-    if (map == null) {
-      throw "No 'problemMessage:' in key $name.";
-    }
-    var index = map['index'];
+    var errorCodeInfo = frontEndMessages[name]!;
+    var index = errorCodeInfo.index;
     if (index != null) {
-      if (index is! int || index < 1) {
+      String? otherName = indexNameMap[index];
+      if (otherName != null) {
         print(
-          'Error: Expected positive int for "index:" field in $name,'
-          ' but found $index',
+          'Error: The "index:" field must be unique, '
+          'but is the same for $otherName and $name',
         );
         hasError = true;
-        index = -1;
         // Continue looking for other problems.
       } else {
-        String? otherName = indexNameMap[index];
-        if (otherName != null) {
-          print(
-            'Error: The "index:" field must be unique, '
-            'but is the same for $otherName and $name',
-          );
-          hasError = true;
-          // Continue looking for other problems.
-        } else {
-          indexNameMap[index] = name;
-          if (largestIndex < index) {
-            largestIndex = index;
-          }
+        indexNameMap[index] = name;
+        if (largestIndex < index) {
+          largestIndex = index;
         }
       }
     }
@@ -153,7 +93,7 @@
       template = _TemplateCompiler(
         name: name,
         index: index,
-        description: description,
+        errorCodeInfo: errorCodeInfo,
       ).compile();
     } catch (e, st) {
       Error.throwWithStackTrace('Error while compiling $name: $e', st);
@@ -190,10 +130,6 @@
   return new Messages("$sharedMessages", "$cfeMessages");
 }
 
-final RegExp placeholderPattern = new RegExp(
-  "#\([-a-zA-Z0-9_]+\)(?:%\([0-9]*\)\.\([0-9]+\))?",
-);
-
 /// Returns a fresh identifier that is not yet present in [usedNames], and adds
 /// it to [usedNames].
 ///
@@ -214,183 +150,6 @@
   Template(this.text, {this.isShared}) : assert(isShared != null);
 }
 
-/// Information about how to convert the CFE's internal representation of a
-/// template parameter to a string.
-///
-/// Instances of this class should implement [==] and [hashCode] so that they
-/// can be used as keys in a [Map].
-sealed class _Conversion {
-  /// Returns Dart code that applies the conversion to a template parameter
-  /// having the given [name] and [type].
-  ///
-  /// If no conversion is needed, returns `null`.
-  String? toCode({required String name, required _TemplateParameterType type});
-}
-
-/// A [_Conversion] that makes use of the [TypeLabeler] class.
-class _LabelerConversion implements _Conversion {
-  /// The name of the [TypeLabeler] method to call.
-  final String methodName;
-
-  const _LabelerConversion(this.methodName);
-
-  @override
-  int get hashCode => Object.hash(runtimeType, methodName.hashCode);
-
-  @override
-  bool operator ==(Object other) =>
-      other is _LabelerConversion && other.methodName == methodName;
-
-  @override
-  String toCode({required String name, required _TemplateParameterType type}) =>
-      'labeler.$methodName($name)';
-}
-
-/// A [_Conversion] that acts on [num], applying formatting parameters specified
-/// in the template.
-class _NumericConversion implements _Conversion {
-  /// If non-null, the number of digits to show after the decimal point.
-  final int? fractionDigits;
-
-  /// The minimum number of characters of output to be generated.
-  ///
-  /// If the number does not require this many characters to display, extra
-  /// padding characters are inserted to the left.
-  final int padWidth;
-
-  /// If `true`, '0' is used for padding (see [padWidth]); otherwise ' ' is
-  /// used.
-  final bool padWithZeros;
-
-  _NumericConversion({
-    required this.fractionDigits,
-    required this.padWidth,
-    required this.padWithZeros,
-  });
-
-  @override
-  int get hashCode => Object.hash(
-    runtimeType,
-    fractionDigits.hashCode,
-    padWidth.hashCode,
-    padWithZeros.hashCode,
-  );
-
-  @override
-  bool operator ==(Object other) =>
-      other is _NumericConversion &&
-      other.fractionDigits == fractionDigits &&
-      other.padWidth == padWidth &&
-      other.padWithZeros == padWithZeros;
-
-  @override
-  String? toCode({required String name, required _TemplateParameterType type}) {
-    if (type != _TemplateParameterType.num) {
-      throw 'format suffix may only be applied to parameters of type num';
-    }
-    return 'conversions.formatNumber($name, fractionDigits: $fractionDigits, '
-        'padWidth: $padWidth, padWithZeros: $padWithZeros)';
-  }
-
-  /// Creates a [_NumericConversion] from the given regular expression [match].
-  ///
-  /// [match] should be the result of matching [placeholderPattern] to the
-  /// template string.
-  ///
-  /// Returns `null` if no special numeric conversion is needed.
-  static _NumericConversion? from(Match match) {
-    String? padding = match[2];
-    String? fractionDigitsStr = match[3];
-
-    int? fractionDigits = fractionDigitsStr == null
-        ? null
-        : int.parse(fractionDigitsStr);
-    if (padding != null && padding.isNotEmpty) {
-      return _NumericConversion(
-        fractionDigits: fractionDigits,
-        padWidth: int.parse(padding),
-        padWithZeros: padding.startsWith('0'),
-      );
-    } else if (fractionDigits != null) {
-      return _NumericConversion(
-        fractionDigits: fractionDigits,
-        padWidth: 0,
-        padWithZeros: false,
-      );
-    } else {
-      return null;
-    }
-  }
-}
-
-/// The result of parsing a [placeholderPattern] match in a template string.
-class _ParsedPlaceholder {
-  /// The name of the template parameter.
-  ///
-  /// This is the identifier that immediately follows the `#`.
-  final String name;
-
-  /// The type of the corresponding template parameter.
-  final _TemplateParameterType templateParameterType;
-
-  /// The conversion that should be applied to the template parameter.
-  final _Conversion? conversion;
-
-  /// Builds a [_ParsedPlaceholder] from the given [match] of
-  /// [placeholderPattern].
-  factory _ParsedPlaceholder.fromMatch(Match match) {
-    String name = match[1]!;
-
-    var templateParameterType = _templateParameterNameToType[name];
-    if (templateParameterType == null) {
-      throw "Unhandled placeholder in template: '$name'";
-    }
-
-    return _ParsedPlaceholder._(
-      name: name,
-      templateParameterType: templateParameterType,
-      conversion:
-          _NumericConversion.from(match) ?? templateParameterType.conversion,
-    );
-  }
-
-  _ParsedPlaceholder._({
-    required this.name,
-    required this.templateParameterType,
-    required this.conversion,
-  });
-
-  @override
-  int get hashCode => Object.hash(name, templateParameterType, conversion);
-
-  @override
-  bool operator ==(Object other) =>
-      other is _ParsedPlaceholder &&
-      other.name == name &&
-      other.templateParameterType == templateParameterType &&
-      other.conversion == conversion;
-}
-
-/// A [_Conversion] that invokes a top level function via the `conversions`
-/// import prefix.
-class _SimpleConversion implements _Conversion {
-  /// The name of the function to be invoked.
-  final String functionName;
-
-  const _SimpleConversion(this.functionName);
-
-  @override
-  int get hashCode => Object.hash(runtimeType, functionName.hashCode);
-
-  @override
-  bool operator ==(Object other) =>
-      other is _SimpleConversion && other.functionName == functionName;
-
-  @override
-  String toCode({required String name, required _TemplateParameterType type}) =>
-      'conversions.$functionName($name)';
-}
-
 class _TemplateCompiler {
   final String name;
   final int? index;
@@ -399,26 +158,24 @@
   final List<String> analyzerCodes;
   final String? severity;
 
-  late final Set<_ParsedPlaceholder> parsedPlaceholders = placeholderPattern
+  late final Set<ParsedPlaceholder> parsedPlaceholders = placeholderPattern
       .allMatches("$problemMessage\n${correctionMessage ?? ''}")
-      .map(_ParsedPlaceholder.fromMatch)
+      .map(ParsedPlaceholder.fromMatch)
       .toSet();
   final List<String> withArgumentsStatements = [];
 
   _TemplateCompiler({
     required this.name,
     required this.index,
-    required Map<Object?, Object?> description,
-  }) : problemMessage =
-           _decodeMessage(description['problemMessage']) ??
-           (throw 'Error: missing problemMessage'),
-       correctionMessage = _decodeMessage(description['correctionMessage']),
-       analyzerCodes = _decodeAnalyzerCode(description['analyzerCode']),
-       severity = _decodeSeverity(description['severity']);
+    required FrontEndErrorCodeInfo errorCodeInfo,
+  }) : problemMessage = errorCodeInfo.problemMessage,
+       correctionMessage = errorCodeInfo.correctionMessage,
+       analyzerCodes = errorCodeInfo.analyzerCode,
+       severity = errorCodeInfo.cfeSeverity;
 
   Template compile() {
     bool hasLabeler = parsedPlaceholders.any(
-      (p) => p.conversion is _LabelerConversion,
+      (p) => p.conversion is LabelerConversion,
     );
     bool canBeShared = !hasLabeler;
 
@@ -453,7 +210,7 @@
     if (hasLabeler) {
       withArgumentsStatements.add("TypeLabeler labeler = new TypeLabeler();");
     }
-    var interpolators = <_ParsedPlaceholder, String>{};
+    var interpolators = <ParsedPlaceholder, String>{};
     for (var p in parsedPlaceholders) {
       if (p.conversion?.toCode(name: p.name, type: p.templateParameterType)
           case var conversion?) {
@@ -472,8 +229,7 @@
           .replaceAll(r"$", r"\$")
           .replaceAllMapped(
             placeholderPattern,
-            (Match m) =>
-                "\${${interpolators[_ParsedPlaceholder.fromMatch(m)]}}",
+            (Match m) => "\${${interpolators[ParsedPlaceholder.fromMatch(m)]}}",
           );
       return "\"\"\"$text\"\"\"";
     }
@@ -511,10 +267,10 @@
     }
 
     var positionalParameters = parsedPlaceholders
-        .map((p) => '${p.templateParameterType.cfeType} ${p.name}')
+        .map((p) => '${p.templateParameterType.cfeName!} ${p.name}')
         .toList();
     var namedParameters = parsedPlaceholders
-        .map((p) => 'required ${p.templateParameterType.cfeType} ${p.name}')
+        .map((p) => 'required ${p.templateParameterType.cfeName!} ${p.name}')
         .toList();
     var oldToNewArguments = parsedPlaceholders
         .map((p) => '${p.name}: ${p.name}')
@@ -539,87 +295,4 @@
     _withArguments$name(${oldToNewArguments.join(', ')});
 """, isShared: canBeShared);
   }
-
-  static List<String> _decodeAnalyzerCode(Object? yamlEntry) {
-    switch (yamlEntry) {
-      case null:
-        return const [];
-      case String():
-        return [yamlEntry];
-      case List():
-        return yamlEntry.cast<String>();
-      default:
-        throw 'Bad analyzerCode type: ${yamlEntry.runtimeType}';
-    }
-  }
-
-  static String? _decodeMessage(Object? yamlEntry) {
-    switch (yamlEntry) {
-      case null:
-        return null;
-      case String():
-        // Remove trailing whitespace. This is necessary for templates defined
-        // with `|` (verbatim) as they always contain a trailing newline that we
-        // don't want.
-        return yamlEntry.trimRight();
-      default:
-        throw 'Bad message type: ${yamlEntry.runtimeType}';
-    }
-  }
-
-  static String? _decodeSeverity(Object? yamlEntry) {
-    switch (yamlEntry) {
-      case null:
-        return null;
-      case String():
-        return severityEnumNames[yamlEntry] ??
-            (throw "Unknown severity '$yamlEntry'");
-      default:
-        throw 'Bad severity type: ${yamlEntry.runtimeType}';
-    }
-  }
-}
-
-/// Enum describing the types of template parameters supported by front_end
-/// diagnostic codes.
-///
-/// Each instance of this enum carries information about the type of the CFE's
-/// internal representation of the parameter and how to convert it to a string.
-enum _TemplateParameterType {
-  character(
-    cfeType: 'String',
-    conversion: _SimpleConversion('validateCharacter'),
-  ),
-  unicode(cfeType: 'int', conversion: _SimpleConversion('codePointToUnicode')),
-  name(
-    cfeType: 'String',
-    conversion: _SimpleConversion('validateAndDemangleName'),
-  ),
-  nameOKEmpty(
-    cfeType: 'String',
-    conversion: _SimpleConversion('nameOrUnnamed'),
-  ),
-  names(
-    cfeType: 'List<String>',
-    conversion: _SimpleConversion('validateAndItemizeNames'),
-  ),
-  string(cfeType: 'String', conversion: _SimpleConversion('validateString')),
-  stringOKEmpty(
-    cfeType: 'String',
-    conversion: _SimpleConversion('stringOrEmpty'),
-  ),
-  token(cfeType: 'Token', conversion: _SimpleConversion('tokenToLexeme')),
-  type(cfeType: 'DartType', conversion: _LabelerConversion('labelType')),
-  uri(cfeType: 'Uri', conversion: _SimpleConversion('relativizeUri')),
-  int(cfeType: 'int'),
-  constant(
-    cfeType: 'Constant',
-    conversion: _LabelerConversion('labelConstant'),
-  ),
-  num(cfeType: 'num', conversion: _SimpleConversion('formatNumber'));
-
-  final String cfeType;
-  final _Conversion? conversion;
-
-  const _TemplateParameterType({required this.cfeType, this.conversion});
 }