Files
sdk/pkg/analyzer/tool/messages/error_code_info.dart
T
Parker Lougheed a409a1c07d [analyzer/linter] Use shared enumeration for lint names
This helps to avoid any accidental mispellings, enables find usages, and potentially makes future renames easier.

The primary goal is to make future work in https://github.com/dart-lang/sdk/issues/56835 easier.

Change-Id: I684630a4d6cb145031de0dabc221f247246ea00c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/388042
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Auto-Submit: Parker Lougheed <parlough@gmail.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
2024-10-03 21:36:49 +00:00

635 lines
23 KiB
Dart

// 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_utilities/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 `ErrorCode` 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: 'AnalysisOptionsHintCode',
type: 'HINT',
severity: 'INFO'),
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',
superclass: 'AnalyzerErrorCode',
type: 'COMPILE_TIME_ERROR',
extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
ErrorClassInfo(
filePath: 'lib/src/error/codes.g.dart',
name: 'StaticWarningCode',
superclass: 'AnalyzerErrorCode',
type: 'STATIC_WARNING',
severity: 'WARNING',
extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
ErrorClassInfo(
filePath: 'lib/src/error/codes.g.dart',
name: 'WarningCode',
superclass: 'AnalyzerErrorCode',
type: 'STATIC_WARNING',
severity: 'WARNING',
extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
ErrorClassInfo(
filePath: 'lib/src/dart/error/ffi_code.g.dart',
name: 'FfiCode',
superclass: 'AnalyzerErrorCode',
type: 'COMPILE_TIME_ERROR',
extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
ErrorClassInfo(
filePath: 'lib/src/dart/error/hint_codes.g.dart',
name: 'HintCode',
superclass: 'AnalyzerErrorCode',
type: 'HINT',
extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
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 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');
}
result[errorName] = FrontEndErrorCodeInfo.fromYaml(errorValue);
}
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 = 'ErrorCode',
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 'ErrorSeverity.$severity';
}
}
/// Generates the code to compute the type of errors of this class.
String get typeCode => 'ErrorType.$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;
/// `true` if diagnostics with this code have documentation for them that has
/// been published.
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 = false,
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? ?? false,
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]. [errorCode] is the name of the error code
/// to be generated.
String toAnalyzerCode(String className, String errorCode,
{String? sharedNameReference}) {
var out = StringBuffer();
out.writeln('$className(');
out.writeln('${sharedNameReference ?? "'${sharedName ?? errorCode}'"},');
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) {
out.writeln('hasPublishedDocs:true,');
}
if (isUnresolvedIdentifier) {
out.writeln('isUnresolvedIdentifier:true,');
}
if (sharedName != null) {
out.writeln("uniqueName: '$errorCode',");
}
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) '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;
}
}
}