| // 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 {} |