blob: 1940a12e51042945e3067e1294400be09a28854e [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';
import 'dart:io';
import 'package:analyzer_testing/package_root.dart' as pkg_root;
import 'package:path/path.dart';
import 'package:yaml/yaml.dart' show loadYaml;
/// Information about all the classes derived from `DiagnosticCode` that are
/// code-generated based on the contents of the analyzer and front end
/// `messages.yaml` files.
const List<ErrorClassInfo> errorClasses = [
ErrorClassInfo(
filePath: 'lib/src/analysis_options/error/option_codes.g.dart',
name: 'AnalysisOptionsErrorCode',
type: 'COMPILE_TIME_ERROR',
severity: 'ERROR',
),
ErrorClassInfo(
filePath: 'lib/src/analysis_options/error/option_codes.g.dart',
name: 'AnalysisOptionsWarningCode',
type: 'STATIC_WARNING',
severity: 'WARNING',
),
ErrorClassInfo(
filePath: 'lib/src/error/codes.g.dart',
name: 'CompileTimeErrorCode',
type: 'COMPILE_TIME_ERROR',
),
ErrorClassInfo(
filePath: 'lib/src/error/codes.g.dart',
name: 'StaticWarningCode',
type: 'STATIC_WARNING',
severity: 'WARNING',
),
ErrorClassInfo(
filePath: 'lib/src/error/codes.g.dart',
name: 'WarningCode',
type: 'STATIC_WARNING',
severity: 'WARNING',
),
ErrorClassInfo(
filePath: 'lib/src/dart/error/ffi_code.g.dart',
name: 'FfiCode',
type: 'COMPILE_TIME_ERROR',
),
ErrorClassInfo(
filePath: 'lib/src/dart/error/hint_codes.g.dart',
name: 'HintCode',
type: 'HINT',
),
ErrorClassInfo(
filePath: 'lib/src/dart/error/syntactic_errors.g.dart',
name: 'ParserErrorCode',
type: 'SYNTACTIC_ERROR',
severity: 'ERROR',
includeCfeMessages: true,
),
ErrorClassInfo(
filePath: 'lib/src/manifest/manifest_warning_code.g.dart',
name: 'ManifestWarningCode',
type: 'STATIC_WARNING',
severity: 'WARNING',
),
ErrorClassInfo(
filePath: 'lib/src/pubspec/pubspec_warning_code.g.dart',
name: 'PubspecWarningCode',
type: 'STATIC_WARNING',
severity: 'WARNING',
),
];
/// Decoded messages from the analyzer's `messages.yaml` file.
final Map<String, Map<String, AnalyzerErrorCodeInfo>> analyzerMessages =
_loadAnalyzerMessages();
/// The path to the `analyzer` package.
final String analyzerPkgPath = normalize(
join(pkg_root.packageRoot, 'analyzer'),
);
/// A set of tables mapping between front end and analyzer error codes.
final CfeToAnalyzerErrorCodeTables cfeToAnalyzerErrorCodeTables =
CfeToAnalyzerErrorCodeTables._(frontEndMessages);
/// Decoded messages from the front end's `messages.yaml` file.
final Map<String, FrontEndErrorCodeInfo> frontEndMessages =
_loadFrontEndMessages();
/// The path to the `front_end` package.
final String frontEndPkgPath = normalize(
join(pkg_root.packageRoot, 'front_end'),
);
/// The path to the `linter` package.
final String linterPkgPath = normalize(join(pkg_root.packageRoot, 'linter'));
/// Decoded messages from the linter's `messages.yaml` file.
final Map<String, Map<String, AnalyzerErrorCodeInfo>> lintMessages =
_loadLintMessages();
/// 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.
final RegExp _placeholderPattern = RegExp(
'#([-a-zA-Z0-9_]+)(?:%([0-9]*).([0-9]+))?',
);
/// Convert a CFE template string (which uses placeholders like `#string`) to
/// an analyzer template string (which uses placeholders like `{0}`).
String convertTemplate(Map<String, int> placeholderToIndexMap, String entry) {
return entry.replaceAllMapped(
_placeholderPattern,
(match) => '{${placeholderToIndexMap[match.group(0)!]}}',
);
}
/// Decodes a YAML object (obtained from a `messages.yaml` file) into a
/// two-level map of [ErrorCodeInfo], indexed first by class name and then by
/// error name.
Map<String, Map<String, AnalyzerErrorCodeInfo>> decodeAnalyzerMessagesYaml(
String packagePath,
) {
var yaml =
loadYaml(File(join(packagePath, 'messages.yaml')).readAsStringSync())
as Object?;
Never problem(String message) {
throw 'Problem in $packagePath/messages.yaml: $message';
}
var result = <String, Map<String, AnalyzerErrorCodeInfo>>{};
if (yaml is! Map<Object?, Object?>) {
problem('root node is not a map');
}
for (var classEntry in yaml.entries) {
var className = classEntry.key;
if (className is! String) {
problem('non-string class key ${json.encode(className)}');
}
var classValue = classEntry.value;
if (classValue is! Map<Object?, Object?>) {
problem('value associated with class key $className is not a map');
}
for (var errorEntry in classValue.entries) {
var errorName = errorEntry.key;
if (errorName is! String) {
problem(
'in class $className, non-string error key '
'${json.encode(errorName)}',
);
}
var errorValue = errorEntry.value;
if (errorValue is! Map<Object?, Object?>) {
problem(
'value associated with error $className.$errorName is not a '
'map',
);
}
AnalyzerErrorCodeInfo errorCodeInfo;
try {
errorCodeInfo =
(result[className] ??=
{})[errorName] = AnalyzerErrorCodeInfo.fromYaml(errorValue);
} catch (e, st) {
Error.throwWithStackTrace(
'while processing $className.$errorName, $e',
st,
);
}
if (errorCodeInfo.hasPublishedDocs == null) {
problem('Missing hasPublishedDocs for $className.$errorName');
}
if (errorCodeInfo case AliasErrorCodeInfo(:var aliasFor)) {
var aliasForPath = aliasFor.split('.');
if (aliasForPath.isEmpty) {
problem("The 'aliasFor' value at '$className.$errorName is empty");
}
var node = yaml;
for (var key in aliasForPath) {
var value = node[key];
if (value is! Map<Object?, Object?>) {
problem(
'No Map value at "$aliasFor", aliased from '
'$className.$errorName',
);
}
node = value;
}
}
}
}
return result;
}
/// Decodes a YAML object (obtained from `pkg/front_end/messages.yaml`) into a
/// map from error name to [ErrorCodeInfo].
Map<String, FrontEndErrorCodeInfo> decodeCfeMessagesYaml(Object? yaml) {
Never problem(String message) {
throw 'Problem in pkg/front_end/messages.yaml: $message';
}
var result = <String, FrontEndErrorCodeInfo>{};
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! Map<Object?, Object?>) {
problem('value associated with error $errorName is not a map');
}
try {
result[errorName] = FrontEndErrorCodeInfo.fromYaml(errorValue);
} catch (e, st) {
Error.throwWithStackTrace('while processing $errorName, $e', st);
}
}
return result;
}
/// Loads analyzer messages from the analyzer's `messages.yaml` file.
Map<String, Map<String, AnalyzerErrorCodeInfo>> _loadAnalyzerMessages() =>
decodeAnalyzerMessagesYaml(analyzerPkgPath);
/// Loads front end messages from the front end's `messages.yaml` file.
Map<String, FrontEndErrorCodeInfo> _loadFrontEndMessages() {
Object? messagesYaml = loadYaml(
File(join(frontEndPkgPath, 'messages.yaml')).readAsStringSync(),
);
return decodeCfeMessagesYaml(messagesYaml);
}
/// Loads linter messages from the linter's `messages.yaml` file.
Map<String, Map<String, AnalyzerErrorCodeInfo>> _loadLintMessages() =>
decodeAnalyzerMessagesYaml(linterPkgPath);
/// 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 [AnalyzerErrorCodeInfo] which is an alias for another, for incremental
/// deprecation purposes.
class AliasErrorCodeInfo extends AnalyzerErrorCodeInfo {
String aliasFor;
AliasErrorCodeInfo._fromYaml(super.yaml, {required this.aliasFor})
: super._fromYaml();
String get aliasForClass => aliasFor.split('.').first;
String get aliasForFilePath =>
errorClasses
.firstWhere((element) => element.name == aliasForClass)
.filePath;
}
/// In-memory representation of error code information obtained from the
/// analyzer's `messages.yaml` file.
class AnalyzerErrorCodeInfo extends ErrorCodeInfo {
AnalyzerErrorCodeInfo({
super.comment,
super.correctionMessage,
super.deprecatedMessage,
super.documentation,
super.hasPublishedDocs,
super.isUnresolvedIdentifier,
required super.problemMessage,
super.removedIn,
super.sharedName,
});
factory AnalyzerErrorCodeInfo.fromYaml(Map<Object?, Object?> yaml) {
if (yaml['aliasFor'] case var aliasFor?) {
return AliasErrorCodeInfo._fromYaml(yaml, aliasFor: aliasFor as String);
} else {
return AnalyzerErrorCodeInfo._fromYaml(yaml);
}
}
AnalyzerErrorCodeInfo._fromYaml(super.yaml) : super.fromYaml();
}
/// 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, FrontEndErrorCodeInfo> 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.';
}
}
}
}
/// Information about a code generated class derived from `ErrorCode`.
class ErrorClassInfo {
/// A list of additional import URIs that are needed by the code generated
/// for this class.
final List<String> extraImports;
/// The file path (relative to the root of `pkg/analyzer`) of the generated
/// file containing this class.
final String filePath;
/// True if this class should contain error messages extracted from the front
/// end's `messages.yaml` file.
///
/// Note: at the moment we only support extracting front end error messages to
/// a single error class.
final bool includeCfeMessages;
/// The name of this class.
final String name;
/// 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 superclass of this class.
final String superclass;
/// The type of errors in this class.
final String type;
const ErrorClassInfo({
this.extraImports = const [],
required this.filePath,
this.includeCfeMessages = false,
required this.name,
this.severity,
this.superclass = 'DiagnosticCode',
required this.type,
});
/// 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';
}
}
/// Generates the code to compute the type of errors of this class.
String get typeCode => 'DiagnosticType.$type';
}
/// 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 String? 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 String 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;
ErrorCodeInfo({
this.comment,
this.documentation,
this.hasPublishedDocs,
this.isUnresolvedIdentifier = false,
this.sharedName,
required this.problemMessage,
this.correctionMessage,
this.deprecatedMessage,
this.previousName,
this.removedIn,
});
/// Decodes an [ErrorCodeInfo] object from its YAML representation.
ErrorCodeInfo.fromYaml(Map<Object?, Object?> yaml)
: this(
comment: yaml['comment'] as String?,
correctionMessage: yaml['correctionMessage'] as String?,
deprecatedMessage: yaml['deprecatedMessage'] as String?,
documentation: yaml['documentation'] as String?,
hasPublishedDocs: yaml['hasPublishedDocs'] as bool?,
isUnresolvedIdentifier:
yaml['isUnresolvedIdentifier'] as bool? ?? false,
problemMessage: yaml['problemMessage'] as String? ?? '',
sharedName: yaml['sharedName'] as String?,
removedIn: yaml['removedIn'] as String?,
previousName: yaml['previousName'] as String?,
);
/// If this error is no longer reported and
/// its error codes should no longer be generated.
bool get isRemoved => removedIn != null;
/// Given a messages.yaml entry, come up with a mapping from placeholder
/// patterns in its message strings to their corresponding indices.
Map<String, int> computePlaceholderToIndexMap() {
var mapping = <String, int>{};
for (var value in [problemMessage, correctionMessage]) {
if (value is! String) continue;
for (Match match in _placeholderPattern.allMatches(value)) {
// CFE supports a bunch of formatting options that analyzer doesn'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;
}
/// 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.
String toAnalyzerCode(
String className,
String diagnosticCode, {
String? sharedNameReference,
}) {
var out = StringBuffer();
out.writeln('$className(');
out.writeln(
'${sharedNameReference ?? "'${sharedName ?? diagnosticCode}'"},',
);
var maxWidth = 80 - 8 /* indentation */ - 2 /* quotes */ - 1 /* comma */;
var placeholderToIndexMap = computePlaceholderToIndexMap();
var messageAsCode = convertTemplate(placeholderToIndexMap, problemMessage);
var messageLines = _splitText(
messageAsCode,
maxWidth: maxWidth,
firstLineWidth: maxWidth + 4,
);
out.writeln('${messageLines.map(json.encode).join('\n')},');
var correctionMessage = this.correctionMessage;
if (correctionMessage is String) {
out.write('correctionMessage: ');
var code = convertTemplate(placeholderToIndexMap, correctionMessage);
var codeLines = _splitText(code, maxWidth: maxWidth);
out.writeln('${codeLines.map(json.encode).join('\n')},');
}
if (hasPublishedDocs ?? false) {
out.writeln('hasPublishedDocs:true,');
}
if (isUnresolvedIdentifier) {
out.writeln('isUnresolvedIdentifier:true,');
}
if (sharedName != null) {
out.writeln("uniqueName: '$diagnosticCode',");
}
out.write(');');
return out.toString();
}
/// Generates doc comments for this error code.
String toAnalyzerComments({String indent = ''}) {
var out = StringBuffer();
var comment = this.comment;
if (comment != null) {
for (var line in comment.split('\n')) {
out.writeln('$indent/// ${line.isEmpty ? '' : ' '}$line');
}
}
return out.toString();
}
/// Encodes this object into a YAML representation.
Map<Object?, Object?> toYaml() => {
if (removedIn != null) 'removedIn': removedIn,
if (sharedName != null) 'sharedName': sharedName,
'problemMessage': problemMessage,
if (correctionMessage != null) 'correctionMessage': correctionMessage,
if (isUnresolvedIdentifier) 'isUnresolvedIdentifier': true,
if (hasPublishedDocs ?? false) 'hasPublishedDocs': true,
if (comment != null) 'comment': comment,
if (documentation != null) 'documentation': documentation,
};
}
/// In-memory representation of error code information obtained from the front
/// end's `messages.yaml` file.
class FrontEndErrorCodeInfo extends ErrorCodeInfo {
/// The set of analyzer error codes that corresponds to this error code, if
/// any.
final List<String> analyzerCode;
/// The index of the error in the analyzer's `fastaAnalyzerErrorCodes` table.
final int? index;
FrontEndErrorCodeInfo.fromYaml(super.yaml)
: analyzerCode = _decodeAnalyzerCode(yaml['analyzerCode']),
index = yaml['index'] as int?,
super.fromYaml();
@override
Map<Object?, Object?> toYaml() => {
if (analyzerCode.isNotEmpty)
'analyzerCode': _encodeAnalyzerCode(analyzerCode),
if (index != null) 'index': index,
...super.toYaml(),
};
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;
}
}
}