Skip to content

Commit d70aa28

Browse files
stereotype441Commit Queue
authored andcommitted
[messages] Rework template parsing.
In the code generators for analyzer and CFE messages, the logic for parsing the `problemMessage` and `correctionMessage` templates is moved into the `ErrorCodeInfo` constructor. Templates are now represented in memory as a list of `TemplatePart`s, each of which is either a literal string (`TemplateLiteralPart`) or a parameter substitution (`TemplateParameterPart`). All the error checking of templates now occurs in the `ErrorCodeInfo` constructor, so it is not possible for a code generator to skip error checking. Previously, the analyzer code generator inadvertently skipped some error checking, which could result in some confusing behaviors (e.g., if a parameter name was misspelled, the text `null` was silently interpolated into the diagnostic message). Change-Id: I6a6a6964a4c47d90ae643ec9c67a955bdcb916cc Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/455261 Commit-Queue: Paul Berry <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]>
1 parent 9338d3c commit d70aa28

File tree

2 files changed

+159
-121
lines changed

2 files changed

+159
-121
lines changed

pkg/analyzer_utilities/lib/messages.dart

Lines changed: 136 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,17 @@ final RegExp placeholderPattern = RegExp(
7272
final 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].
631650
class 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`.
1005996
class 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

Comments
 (0)