blob: 23c391a18f2d47e853525ea53695db23180aa680 [file] [log] [blame]
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// @docImport 'package:front_end/src/codes/type_labeler.dart';
library;
import 'dart:io' show File, 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,
};
Uri computeSharedGeneratedFile(Uri repoDir) {
return repoDir.resolve(
"pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart",
);
}
Uri computeCfeGeneratedFile(Uri repoDir) {
return repoDir.resolve(
"pkg/front_end/lib/src/codes/cfe_codes_generated.dart",
);
}
class Messages {
final String sharedMessages;
final String cfeMessages;
Messages(this.sharedMessages, this.cfeMessages);
}
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();
const String preamble1 = """
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
""";
const String preamble2 = """
// NOTE: THIS FILE IS GENERATED. DO NOT EDIT.
//
// Instead modify 'pkg/front_end/messages.yaml' and defer to it for the
// commands to update this file.
// ignore_for_file: lines_longer_than_80_chars
""";
sharedMessages.writeln(preamble1);
sharedMessages.writeln(preamble2);
sharedMessages.writeln("""
part of 'codes.dart';
""");
cfeMessages.writeln(preamble1);
cfeMessages.writeln("""
""");
cfeMessages.writeln(preamble2);
cfeMessages.writeln("""
part of 'cfe_codes.dart';
""");
bool hasError = false;
int largestIndex = 0;
final indexNameMap = new Map<int, String>();
List<String> keys = yaml.keys.cast<String>().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'];
if (index != null) {
if (index is! int || index < 1) {
print(
'Error: Expected positive int for "index:" field in $name,'
' but found $index',
);
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;
}
}
}
}
Template template;
try {
template = _TemplateCompiler(
name: name,
index: index,
description: description,
).compile();
} catch (e, st) {
Error.throwWithStackTrace('Error while compiling $name: $e', st);
}
if (template.isShared) {
sharedMessages.writeln(template.text);
} else {
cfeMessages.writeln(template.text);
}
}
if (largestIndex > indexNameMap.length) {
print(
'Error: The "index:" field values should be unique, consecutive'
' whole numbers starting with 1.',
);
hasError = true;
// Fall through to print more information.
}
if (hasError) {
exitCode = 1;
print('The largest index is $largestIndex');
final sortedIndices = indexNameMap.keys.toList()..sort();
int nextAvailableIndex = largestIndex + 1;
for (int index = 1; index <= sortedIndices.length; ++index) {
if (sortedIndices[index - 1] != index) {
nextAvailableIndex = index;
break;
}
}
print('The next available index is ${nextAvailableIndex}');
return new Messages('', '');
}
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].
///
/// The name [nameHint] is used if it is available. Otherwise a new name is
/// chosen by appending characters to it.
String _newName({required Set<String> usedNames, required String nameHint}) {
if (usedNames.add(nameHint)) return nameHint;
for (var i = 0; ; i++) {
var name = "${nameHint}_$i";
if (usedNames.add(name)) return name;
}
}
class Template {
final String text;
final isShared;
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;
final String problemMessage;
final String? correctionMessage;
final List<String> analyzerCodes;
final String? severity;
late final Set<_ParsedPlaceholder> parsedPlaceholders = placeholderPattern
.allMatches("$problemMessage\n${correctionMessage ?? ''}")
.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']);
Template compile() {
bool hasLabeler = parsedPlaceholders.any(
(p) => p.conversion is _LabelerConversion,
);
bool canBeShared = !hasLabeler;
var codeArguments = <String>[
if (index != null)
'index: $index'
else if (analyzerCodes.isNotEmpty)
// If "index:" is defined, then "analyzerCode:" should not be generated
// in the front end. See comment in messages.yaml
'analyzerCodes: <String>["${analyzerCodes.join('", "')}"]',
if (severity != null) 'severity: CfeSeverity.$severity',
];
if (parsedPlaceholders.isEmpty) {
codeArguments.add('problemMessage: r"""$problemMessage"""');
if (correctionMessage != null) {
codeArguments.add('correctionMessage: r"""$correctionMessage"""');
}
return new Template("""
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const MessageCode code$name =
const MessageCode(\"$name\", ${codeArguments.join(', ')},);
""", isShared: canBeShared);
}
var usedNames = {
'conversions',
'labeler',
...parsedPlaceholders.map((p) => p.name),
};
if (hasLabeler) {
withArgumentsStatements.add("TypeLabeler labeler = new TypeLabeler();");
}
var interpolators = <_ParsedPlaceholder, String>{};
for (var p in parsedPlaceholders) {
if (p.conversion?.toCode(name: p.name, type: p.templateParameterType)
case var conversion?) {
var interpolator = interpolators[p] = _newName(
usedNames: usedNames,
nameHint: p.name,
);
withArgumentsStatements.add("var $interpolator = $conversion;");
} else {
interpolators[p] = p.name;
}
}
String interpolate(String text) {
text = text
.replaceAll(r"$", r"\$")
.replaceAllMapped(
placeholderPattern,
(Match m) =>
"\${${interpolators[_ParsedPlaceholder.fromMatch(m)]}}",
);
return "\"\"\"$text\"\"\"";
}
List<String> templateArguments = <String>[];
templateArguments.add('\"$name\"');
templateArguments.add('problemMessageTemplate: r"""$problemMessage"""');
if (correctionMessage != null) {
templateArguments.add(
'correctionMessageTemplate: r"""$correctionMessage"""',
);
}
templateArguments.add("withArgumentsOld: _withArgumentsOld$name");
templateArguments.add("withArguments: _withArguments$name");
templateArguments.addAll(codeArguments);
String message = interpolate(problemMessage);
if (hasLabeler) {
message += " + labeler.originMessages";
}
var arguments = parsedPlaceholders
.map((p) => "'${p.name}': ${p.name}")
.toList();
List<String> messageArguments = <String>[
"problemMessage: ${message}",
if (correctionMessage case var correctionMessage?)
"correctionMessage: ${interpolate(correctionMessage)}",
"arguments: { ${arguments.join(', ')}, }",
];
if (codeArguments.isNotEmpty) {
codeArguments.add("");
}
var positionalParameters = parsedPlaceholders
.map((p) => '${p.templateParameterType.cfeType} ${p.name}')
.toList();
var namedParameters = parsedPlaceholders
.map((p) => 'required ${p.templateParameterType.cfeType} ${p.name}')
.toList();
var oldToNewArguments = parsedPlaceholders
.map((p) => '${p.name}: ${p.name}')
.toList();
return new Template("""
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<
Message Function(${positionalParameters.join(', ')}),
Message Function({${namedParameters.join(', ')}})
> code$name = const Template(${templateArguments.join(', ')},);
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArguments$name({${namedParameters.join(', ')}}) {
${withArgumentsStatements.join('\n ')}
return new Message(
code$name,
${messageArguments.join(', ')},);
}
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
Message _withArgumentsOld$name(${positionalParameters.join(', ')}) =>
_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});
}