blob: 4a94bf80f920f381410b5bcdd77781b567a9b52b [file] [log] [blame]
// 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.
import 'dart:convert';
/// Decodes a YAML object (obtained from `pkg/front_end/messages.yaml`) into a
/// map from error name to [ErrorCodeInfo].
Map<String, ErrorCodeInfo> decodeCfeMessagesYaml(Map<Object?, Object?> yaml) {
var result = <String, ErrorCodeInfo>{};
for (var entry in yaml.entries) {
result[entry.key as String] =
ErrorCodeInfo.fromYaml(entry.value as Map<Object?, Object?>);
}
return result;
}
/// Data tables mapping between CFE errors and their corresponding automatically
/// generated analyzer errors.
class CfeToAnalyzerErrorCodeTables {
/// List of CFE errors for which analyzer errors should be automatically
/// generated, organized by their `index` property.
final List<ErrorCodeInfo?> indexToInfo = [];
/// Map whose values are the CFE errors for which analyzer errors should be
/// automatically generated, and whose keys are the corresponding analyzer
/// error name. (Names are simple identifiers; they are not prefixed by the
/// class name `ParserErrorCode`)
final Map<String, ErrorCodeInfo> analyzerCodeToInfo = {};
/// Map whose values are the CFE errors for which analyzer errors should be
/// automatically generated, and whose keys are the front end error name.
final Map<String, ErrorCodeInfo> frontEndCodeToInfo = {};
/// Map whose keys are the CFE errors for which analyzer errors should be
/// automatically generated, and whose values are the corresponding analyzer
/// error name. (Names are simple identifiers; they are not prefixed by the
/// class name `ParserErrorCode`)
final Map<ErrorCodeInfo, String> infoToAnalyzerCode = {};
/// Map whose keys are the CFE errors for which analyzer errors should be
/// automatically generated, and whose values are the front end error name.
final Map<ErrorCodeInfo, String> infoToFrontEndCode = {};
CfeToAnalyzerErrorCodeTables(Map<String, ErrorCodeInfo> messages) {
for (var entry in messages.entries) {
var errorCodeInfo = entry.value;
var index = errorCodeInfo.index;
if (index == null || errorCodeInfo.analyzerCode.length != 1) {
continue;
}
var frontEndCode = entry.key;
if (index < 1) {
throw '''
$frontEndCode specifies index $index but indices must be 1 or greater.
For more information run:
pkg/front_end/tool/fasta generate-messages
''';
}
if (indexToInfo.length <= index) {
indexToInfo.length = index + 1;
}
var previousEntryForIndex = indexToInfo[index];
if (previousEntryForIndex != null) {
throw 'Index $index used by both '
'${infoToFrontEndCode[previousEntryForIndex]} and $frontEndCode';
}
indexToInfo[index] = errorCodeInfo;
frontEndCodeToInfo[frontEndCode] = errorCodeInfo;
infoToFrontEndCode[errorCodeInfo] = frontEndCode;
var analyzerCodeLong = errorCodeInfo.analyzerCode.single;
var expectedPrefix = 'ParserErrorCode.';
if (!analyzerCodeLong.startsWith(expectedPrefix)) {
throw 'Expected all analyzer error codes to be prefixed with '
'${json.encode(expectedPrefix)}. Found '
'${json.encode(analyzerCodeLong)}.';
}
var analyzerCode = analyzerCodeLong.substring(expectedPrefix.length);
infoToAnalyzerCode[errorCodeInfo] = analyzerCode;
var previousEntryForAnalyzerCode = analyzerCodeToInfo[analyzerCode];
if (previousEntryForAnalyzerCode != null) {
throw 'Analyzer code $analyzerCode used by both '
'${infoToFrontEndCode[previousEntryForAnalyzerCode]} and '
'$frontEndCode';
}
analyzerCodeToInfo[analyzerCode] = errorCodeInfo;
}
for (int i = 1; i < indexToInfo.length; i++) {
if (indexToInfo[i] == null) {
throw 'Indices are not consecutive; no error code has index $i.';
}
}
}
}
/// In-memory representation of error code information obtained from either a
/// `messages.yaml` file. Supports both the analyzer and front_end message file
/// formats.
class ErrorCodeInfo {
/// Pattern used by the front end to identify placeholders in error message
/// strings. TODO(paulberry): share this regexp (and the code for interpreting
/// it) between the CFE and analyzer.
static final RegExp _placeholderPattern =
RegExp("#\([-a-zA-Z0-9_]+\)(?:%\([0-9]*\)\.\([0-9]+\))?");
/// For error code information obtained from the CFE, the set of analyzer
/// error codes that corresponds to this error code, if any.
final List<String> analyzerCode;
/// If present, a documentation comment that should be associated with the
/// error in code generated output.
final String? comment;
/// `true` if this error should be copied from an error in the CFE. The
/// purpose of this field is so that the documentation for the error can exist
/// in the analyzer's messages.yaml file but the error text can come from the
/// CFE's messages.yaml file. TODO(paulberry): add support for documentation
/// to the CFE's messages.yaml file so that this isn't necessary.
final bool copyFromCfe;
/// If present, user-facing documentation for the error.
final String? documentation;
/// `true` if diagnostics with this code have documentation for them that has
/// been published.
final bool hasPublishedDocs;
/// For error code information obtained from the CFE, the index of the error
/// in the analyzer's `fastaAnalyzerErrorCodes` table.
final int? index;
/// Indicates whether this error is caused by an unresolved identifier.
final bool isUnresolvedIdentifier;
/// 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;
/// The template for the error message, or `null` if [copyFromCfe] is `true`.
final String? template;
/// If the error code has an associated tip/correction message, the template
/// for it.
final String? tip;
ErrorCodeInfo(
{this.analyzerCode = const [],
this.comment,
this.copyFromCfe = false,
this.documentation,
this.hasPublishedDocs = false,
this.index,
this.isUnresolvedIdentifier = false,
this.sharedName,
this.template,
this.tip}) {
if (copyFromCfe) {
if (template != null) {
throw "Error codes marked `copyFromCfe: true` can't have a template.";
}
} else {
if (template == null) {
throw 'Error codes must have a template unless they are marked '
'`copyFromCfe: true`.';
}
}
}
/// Decodes an [ErrorCodeInfo] object from its YAML representation.
ErrorCodeInfo.fromYaml(Map<Object?, Object?> yaml)
: this(
analyzerCode: _decodeAnalyzerCode(yaml['analyzerCode']),
comment: yaml['comment'] as String?,
copyFromCfe: yaml['copyFromCfe'] as bool? ?? false,
documentation: yaml['documentation'] as String?,
hasPublishedDocs: yaml['hasPublishedDocs'] as bool? ?? false,
index: yaml['index'] as int?,
isUnresolvedIdentifier:
yaml['isUnresolvedIdentifier'] as bool? ?? false,
sharedName: yaml['sharedName'] as String?,
template: yaml['template'] as String?,
tip: yaml['tip'] as String?);
/// Generates a dart declaration for this error code, suitable for inclusion
/// in the error class [className]. [errorCode] is the name of the error code
/// to be generated.
String toAnalyzerCode(String className, String errorCode) {
var out = StringBuffer();
out.writeln('$className(');
out.writeln("'$errorCode',");
final placeholderToIndexMap = _computePlaceholderToIndexMap();
out.writeln(
json.encode(_convertTemplate(placeholderToIndexMap, template!)));
final tip = this.tip;
if (tip is String) {
out.write(',correction: ');
out.writeln(json.encode(_convertTemplate(placeholderToIndexMap, tip)));
}
if (hasPublishedDocs) {
out.writeln(',hasPublishedDocs:true');
}
out.write(');');
return out.toString();
}
/// Encodes this object into a YAML representation.
Map<Object?, Object?> toYaml() => {
if (copyFromCfe) 'copyFromCfe': true,
if (sharedName != null) 'sharedName': sharedName,
if (analyzerCode.isNotEmpty)
'analyzerCode': _encodeAnalyzerCode(analyzerCode),
if (template != null) 'template': template,
if (tip != null) 'tip': tip,
if (isUnresolvedIdentifier) 'isUnresolvedIdentifier': true,
if (hasPublishedDocs) 'hasPublishedDocs': true,
if (comment != null) 'comment': comment,
if (documentation != null) 'documentation': documentation,
};
/// Given a messages.yaml entry, come up with a mapping from placeholder
/// patterns in its message and tip strings to their corresponding indices.
Map<String, int> _computePlaceholderToIndexMap() {
var mapping = <String, int>{};
for (var value in [template, tip]) {
if (value is! String) continue;
for (Match match in _placeholderPattern.allMatches(value)) {
// CFE supports a bunch of formatting options that we don't; make sure
// none of those are used.
if (match.group(0) != '#${match.group(1)}') {
throw 'Template string ${json.encode(value)} contains unsupported '
'placeholder pattern ${json.encode(match.group(0))}';
}
mapping[match.group(0)!] ??= mapping.length;
}
}
return mapping;
}
/// Convert a CFE template string (which uses placeholders like `#string`) to
/// an analyzer template string (which uses placeholders like `{0}`).
static String _convertTemplate(
Map<String, int> placeholderToIndexMap, String entry) {
return entry.replaceAllMapped(_placeholderPattern,
(match) => '{${placeholderToIndexMap[match.group(0)!]}}');
}
static List<String> _decodeAnalyzerCode(Object? value) {
if (value == null) {
return const [];
} else if (value is String) {
return [value];
} else if (value is List) {
return [for (var s in value) s as String];
} else {
throw 'Unrecognized analyzer code: $value';
}
}
static Object _encodeAnalyzerCode(List<String> analyzerCode) {
if (analyzerCode.length == 1) {
return analyzerCode.single;
} else {
return analyzerCode;
}
}
}