blob: 6008126fec2a7a2b2c57b78c97903899343f287c [file] [log] [blame] [edit]
// 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:convert';
import 'dart:io' show exitCode;
import 'package:analyzer_utilities/extensions/string.dart';
import 'package:analyzer_utilities/messages.dart';
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) {
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 = frontEndAndSharedMessages.keys.toList()..sort();
var pseudoSharedCodeValues = <String>{};
for (String name in keys) {
var errorCodeInfo = frontEndAndSharedMessages[name]!;
var index = errorCodeInfo.index;
if (index != null) {
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;
}
}
}
var forFeAnalyzerShared =
errorCodeInfo is SharedErrorCodeInfo ||
errorCodeInfo is FrontEndErrorCodeInfo &&
errorCodeInfo.pseudoSharedCode != null;
String template;
try {
template = _TemplateCompiler(
name: name,
index: index,
errorCodeInfo: errorCodeInfo,
pseudoSharedCodeValues: forFeAnalyzerShared
? pseudoSharedCodeValues
: null,
).compile();
} catch (e, st) {
Error.throwWithStackTrace('Error while compiling $name: $e', st);
}
if (forFeAnalyzerShared) {
sharedMessages.writeln(template);
} else {
cfeMessages.writeln(template);
}
}
sharedMessages.writeln();
sharedMessages.writeln(
'/// Enum containing analyzer error codes referenced by '
'[Code.pseudoSharedCode].',
);
sharedMessages.writeln('enum PseudoSharedCode {');
for (var code in pseudoSharedCodeValues.toList()..sort()) {
sharedMessages.writeln(' $code,');
}
sharedMessages.writeln('}');
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");
}
/// 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 _TemplateCompiler {
final String name;
final int? index;
final String problemMessage;
final String? correctionMessage;
final String? severity;
final Map<String, ErrorCodeParameter> parameters;
final String? pseudoSharedCode;
/// If the template will be generated into `pkg/_fe_analyzer_shared`, a set of
/// strings representing the values that will be generated for the
/// `PseudoSharedCode` enum; otherwise `null`.
///
/// The template compiler will add to this set as needed.
final Set<String>? pseudoSharedCodeValues;
late final Set<String> usedNames = {
'conversions',
'labeler',
...parameters.keys,
};
late final List<String> arguments = parameters.keys
.map((name) => "'$name': $name")
.toList();
final Map<ParsedPlaceholder, String> interpolators = {};
final List<String> withArgumentsStatements = [];
bool hasLabeler = false;
_TemplateCompiler({
required this.name,
required this.index,
required CfeStyleErrorCodeInfo errorCodeInfo,
required this.pseudoSharedCodeValues,
}) : problemMessage = errorCodeInfo.problemMessage,
correctionMessage = errorCodeInfo.correctionMessage,
severity = errorCodeInfo.cfeSeverity,
parameters = errorCodeInfo.parameters,
pseudoSharedCode = errorCodeInfo is FrontEndErrorCodeInfo
? errorCodeInfo.pseudoSharedCode
: null;
String compile() {
var codeArguments = <String>[
if (index != null)
'index: $index'
else if (pseudoSharedCodeValues != null && pseudoSharedCode != null)
// If "index:" is defined, then "analyzerCode:" should not be generated
// in the front end. See comment in messages.yaml
'pseudoSharedCode: ${_encodePseudoSharedCode(pseudoSharedCode!)}',
if (severity != null) 'severity: CfeSeverity.$severity',
];
if (parameters.isEmpty) {
codeArguments.add('problemMessage: r"""$problemMessage"""');
if (correctionMessage != null) {
codeArguments.add('correctionMessage: r"""$correctionMessage"""');
}
return """
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const MessageCode code$name =
const MessageCode(\"$name\", ${codeArguments.join(', ')},);
""";
}
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 interpolatedProblemMessage = interpolate(problemMessage)!;
String? interpolatedCorrectionMessage = interpolate(correctionMessage);
if (hasLabeler) {
interpolatedProblemMessage += " + labeler.originMessages";
}
List<String> messageArguments = <String>[
"problemMessage: $interpolatedProblemMessage",
if (interpolatedCorrectionMessage case var m?) "correctionMessage: $m",
"arguments: { ${arguments.join(', ')}, }",
];
List<String> positionalParameters = parameters.entries
.map((entry) => '${entry.value.type.cfeName!} ${entry.key}')
.toList();
List<String> namedParameters = parameters.entries
.map((entry) => 'required ${entry.value.type.cfeName!} ${entry.key}')
.toList();
List<String> oldToNewArguments = parameters.keys
.map((name) => '$name: $name')
.toList();
return """
// 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(', ')});
""";
}
String computeInterpolator(ParsedPlaceholder placeholder) {
var name = placeholder.name;
var parameter = parameters[name];
if (parameter == null) {
throw StateError(
'Placeholder ${json.encode(name)} not declared as a parameter',
);
}
var conversion =
placeholder.conversionOverride ?? parameter.type.cfeConversion;
if (conversion is LabelerConversion && !hasLabeler) {
withArgumentsStatements.add("TypeLabeler labeler = new TypeLabeler();");
hasLabeler = true;
}
if (conversion?.toCode(
name: placeholder.name,
type: parameters[placeholder.name]!.type,
)
case var conversion?) {
var interpolator = _newName(
usedNames: usedNames,
nameHint: placeholder.name,
);
withArgumentsStatements.add("var $interpolator = $conversion;");
return interpolator;
} else {
return placeholder.name;
}
}
String? interpolate(String? text) {
if (text == null) return null;
text = text.replaceAll(r"$", r"\$").replaceAllMapped(placeholderPattern, (
Match m,
) {
var placeholder = ParsedPlaceholder.fromMatch(m);
var interpolator = interpolators[placeholder] ??= computeInterpolator(
placeholder,
);
return "\${$interpolator}";
});
return "\"\"\"$text\"\"\"";
}
/// Creates the list literal that should populate the error code's
/// `pseudoSharedCode` value.
String _encodePseudoSharedCode(String code) {
var camelCaseCode = code.toCamelCase();
pseudoSharedCodeValues!.add(camelCaseCode);
return 'PseudoSharedCode.$camelCaseCode';
}
}