@@ -72,14 +72,17 @@ final RegExp placeholderPattern = RegExp(
7272final SharedToAnalyzerErrorCodeTables sharedToAnalyzerErrorCodeTables =
7373 SharedToAnalyzerErrorCodeTables ._(feAnalyzerSharedMessages);
7474
75- /// Convert a template string (which uses placeholders matching
76- /// [placeholderPattern] ) to an analyzer internal template string (which uses
75+ /// Converts a template to an analyzer internal template string (which uses
7776/// placeholders like `{0}` ).
78- String convertTemplate (Map <String , int > placeholderToIndexMap, String entry) {
79- return entry.replaceAllMapped (
80- placeholderPattern,
81- (match) => '{${placeholderToIndexMap [match .group (0 )!]}}' ,
82- );
77+ String convertTemplate (List <TemplatePart > template) {
78+ return template
79+ .map (
80+ (part) => switch (part) {
81+ TemplateLiteralPart (: var text) => text,
82+ TemplateParameterPart (: var parameter) => '{${parameter .index }}' ,
83+ },
84+ )
85+ .join ();
8386}
8487
8588/// Decodes a YAML object (in CFE style `messages.yaml` format) into a map from
@@ -310,7 +313,7 @@ abstract class ErrorCodeInfo {
310313
311314 /// If the error code has an associated correctionMessage, the template for
312315 /// it.
313- final String ? correctionMessage;
316+ final List < TemplatePart > ? correctionMessage;
314317
315318 /// If non-null, the deprecation message for this error code.
316319 final String ? deprecatedMessage;
@@ -328,7 +331,7 @@ abstract class ErrorCodeInfo {
328331 final bool isUnresolvedIdentifier;
329332
330333 /// The problemMessage for the error code.
331- final String problemMessage;
334+ final List < TemplatePart > problemMessage;
332335
333336 /// If present, the SDK version this error code stopped being reported in.
334337 /// If not null, error codes will not be generated for this error.
@@ -362,39 +365,37 @@ abstract class ErrorCodeInfo {
362365 this .hasPublishedDocs,
363366 this .isUnresolvedIdentifier = false ,
364367 this .sharedName,
365- required this .problemMessage ,
366- this .correctionMessage ,
368+ required Object ? problemMessageYaml ,
369+ required Object ? correctionMessageYaml ,
367370 this .deprecatedMessage,
368371 this .previousName,
369372 this .removedIn,
370373 required this .parameters,
371374 this .yamlNode,
372- }) {
373- for (var MapEntry (: key, : value) in {
374- 'problemMessage' : problemMessage,
375- 'correctionMessage' : correctionMessage,
376- }.entries) {
377- if (value == null ) continue ;
378- if (value.contains (oldPlaceholderPattern)) {
379- throw StateError (
380- '$key is ${json .encode (value )}, which contains an old-style analyzer '
381- 'placeholder pattern. Please convert to #NAME format.' ,
382- );
383- }
384- }
385- }
375+ }) : problemMessage =
376+ _decodeMessage (
377+ problemMessageYaml,
378+ parameters: parameters,
379+ kind: 'problemMessage' ,
380+ ) ??
381+ [],
382+ correctionMessage = _decodeMessage (
383+ correctionMessageYaml,
384+ parameters: parameters,
385+ kind: 'correctionMessage' ,
386+ );
386387
387388 /// Decodes an [ErrorCodeInfo] object from its YAML representation.
388389 ErrorCodeInfo .fromYaml (YamlMap yaml)
389390 : this (
390391 comment: yaml['comment' ] as String ? ,
391- correctionMessage : _decodeMessage ( yaml['correctionMessage' ]) ,
392+ correctionMessageYaml : yaml['correctionMessage' ],
392393 deprecatedMessage: yaml['deprecatedMessage' ] as String ? ,
393394 documentation: yaml['documentation' ] as String ? ,
394395 hasPublishedDocs: yaml['hasPublishedDocs' ] as bool ? ,
395396 isUnresolvedIdentifier:
396397 yaml['isUnresolvedIdentifier' ] as bool ? ?? false ,
397- problemMessage : _decodeMessage ( yaml['problemMessage' ]) ?? '' ,
398+ problemMessageYaml : yaml['problemMessage' ],
398399 sharedName: yaml['sharedName' ] as String ? ,
399400 removedIn: yaml['removedIn' ] as String ? ,
400401 previousName: yaml['previousName' ] as String ? ,
@@ -406,14 +407,6 @@ abstract class ErrorCodeInfo {
406407 /// its error codes should no longer be generated.
407408 bool get isRemoved => removedIn != null ;
408409
409- /// Given a messages.yaml entry, come up with a mapping from placeholder
410- /// patterns in its message strings to their corresponding indices.
411- Map <String , int > computePlaceholderToIndexMap () {
412- // Parameters are always explicitly specified, so the mapping is determined
413- // by the order in which they were specified.
414- return {for (var (index, name) in parameters.keys.indexed) '#$name ' : index};
415- }
416-
417410 void outputConstantHeader (StringSink out) {
418411 out.write (toAnalyzerComments (indent: ' ' ));
419412 if (deprecatedMessage != null ) {
@@ -433,10 +426,10 @@ abstract class ErrorCodeInfo {
433426 }) {
434427 var correctionMessage = this .correctionMessage;
435428 var parameters = this .parameters;
436- var usesParameters = [
437- problemMessage,
438- correctionMessage ,
439- ]. any ((value) => value != null && value. contains (placeholderPattern) );
429+ var usesParameters = [problemMessage, correctionMessage]. any (
430+ (value) =>
431+ value != null && value. any ((part) => part is TemplateParameterPart ) ,
432+ );
440433 var constantName = diagnosticCode.toCamelCase ();
441434 String className;
442435 String templateParameters = '' ;
@@ -487,17 +480,16 @@ static LocatableDiagnostic $withArgumentsName({$withArgumentsParams}) {
487480 '${sharedNameReference ?? "'${sharedName ?? diagnosticCode }'" },' ,
488481 );
489482 var maxWidth = 80 - 8 /* indentation */ - 2 /* quotes */ - 1 /* comma */ ;
490- var placeholderToIndexMap = computePlaceholderToIndexMap ();
491- var messageAsCode = convertTemplate (placeholderToIndexMap, problemMessage);
483+ var messageAsCode = convertTemplate (problemMessage);
492484 var messageLines = _splitText (
493485 messageAsCode,
494486 maxWidth: maxWidth,
495487 firstLineWidth: maxWidth + 4 ,
496488 );
497489 constant.writeln ('${messageLines .map (_encodeString ).join ('\n ' )},' );
498- if (correctionMessage is String ) {
490+ if (correctionMessage != null ) {
499491 constant.write ('correctionMessage: ' );
500- var code = convertTemplate (placeholderToIndexMap, correctionMessage);
492+ var code = convertTemplate (correctionMessage);
501493 var codeLines = _splitText (code, maxWidth: maxWidth);
502494 constant.writeln ('${codeLines .map (_encodeString ).join ('\n ' )},' );
503495 }
@@ -581,15 +573,42 @@ static LocatableDiagnostic $withArgumentsName({$withArgumentsParams}) {
581573 return jsonEncoded.replaceAll (r'$' , r'\$' );
582574 }
583575
584- static String ? _decodeMessage (Object ? rawMessage) {
576+ static List <TemplatePart >? _decodeMessage (
577+ Object ? rawMessage, {
578+ required Map <String , ErrorCodeParameter > parameters,
579+ required String kind,
580+ }) {
585581 switch (rawMessage) {
586582 case null :
587583 return null ;
588584 case String ():
589585 // Remove trailing whitespace. This is necessary for templates defined
590586 // with `|` (verbatim) as they always contain a trailing newline that we
591587 // don't want.
592- return rawMessage.trimRight ();
588+ var text = rawMessage.trimRight ();
589+ if (text.contains (oldPlaceholderPattern)) {
590+ throw StateError (
591+ '$kind is ${json .encode (text )}, which contains an old-style '
592+ 'analyzer placeholder pattern. Please convert to #NAME format.' ,
593+ );
594+ }
595+
596+ var template = < TemplatePart > [];
597+ var i = 0 ;
598+ for (var match in placeholderPattern.allMatches (text)) {
599+ var matchStart = match.start;
600+ if (matchStart > i) {
601+ template.add (TemplateLiteralPart (text.substring (i, matchStart)));
602+ }
603+ template.add (
604+ TemplateParameterPart .fromMatch (match, parameters: parameters),
605+ );
606+ i = match.end;
607+ }
608+ if (text.length > i) {
609+ template.add (TemplateLiteralPart (text.substring (i)));
610+ }
611+ return template;
593612 default :
594613 throw 'Bad message type: ${rawMessage .runtimeType }' ;
595614 }
@@ -602,15 +621,18 @@ static LocatableDiagnostic $withArgumentsName({$withArgumentsParams}) {
602621 if (yaml == 'none' ) return const {};
603622 yaml as Map <Object ?, Object ?>;
604623 var result = < String , ErrorCodeParameter > {};
624+ var index = 0 ;
605625 for (var MapEntry (: key, : value) in yaml.entries) {
606626 switch ((key as String ).split (' ' )) {
607627 case [var type, var name]:
608628 if (result.containsKey (name)) {
609629 throw StateError ('Duplicate parameter name: $name ' );
610630 }
611631 result[name] = ErrorCodeParameter (
632+ name: name,
612633 type: ErrorCodeParameterType .fromMessagesYamlName (type),
613634 comment: value as String ,
635+ index: index++ ,
614636 );
615637 default :
616638 throw StateError (
@@ -625,14 +647,18 @@ static LocatableDiagnostic $withArgumentsName({$withArgumentsParams}) {
625647
626648/// In-memory representation of a single key/value pair from the `parameters`
627649/// map for an error code.
628- ///
629- /// The name of the parameter is not included, since parameters are stored in a
630- /// map from name to [ErrorCodeParameter] .
631650class ErrorCodeParameter {
651+ final String name;
632652 final ErrorCodeParameterType type;
633653 final String comment;
654+ final int index;
634655
635- ErrorCodeParameter ({required this .type, required this .comment});
656+ ErrorCodeParameter ({
657+ required this .name,
658+ required this .type,
659+ required this .comment,
660+ required this .index,
661+ });
636662}
637663
638664/// In-memory representation of the type of a single diagnostic code's
@@ -965,41 +991,6 @@ class NumericConversion implements Conversion {
965991 }
966992}
967993
968- /// The result of parsing a [placeholderPattern] match in a template string.
969- class ParsedPlaceholder {
970- /// The name of the template parameter.
971- ///
972- /// This is the identifier that immediately follows the `#` .
973- final String name;
974-
975- /// The conversion specified in the placeholder, if any.
976- ///
977- /// If `null` , the default conversion for the parameter's type will be used.
978- final Conversion ? conversionOverride;
979-
980- /// Builds a [ParsedPlaceholder] from the given [match] of
981- /// [placeholderPattern] .
982- factory ParsedPlaceholder .fromMatch (Match match) {
983- String name = match[1 ]! ;
984-
985- return ParsedPlaceholder ._(
986- name: name,
987- conversionOverride: NumericConversion .from (match),
988- );
989- }
990-
991- ParsedPlaceholder ._({required this .name, required this .conversionOverride});
992-
993- @override
994- int get hashCode => Object .hash (name, conversionOverride);
995-
996- @override
997- bool operator == (Object other) =>
998- other is ParsedPlaceholder &&
999- other.name == name &&
1000- other.conversionOverride == conversionOverride;
1001- }
1002-
1003994/// In-memory representation of error code information obtained from the file
1004995/// `pkg/_fe_analyzer_shared/messages.yaml` .
1005996class SharedErrorCodeInfo extends CfeStyleErrorCodeInfo {
@@ -1090,3 +1081,64 @@ class SimpleConversion implements Conversion {
10901081 String toCode ({required String name, required ErrorCodeParameterType type}) =>
10911082 'conversions.$functionName ($name )' ;
10921083}
1084+
1085+ /// [TemplatePart] representing a literal string of characters, with no
1086+ /// parameter substitutions.
1087+ class TemplateLiteralPart implements TemplatePart {
1088+ /// The literal text.
1089+ final String text;
1090+
1091+ TemplateLiteralPart (this .text);
1092+ }
1093+
1094+ /// [TemplatePart] representing a parameter to be substituted into the
1095+ /// diagnostic message.
1096+ class TemplateParameterPart implements TemplatePart {
1097+ /// The parameter to be substituted.
1098+ final ErrorCodeParameter parameter;
1099+
1100+ /// The conversion to apply to the parameter.
1101+ ///
1102+ /// If `null` , the default conversion for the parameter's type will be used.
1103+ final Conversion ? conversionOverride;
1104+
1105+ /// Builds a [TemplateParameterPart] from the given [match] of
1106+ /// [placeholderPattern] .
1107+ factory TemplateParameterPart .fromMatch (
1108+ Match match, {
1109+ required Map <String , ErrorCodeParameter > parameters,
1110+ }) {
1111+ String name = match[1 ]! ;
1112+ var parameter = parameters[name];
1113+ if (parameter == null ) {
1114+ throw StateError (
1115+ 'Placeholder ${json .encode (name )} not declared as a parameter' ,
1116+ );
1117+ }
1118+
1119+ return TemplateParameterPart ._(
1120+ parameter: parameter,
1121+ conversionOverride: NumericConversion .from (match),
1122+ );
1123+ }
1124+
1125+ TemplateParameterPart ._({
1126+ required this .parameter,
1127+ required this .conversionOverride,
1128+ });
1129+
1130+ @override
1131+ int get hashCode => Object .hash (parameter, conversionOverride);
1132+
1133+ @override
1134+ bool operator == (Object other) =>
1135+ other is TemplateParameterPart &&
1136+ other.parameter == parameter &&
1137+ other.conversionOverride == conversionOverride;
1138+ }
1139+
1140+ /// A part of a parsed template string.
1141+ ///
1142+ /// Each `problemMessage` and `correctionMessage` template string in a
1143+ /// `messages.yaml` file is decoded into a list of [TemplatePart] .
1144+ sealed class TemplatePart {}
0 commit comments