// 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 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> 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 frontEndMessages = _loadFrontEndMessages(); /// The path to the `front_end` package. final String frontEndPkgPath = normalize(join(pkg_root.packageRoot, 'front_end')); /// 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 placeholderToIndexMap, String entry) { return entry.replaceAllMapped(_placeholderPattern, (match) => '{${placeholderToIndexMap[match.group(0)!]}}'); } /// Decodes a YAML object (obtained from `pkg/analyzer/messages.yaml`) into a /// two-level map of [ErrorCodeInfo], indexed first by class name and then by /// error name. Map> decodeAnalyzerMessagesYaml( Object? yaml) { Never problem(String message) { throw 'Problem in pkg/analyzer/messages.yaml: $message'; } var result = >{}; if (yaml is! Map) { 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) { 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) { problem('value associated with error $className.$errorName is not a ' 'map'); } try { var aliasFor = errorValue['aliasFor']; if (aliasFor is String) { 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) { problem('No Map value at "$aliasFor", aliased from ' '$className.$errorName'); } node = value; } (result[className] ??= {})[errorName] = AliasErrorCodeInfo( aliasFor: aliasFor, comment: errorValue['comment'] as String?); } else { (result[className] ??= {})[errorName] = AnalyzerErrorCodeInfo.fromYaml(errorValue); } } catch (e) { problem('while processing $className.$errorName, $e'); } } } return result; } /// Decodes a YAML object (obtained from `pkg/front_end/messages.yaml`) into a /// map from error name to [ErrorCodeInfo]. Map decodeCfeMessagesYaml(Object? yaml) { Never problem(String message) { throw 'Problem in pkg/front_end/messages.yaml: $message'; } var result = {}; if (yaml is! Map) { 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) { 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> _loadAnalyzerMessages() { Object? messagesYaml = loadYaml(File(join(analyzerPkgPath, 'messages.yaml')).readAsStringSync()); return decodeAnalyzerMessagesYaml(messagesYaml); } /// Loads front end messages from the front end's `messages.yaml` file. Map _loadFrontEndMessages() { Object? messagesYaml = loadYaml(File(join(frontEndPkgPath, 'messages.yaml')).readAsStringSync()); return decodeCfeMessagesYaml(messagesYaml); } /// Splits [text] on spaces using the given [maxWidth] (and [firstLineWidth] if /// given). List _splitText( String text, { required int maxWidth, int? firstLineWidth, }) { firstLineWidth ??= maxWidth; var lines = []; // 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({required this.aliasFor, super.comment}) : super( documentation: null, hasPublishedDocs: false, isUnresolvedIdentifier: false, sharedName: null, problemMessage: 'UNUSED', correctionMessage: null); 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, }); 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 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 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 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 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 infoToFrontEndCode = {}; CfeToAnalyzerErrorCodeTables._(Map 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 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 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 computePlaceholderToIndexMap() { var mapping = {}; 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) { var out = StringBuffer(); out.writeln('$className('); out.writeln("'${sharedName ?? errorCode}',"); var maxWidth = 80 - 8 /* indentation */ - 2 /* quotes */ - 1 /* comma */; final placeholderToIndexMap = computePlaceholderToIndexMap(); var messageAsCode = convertTemplate(placeholderToIndexMap, problemMessage); var messageLines = _splitText(messageAsCode, maxWidth: maxWidth, firstLineWidth: maxWidth + 4); out.writeln('${messageLines.map(json.encode).join('\n')},'); final 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 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 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 toYaml() => { if (analyzerCode.isNotEmpty) 'analyzerCode': _encodeAnalyzerCode(analyzerCode), if (index != null) 'index': index, ...super.toYaml(), }; static List _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 analyzerCode) { if (analyzerCode.length == 1) { return analyzerCode.single; } else { return analyzerCode; } } }