// 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 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> 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'), ); /// 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> 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 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> 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 = >{}; 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', ); } 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) { 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 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() => decodeAnalyzerMessagesYaml(analyzerPkgPath); /// 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); } /// Loads linter messages from the linter's `messages.yaml` file. Map> _loadLintMessages() => decodeAnalyzerMessagesYaml(linterPkgPath); /// 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._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 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 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 = '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; /// `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]. /// /// [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) { 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 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; } } }