blob: 53ab1330f93aa68e6d27a7d6045d3b83e702bb71 [file] [log] [blame]
// Copyright (c) 2025, 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:analyzer/src/fasta/error_converter.dart';
library;
import 'dart:convert';
import 'dart:io';
import 'package:analyzer_testing/package_root.dart' as pkg_root;
import 'package:analyzer_utilities/analyzer_messages.dart';
import 'package:analyzer_utilities/extensions/string.dart';
import 'package:analyzer_utilities/tools.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart';
import 'package:yaml/yaml.dart' show loadYaml, YamlMap;
const Map<String, String> severityEnumNames = <String, String>{
'CONTEXT': 'context',
'ERROR': 'error',
'IGNORED': 'ignored',
'INTERNAL_PROBLEM': 'internalProblem',
'WARNING': 'warning',
'INFO': 'info',
};
/// Decoded messages from the `_fe_analyzer_shared` package's `messages.yaml`
/// file.
final Map<String, SharedErrorCodeInfo> feAnalyzerSharedMessages =
_loadCfeStyleMessages(
feAnalyzerSharedPkgPath,
decodeMessage: SharedErrorCodeInfo.fromYaml,
);
/// The path to the `fe_analyzer_shared` package.
final String feAnalyzerSharedPkgPath = normalize(
join(pkg_root.packageRoot, '_fe_analyzer_shared'),
);
/// Decoded messages from the `messages.yaml` files in the front end and
/// `_fe_analyzer_shared`.
final Map<String, CfeStyleErrorCodeInfo> frontEndAndSharedMessages = Map.from(
frontEndMessages,
)..addAll(feAnalyzerSharedMessages);
/// Decoded messages from the front end's `messages.yaml` file.
final Map<String, FrontEndErrorCodeInfo> frontEndMessages =
_loadCfeStyleMessages(
frontEndPkgPath,
decodeMessage: FrontEndErrorCodeInfo.fromYaml,
);
/// The path to the `front_end` package.
final String frontEndPkgPath = normalize(
join(pkg_root.packageRoot, 'front_end'),
);
/// Pattern formerly used by the analyzer to identify placeholders in error
/// message strings.
///
/// (This pattern is still used internally by the analyzer implementation, but
/// it is no longer supported in `messages.yaml`.)
final RegExp oldPlaceholderPattern = RegExp(r'\{\d+\}');
/// Pattern for placeholders in error message strings.
final RegExp placeholderPattern = RegExp(
'#([-a-zA-Z0-9_]+)(?:%([0-9]*).([0-9]+))?',
);
/// A set of tables mapping between shared and analyzer error codes.
final SharedToAnalyzerErrorCodeTables sharedToAnalyzerErrorCodeTables =
SharedToAnalyzerErrorCodeTables._(feAnalyzerSharedMessages);
/// Converts a template to an analyzer internal template string (which uses
/// placeholders like `{0}`).
String convertTemplate(List<TemplatePart> template) {
return template
.map(
(part) => switch (part) {
TemplateLiteralPart(:var text) => text,
TemplateParameterPart(:var parameter) => '{${parameter.index}}',
},
)
.join();
}
/// Decodes a YAML object (in CFE style `messages.yaml` format) into a map from
/// error name to [ErrorCodeInfo].
Map<String, T> decodeCfeStyleMessagesYaml<T extends CfeStyleErrorCodeInfo>(
Object? yaml, {
required T Function(YamlMap) decodeMessage,
required String path,
}) {
Never problem(String message) {
throw 'Problem in $path: $message';
}
var result = <String, T>{};
if (yaml is! Map<Object?, Object?>) {
problem('root node is not a map');
}
for (var entry in yaml.entries) {
var errorName = entry.key;
if (errorName is! String) {
problem('non-string error key ${json.encode(errorName)}');
}
var errorValue = entry.value;
if (errorValue is! YamlMap) {
problem('value associated with error $errorName is not a map');
}
try {
result[errorName] = decodeMessage(errorValue);
} catch (e, st) {
Error.throwWithStackTrace(
'while processing $errorName from $path, $e',
st,
);
}
}
return result;
}
/// Loads messages in CFE style `messages.yaml` format.
Map<String, T> _loadCfeStyleMessages<T extends CfeStyleErrorCodeInfo>(
String packagePath, {
required T Function(YamlMap) decodeMessage,
}) {
var path = join(packagePath, 'messages.yaml');
Object? messagesYaml = loadYaml(
File(path).readAsStringSync(),
sourceUrl: Uri.file(path),
);
return decodeCfeStyleMessagesYaml(
messagesYaml,
decodeMessage: decodeMessage,
path: path,
);
}
/// Splits [text] on spaces using the given [maxWidth] (and [firstLineWidth] if
/// given).
List<String> _splitText(
String text, {
required int maxWidth,
int? firstLineWidth,
}) {
firstLineWidth ??= maxWidth;
var lines = <String>[];
// The character width to use as a maximum width. This starts as
// [firstLineWidth] but becomes [maxWidth] on every iteration after the first.
var width = firstLineWidth;
var lineMaxEndIndex = width;
var lineStartIndex = 0;
while (true) {
if (lineMaxEndIndex >= text.length) {
lines.add(text.substring(lineStartIndex, text.length));
break;
} else {
var lastSpaceIndex = text.lastIndexOf(' ', lineMaxEndIndex);
if (lastSpaceIndex == -1 || lastSpaceIndex <= lineStartIndex) {
// No space between [lineStartIndex] and [lineMaxEndIndex]. Get the
// _next_ space.
lastSpaceIndex = text.indexOf(' ', lineMaxEndIndex);
if (lastSpaceIndex == -1) {
// No space at all after [lineStartIndex].
lines.add(text.substring(lineStartIndex));
break;
}
}
lines.add(text.substring(lineStartIndex, lastSpaceIndex + 1));
lineStartIndex = lastSpaceIndex + 1;
width = maxWidth;
}
lineMaxEndIndex = lineStartIndex + maxWidth;
}
return lines;
}
/// An analyzer error code, consisting of an optional class name and an error
/// name.
///
/// This class implements [operator==] and [hashCode] so it can be used as a map
/// key or in a set.
///
/// This class implements [Comparable], so lists of it can be safely
/// [List.sort]ed.
class AnalyzerCode implements Comparable<AnalyzerCode> {
/// The class name.
final ErrorClassInfo errorClass;
/// The error name.
///
/// The error name is in "snake case", meaning it consists of words separated
/// by underscores. Those words might be lower case or upper case.
///
// TODO(paulberry): change `messages.yaml` to consistently use lower snake
// case.
final String snakeCaseErrorName;
AnalyzerCode({required this.errorClass, required this.snakeCaseErrorName});
/// The string that should be generated into analyzer source code to refer to
/// this diagnostic code.
String get analyzerCodeReference =>
[errorClass.name, camelCaseErrorName].join('.');
/// The error name, converted to camel case.
String get camelCaseErrorName => snakeCaseErrorName.toCamelCase();
@override
int get hashCode => Object.hash(errorClass, snakeCaseErrorName);
@override
bool operator ==(Object other) =>
other is AnalyzerCode &&
errorClass == other.errorClass &&
snakeCaseErrorName == other.snakeCaseErrorName;
@override
int compareTo(AnalyzerCode other) {
// Compare the error classes by name. This works because we know that the
// error classes are unique (this is verified by the `ErrorClassInfo.byName`
// method).
var className = errorClass.name;
var otherClassName = other.errorClass.name;
if (className.compareTo(otherClassName) case var result when result != 0) {
return result;
}
return snakeCaseErrorName.compareTo(other.snakeCaseErrorName);
}
@override
String toString() => [errorClass.name, snakeCaseErrorName].join('.');
}
/// In-memory representation of error code information obtained from a
/// `messages.yaml` file in `pkg/front_end` or `pkg/_fe_analyzer_shared`.
abstract class CfeStyleErrorCodeInfo extends ErrorCodeInfo {
/// The name of the [CfeSeverity] constant describing this error code's CFE
/// severity.
final String? cfeSeverity;
CfeStyleErrorCodeInfo.fromYaml(YamlMap yaml)
: cfeSeverity = _decodeSeverity(yaml['severity']),
super.fromYaml(yaml) {
if (yaml['problemMessage'] == null) {
throw 'Missing problemMessage';
}
}
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}';
}
}
}
/// 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 class derived from `ErrorCode`.
class ErrorClassInfo {
static final Map<String, ErrorClassInfo> _errorClassesByName = () {
var result = <String, ErrorClassInfo>{};
for (var info in errorClasses) {
if (result.containsKey(info.name)) {
throw 'Duplicate error class name: ${json.encode(info.name)}';
}
result[info.name] = info;
}
return result;
}();
static String get _allErrorClassNames =>
(_errorClassesByName.keys.toList()..sort()).map(json.encode).join(', ');
/// The name of this class.
final String name;
const ErrorClassInfo({required this.name});
static ErrorClassInfo byName(String name) =>
_errorClassesByName[name] ??
(throw 'No error class named ${json.encode(name)}. Possible names: '
'$_allErrorClassNames');
}
/// In-memory representation of error code information obtained from either the
/// analyzer or the front end's `messages.yaml` file. This class contains the
/// common functionality supported by both formats.
abstract class ErrorCodeInfo {
/// If present, a documentation comment that should be associated with the
/// error in code generated output.
final String? comment;
/// If the error code has an associated correctionMessage, the template for
/// it.
final List<TemplatePart>? correctionMessage;
/// If non-null, the deprecation message for this error code.
final String? deprecatedMessage;
/// If present, user-facing documentation for the error.
final String? documentation;
/// Whether diagnostics with this code have documentation for them that has
/// been published.
///
/// `null` if the YAML doesn't contain this information.
final bool? hasPublishedDocs;
/// Indicates whether this error is caused by an unresolved identifier.
final bool isUnresolvedIdentifier;
/// The problemMessage for the error code.
final List<TemplatePart> problemMessage;
/// If present, the SDK version this error code stopped being reported in.
/// If not null, error codes will not be generated for this error.
final String? removedIn;
/// If present, indicates that this error code has a special name for
/// presentation to the user, that is potentially shared with other error
/// codes.
final String? sharedName;
/// If present, indicates that this error code has been renamed from
/// [previousName] to its current name (or [sharedName]).
final String? previousName;
/// Map describing the parameters for this error code, obtained from the
/// `parameters` entry in the yaml file.
///
/// Map keys are parameter names. Map values are [ErrorCodeParameter] objects.
final Map<String, ErrorCodeParameter> parameters;
/// The raw YAML node that this `ErrorCodeInfo` was parsed from, or `null` if
/// this `ErrorCodeInfo` was created without reference to a raw YAML node.
///
/// This exists to make it easier for automated scripts to edit the YAML
/// source.
final YamlMap? yamlNode;
ErrorCodeInfo({
this.comment,
this.documentation,
this.hasPublishedDocs,
this.isUnresolvedIdentifier = false,
this.sharedName,
required Object? problemMessageYaml,
required Object? correctionMessageYaml,
this.deprecatedMessage,
this.previousName,
this.removedIn,
required this.parameters,
this.yamlNode,
}) : problemMessage =
_decodeMessage(
problemMessageYaml,
parameters: parameters,
kind: 'problemMessage',
) ??
[],
correctionMessage = _decodeMessage(
correctionMessageYaml,
parameters: parameters,
kind: 'correctionMessage',
);
/// Decodes an [ErrorCodeInfo] object from its YAML representation.
ErrorCodeInfo.fromYaml(YamlMap yaml)
: this(
comment: yaml['comment'] as String?,
correctionMessageYaml: yaml['correctionMessage'],
deprecatedMessage: yaml['deprecatedMessage'] as String?,
documentation: yaml['documentation'] as String?,
hasPublishedDocs: yaml['hasPublishedDocs'] as bool?,
isUnresolvedIdentifier:
yaml['isUnresolvedIdentifier'] as bool? ?? false,
problemMessageYaml: yaml['problemMessage'],
sharedName: yaml['sharedName'] as String?,
removedIn: yaml['removedIn'] as String?,
previousName: yaml['previousName'] as String?,
parameters: _decodeParameters(yaml['parameters']),
yamlNode: yaml,
);
/// If this error is no longer reported and
/// its error codes should no longer be generated.
bool get isRemoved => removedIn != null;
void outputConstantHeader(StringSink out) {
out.write(toAnalyzerComments(indent: ' '));
if (deprecatedMessage != null) {
out.writeln(' @Deprecated("$deprecatedMessage")');
}
}
/// Generates a dart declaration for this error code, suitable for inclusion
/// in the error class [className].
///
/// [diagnosticCode] is the name of the error code to be generated.
void toAnalyzerCode(
GeneratedErrorClassInfo errorClassInfo,
String diagnosticCode, {
String? sharedNameReference,
required MemberAccumulator memberAccumulator,
}) {
var correctionMessage = this.correctionMessage;
var parameters = this.parameters;
var usesParameters = [problemMessage, correctionMessage].any(
(value) =>
value != null && value.any((part) => part is TemplateParameterPart),
);
var constantName = diagnosticCode.toCamelCase();
String className;
String templateParameters = '';
String? withArgumentsName;
if (parameters.isNotEmpty && !usesParameters) {
throw StateError(
'Error code declares parameters using a `parameters` entry, but '
"doesn't use them",
);
} else if (parameters.values.any((p) => !p.type.isSupportedByAnalyzer)) {
// Do not generate literate API yet.
className = errorClassInfo.name;
} else if (parameters.isNotEmpty) {
// Parameters are present so generate a diagnostic template (with
// `.withArguments` support).
className = errorClassInfo.templateName;
var withArgumentsParams = parameters.entries
.map((p) => 'required ${p.value.type.analyzerName} ${p.key}')
.join(', ');
var argumentNames = parameters.keys.join(', ');
var pascalCaseName = diagnosticCode.toPascalCase();
withArgumentsName = '_withArguments$pascalCaseName';
templateParameters =
'<LocatableDiagnostic Function({$withArgumentsParams})>';
var newIfNeeded = errorClassInfo.file.shouldUseExplicitNewOrConst
? 'new '
: '';
memberAccumulator.staticMethods[withArgumentsName] =
'''
static LocatableDiagnostic $withArgumentsName({$withArgumentsParams}) {
return ${newIfNeeded}LocatableDiagnosticImpl(
${errorClassInfo.name}.$constantName, [$argumentNames]);
}''';
} else {
// Parameters are not present so generate a "withoutArguments" constant.
className = errorClassInfo.withoutArgumentsName;
}
var constant = StringBuffer();
outputConstantHeader(constant);
constant.writeln(
' static const $className$templateParameters $constantName =',
);
if (errorClassInfo.file.shouldUseExplicitNewOrConst) {
constant.writeln('const ');
}
constant.writeln('$className(');
constant.writeln(
'${sharedNameReference ?? "'${sharedName ?? diagnosticCode}'"},',
);
var maxWidth = 80 - 8 /* indentation */ - 2 /* quotes */ - 1 /* comma */;
var messageAsCode = convertTemplate(problemMessage);
var messageLines = _splitText(
messageAsCode,
maxWidth: maxWidth,
firstLineWidth: maxWidth + 4,
);
constant.writeln('${messageLines.map(_encodeString).join('\n')},');
if (correctionMessage != null) {
constant.write('correctionMessage: ');
var code = convertTemplate(correctionMessage);
var codeLines = _splitText(code, maxWidth: maxWidth);
constant.writeln('${codeLines.map(_encodeString).join('\n')},');
}
if (hasPublishedDocs ?? false) {
constant.writeln('hasPublishedDocs:true,');
}
if (isUnresolvedIdentifier) {
constant.writeln('isUnresolvedIdentifier:true,');
}
if (sharedName != null) {
constant.writeln("uniqueName: '$diagnosticCode',");
}
if (withArgumentsName != null) {
constant.writeln('withArguments: $withArgumentsName,');
}
constant.writeln('expectedTypes: ${_computeExpectedTypes()},');
constant.writeln(');');
memberAccumulator.constants[constantName] = constant.toString();
if (errorClassInfo.deprecatedSnakeCaseNames.contains(diagnosticCode)) {
memberAccumulator.constants[diagnosticCode] =
'''
@Deprecated("Please use $constantName")
static const ${errorClassInfo.name} $diagnosticCode = $constantName;
''';
}
}
/// Generates doc comments for this error code.
String toAnalyzerComments({String indent = ''}) {
// Start with the comment specified in `messages.yaml`.
var out = StringBuffer();
List<String> commentLines = switch (comment) {
null || '' => [],
var c => c.split('\n'),
};
// Add a `Parameters:` section to the bottom of the comment if appropriate.
switch (parameters) {
case Map(isEmpty: true):
if (commentLines.isNotEmpty) commentLines.add('');
commentLines.add('No parameters.');
default:
if (commentLines.isNotEmpty) commentLines.add('');
commentLines.add('Parameters:');
for (var MapEntry(key: name, value: p) in parameters.entries) {
var prefix = '${p.type.messagesYamlName} $name: ';
var extraIndent = ' ' * prefix.length;
var firstLineWidth = 80 - 4 - indent.length;
var lines = _splitText(
'$prefix${p.comment}',
maxWidth: firstLineWidth - prefix.length,
firstLineWidth: firstLineWidth,
);
commentLines.add(lines[0]);
for (var line in lines.skip(1)) {
commentLines.add('$extraIndent$line');
}
}
}
// Indent the result and prefix with `///`.
for (var line in commentLines) {
out.writeln('$indent///${line.isEmpty ? '' : ' '}$line');
}
return out.toString();
}
String _computeExpectedTypes() {
var expectedTypes = [
for (var parameter in parameters.values)
'ExpectedType.${parameter.type.name}',
];
return '[${expectedTypes.join(', ')}]';
}
String _encodeString(String s) {
// JSON encoding gives us mostly what we need.
var jsonEncoded = json.encode(s);
// But we also need to escape `$`.
return jsonEncoded.replaceAll(r'$', r'\$');
}
static List<TemplatePart>? _decodeMessage(
Object? rawMessage, {
required Map<String, ErrorCodeParameter> parameters,
required String kind,
}) {
switch (rawMessage) {
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.
var text = rawMessage.trimRight();
if (text.contains(oldPlaceholderPattern)) {
throw StateError(
'$kind is ${json.encode(text)}, which contains an old-style '
'analyzer placeholder pattern. Please convert to #NAME format.',
);
}
var template = <TemplatePart>[];
var i = 0;
for (var match in placeholderPattern.allMatches(text)) {
var matchStart = match.start;
if (matchStart > i) {
template.add(TemplateLiteralPart(text.substring(i, matchStart)));
}
template.add(
TemplateParameterPart.fromMatch(match, parameters: parameters),
);
i = match.end;
}
if (text.length > i) {
template.add(TemplateLiteralPart(text.substring(i)));
}
return template;
default:
throw 'Bad message type: ${rawMessage.runtimeType}';
}
}
static Map<String, ErrorCodeParameter> _decodeParameters(Object? yaml) {
if (yaml == null) {
throw StateError('Missing parameters section');
}
if (yaml == 'none') return const {};
yaml as Map<Object?, Object?>;
var result = <String, ErrorCodeParameter>{};
var index = 0;
for (var MapEntry(:key, :value) in yaml.entries) {
switch ((key as String).split(' ')) {
case [var type, var name]:
if (result.containsKey(name)) {
throw StateError('Duplicate parameter name: $name');
}
result[name] = ErrorCodeParameter(
name: name,
type: ErrorCodeParameterType.fromMessagesYamlName(type),
comment: value as String,
index: index++,
);
default:
throw StateError(
'Malformed parameter key (should be `TYPE NAME`): '
'${json.encode(key)}',
);
}
}
return result;
}
}
/// In-memory representation of a single key/value pair from the `parameters`
/// map for an error code.
class ErrorCodeParameter {
final String name;
final ErrorCodeParameterType type;
final String comment;
final int index;
ErrorCodeParameter({
required this.name,
required this.type,
required this.comment,
required this.index,
});
}
/// 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', 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',
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.
///
/// Used for decoding parameter types from `messages.yaml`.
static final _messagesYamlNameToValue = {
for (var value in values) value.messagesYamlName: value,
};
/// Name of this type as it appears in `messages.yaml`.
final String messagesYamlName;
/// 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,
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 file
/// `pkg/front_end/messages.yaml`.
class FrontEndErrorCodeInfo extends CfeStyleErrorCodeInfo {
/// The value of the `pseudoSharedCode` property in
/// `pkg/front_end/messages.yaml`.
///
/// Messages with this property set are not shared; they have separately
/// declared analyzer and CFE codes. However, they are reported by code
/// defined in `pkg/_fe_analyzer_shared` using the CFE error reporting
/// mechanism. When running under the analyzer, they are then translated
/// into the associated analyzer error using [FastaErrorReporter].
// TODO(paulberry): migrate all pseudo-shared error codes to shared error
// codes.
final String? pseudoSharedCode;
FrontEndErrorCodeInfo.fromYaml(YamlMap yaml)
: pseudoSharedCode = yaml['pseudoSharedCode'] as String?,
super.fromYaml(yaml) {
if (yaml['analyzerCode'] != null) {
throw StateError('Only shared messages can have an analyzer code');
}
}
}
/// Information about a code generated class derived from `ErrorCode`.
class GeneratedErrorClassInfo extends ErrorClassInfo {
/// The generated file containing this class.
final GeneratedErrorCodeFile file;
/// The severity of errors in this class, or `null` if the severity should be
/// based on the [type] of the error.
final String? severity;
/// The type of errors in this class.
final String type;
/// The names of any errors which are relied upon by analyzer clients, and
/// therefore will need their "snake case" form preserved (with a deprecation
/// notice) after migration to camel case error codes.
final Set<String> deprecatedSnakeCaseNames;
/// If `true` (the default), error codes of this class will be included in the
/// automatically-generated `diagnosticCodeValues` list.
final bool includeInDiagnosticCodeValues;
/// Documentation comment to generate for the error class.
///
/// If no documentation comment is needed, this should be the empty string.
final String comment;
const GeneratedErrorClassInfo({
required this.file,
required super.name,
this.severity,
required this.type,
this.deprecatedSnakeCaseNames = const {},
this.includeInDiagnosticCodeValues = true,
this.comment = '',
});
/// Generates the code to compute the severity of errors of this class.
String get severityCode {
var severity = this.severity;
if (severity == null) {
return '$typeCode.severity';
} else {
return 'DiagnosticSeverity.$severity';
}
}
String get templateName => '${_baseName}Template';
/// Generates the code to compute the type of errors of this class.
String get typeCode => 'DiagnosticType.$type';
String get withoutArgumentsName => '${_baseName}WithoutArguments';
String get _baseName {
const suffix = 'Code';
if (name.endsWith(suffix)) {
return name.substring(0, name.length - suffix.length);
} else {
throw StateError("Can't infer base name for class $name");
}
}
}
/// Representation of a single file containing generated error codes.
class GeneratedErrorCodeFile {
/// The file path (relative to the SDK's `pkg` directory) of the generated
/// file.
final String path;
/// The URI of the library that the generated file will be a part of.
final String parentLibrary;
/// Whether the generated file should use the `new` and `const` keywords when
/// generating constructor invocations.
final bool shouldUseExplicitNewOrConst;
final bool shouldIgnorePreferSingleQuotes;
const GeneratedErrorCodeFile({
required this.path,
required this.parentLibrary,
this.shouldUseExplicitNewOrConst = false,
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;
}
}
}
/// In-memory representation of error code information obtained from the file
/// `pkg/_fe_analyzer_shared/messages.yaml`.
class SharedErrorCodeInfo extends CfeStyleErrorCodeInfo {
/// The analyzer error code that corresponds to this shared error code.
///
/// Shared error codes are required to have exactly one analyzer error code
/// associated with them.
final AnalyzerCode analyzerCode;
SharedErrorCodeInfo.fromYaml(super.yaml)
: analyzerCode = _decodeAnalyzerCode(
(yaml['analyzerCode'] ??
(throw StateError(
'Shared error codes must specify an analyzerCode.',
)))
as String,
),
super.fromYaml();
static AnalyzerCode _decodeAnalyzerCode(String s) {
switch (s.split('.')) {
case [var className, var errorName]
when errorName == errorName.toUpperCase():
return AnalyzerCode(
errorClass: ErrorClassInfo.byName(className),
snakeCaseErrorName: errorName,
);
default:
throw StateError(
'Analyzer codes must take the form ClassName.DIAGNOSTIC_NAME. Found '
'${json.encode(s)} instead.',
);
}
}
}
/// Data tables mapping between shared errors and their corresponding
/// automatically generated analyzer errors.
class SharedToAnalyzerErrorCodeTables {
/// Map whose values are the shared errors for which analyzer errors should be
/// automatically generated, and whose keys are the corresponding analyzer
/// error code.
final Map<AnalyzerCode, SharedErrorCodeInfo> analyzerCodeToInfo = {};
/// List of shared errors for which analyzer errors should be automatically
/// generated, sorted by analyzer code.
final List<SharedErrorCodeInfo> sortedSharedErrors = [];
SharedToAnalyzerErrorCodeTables._(Map<String, SharedErrorCodeInfo> messages) {
var infoToFrontEndCode = <SharedErrorCodeInfo, String>{};
for (var entry in messages.entries) {
var errorCodeInfo = entry.value;
var frontEndCode = entry.key;
sortedSharedErrors.add(errorCodeInfo);
infoToFrontEndCode[errorCodeInfo] = frontEndCode;
var analyzerCode = errorCodeInfo.analyzerCode;
var previousEntryForAnalyzerCode = analyzerCodeToInfo[analyzerCode];
if (previousEntryForAnalyzerCode != null) {
throw 'Analyzer code $analyzerCode used by both '
'${infoToFrontEndCode[previousEntryForAnalyzerCode]} and '
'$frontEndCode';
}
analyzerCodeToInfo[analyzerCode] = errorCodeInfo;
}
sortedSharedErrors.sortBy((e) => e.analyzerCode.camelCaseErrorName);
}
}
/// 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)';
}
/// [TemplatePart] representing a literal string of characters, with no
/// parameter substitutions.
class TemplateLiteralPart implements TemplatePart {
/// The literal text.
final String text;
TemplateLiteralPart(this.text);
}
/// [TemplatePart] representing a parameter to be substituted into the
/// diagnostic message.
class TemplateParameterPart implements TemplatePart {
/// The parameter to be substituted.
final ErrorCodeParameter parameter;
/// The conversion to apply to the parameter.
///
/// If `null`, the default conversion for the parameter's type will be used.
final Conversion? conversionOverride;
/// Builds a [TemplateParameterPart] from the given [match] of
/// [placeholderPattern].
factory TemplateParameterPart.fromMatch(
Match match, {
required Map<String, ErrorCodeParameter> parameters,
}) {
String name = match[1]!;
var parameter = parameters[name];
if (parameter == null) {
throw StateError(
'Placeholder ${json.encode(name)} not declared as a parameter',
);
}
return TemplateParameterPart._(
parameter: parameter,
conversionOverride: NumericConversion.from(match),
);
}
TemplateParameterPart._({
required this.parameter,
required this.conversionOverride,
});
@override
int get hashCode => Object.hash(parameter, conversionOverride);
@override
bool operator ==(Object other) =>
other is TemplateParameterPart &&
other.parameter == parameter &&
other.conversionOverride == conversionOverride;
}
/// A part of a parsed template string.
///
/// Each `problemMessage` and `correctionMessage` template string in a
/// `messages.yaml` file is decoded into a list of [TemplatePart].
sealed class TemplatePart {}