| // Copyright (c) 2024, 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:front_end/src/codes/type_labeler.dart'; |
| library; |
| |
| import 'package:analyzer_testing/utilities/extensions/string.dart'; |
| import 'package:analyzer_utilities/located_error.dart'; |
| import 'package:analyzer_utilities/messages.dart'; |
| |
| class MessageAccumulator { |
| static const doNotEditComment = |
| '// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.'; |
| |
| /// The buffer in which generated code will be accumulated. |
| final StringBuffer _newBuffer = new StringBuffer(); |
| |
| new() { |
| const String preamble1 = """ |
| // 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. |
| |
| """; |
| |
| const String preamble2 = """ |
| |
| // NOTE: THIS FILE IS GENERATED. DO NOT EDIT. |
| // |
| // Instead modify 'pkg/front_end/messages.yaml' and defer to it for the |
| // commands to update this file. |
| |
| // ignore_for_file: lines_longer_than_80_chars |
| """; |
| |
| _newBuffer.writeln(preamble1); |
| _newBuffer.writeln(preamble2); |
| _newBuffer.writeln(""" |
| part of 'diagnostic.dart'; |
| """); |
| } |
| |
| Messages finish({required String packageName, required String path}) { |
| return Messages( |
| packageName: packageName, |
| path: path, |
| contents: _newBuffer.toString(), |
| ); |
| } |
| |
| void writeConstant({ |
| required String type, |
| required String oldName, |
| required String newName, |
| required String initializer, |
| }) { |
| _newBuffer.writeln(doNotEditComment); |
| _newBuffer.writeln('const $type $newName = $initializer;'); |
| _newBuffer.writeln(); |
| } |
| |
| void writeEnum({ |
| required String documentation, |
| required String name, |
| required List<String> values, |
| }) { |
| _newBuffer.writeln(); |
| _newBuffer.writeln('/// $documentation'); |
| _newBuffer.writeln('enum $name {'); |
| for (var value in values) { |
| _newBuffer.writeln(' $value,'); |
| } |
| _newBuffer.writeln('}'); |
| } |
| |
| void writeWithArgumentsFunction(String function) { |
| _newBuffer.writeln(doNotEditComment); |
| _newBuffer.writeln(function); |
| } |
| } |
| |
| /// Information about the message code to generate for a given package. |
| class Messages { |
| /// The name of the package to which files are being generated. |
| final String packageName; |
| |
| /// The path to the generated file, relative to the root of the package. |
| final String path; |
| |
| /// The string to write to the generated file. |
| final String contents; |
| |
| new({required this.packageName, required this.path, required this.contents}); |
| |
| /// Computes the absolute file URI to the generated file. |
| /// |
| /// [repoDir] is the absolute file URI of the SDK repo. |
| Uri uri(Uri repoDir) => repoDir.resolve('pkg/$packageName/$path'); |
| } |
| |
| List<Messages> generateMessagesFilesRaw(Uri repoDir) { |
| MessageAccumulator sharedMessages = new MessageAccumulator(); |
| MessageAccumulator cfeMessages = new MessageAccumulator(); |
| |
| var pseudoSharedCodeValues = <String>{}; |
| for (var message in diagnosticTables.sortedFrontEndDiagnostics) { |
| var forFeAnalyzerShared = |
| message is SharedMessage || |
| message is FrontEndMessage && message.pseudoSharedCode != null; |
| LocatedError.wrap( |
| span: message.keySpan, |
| () => _TemplateCompiler( |
| message: message, |
| pseudoSharedCodeValues: forFeAnalyzerShared |
| ? pseudoSharedCodeValues |
| : null, |
| ).compile(forFeAnalyzerShared ? sharedMessages : cfeMessages), |
| ); |
| } |
| sharedMessages.writeEnum( |
| documentation: |
| 'Enum containing analyzer error codes referenced by ' |
| '[Code.pseudoSharedCode].', |
| name: 'PseudoSharedCode', |
| values: pseudoSharedCodeValues.toList()..sort(), |
| ); |
| sharedMessages.writeEnum( |
| documentation: |
| 'Enum containing analyzer error codes referenced by ' |
| '[Code.sharedCode].', |
| name: 'SharedCode', |
| values: [ |
| for (var code in diagnosticTables.sortedSharedDiagnostics) |
| code.analyzerCode.camelCaseName, |
| ], |
| ); |
| |
| return [ |
| sharedMessages.finish( |
| packageName: '_fe_analyzer_shared', |
| path: "lib/src/messages/diagnostic.g.dart", |
| ), |
| cfeMessages.finish( |
| packageName: 'front_end', |
| path: "lib/src/codes/diagnostic.g.dart", |
| ), |
| ]; |
| } |
| |
| /// Returns a fresh identifier that is not yet present in [usedNames], and adds |
| /// it to [usedNames]. |
| /// |
| /// The name [nameHint] is used if it is available. Otherwise a new name is |
| /// chosen by appending characters to it. |
| String _newName({required Set<String> usedNames, required String nameHint}) { |
| if (usedNames.add(nameHint)) return nameHint; |
| for (var i = 0; ; i++) { |
| var name = "${nameHint}_$i"; |
| if (usedNames.add(name)) return name; |
| } |
| } |
| |
| class _TemplateCompiler { |
| final String pascalCaseName; |
| final String camelCaseName; |
| final CfeStyleMessage message; |
| final List<TemplatePart> problemMessage; |
| final List<TemplatePart>? correctionMessage; |
| final String? severity; |
| final Map<String, DiagnosticParameter> parameters; |
| final String? pseudoSharedCode; |
| |
| /// If the template will be generated into `pkg/_fe_analyzer_shared`, a set of |
| /// strings representing the values that will be generated for the |
| /// `PseudoSharedCode` enum; otherwise `null`. |
| /// |
| /// The template compiler will add to this set as needed. |
| final Set<String>? pseudoSharedCodeValues; |
| |
| late final Set<String> usedNames = { |
| 'conversions', |
| 'labeler', |
| ...parameters.keys, |
| }; |
| late final List<String> arguments = parameters.keys |
| .map((name) => "'$name': $name") |
| .toList(); |
| final Map<TemplateParameterPart, String> interpolators = {}; |
| final List<String> withArgumentsStatements = []; |
| bool hasLabeler = false; |
| |
| new({required this.message, required this.pseudoSharedCodeValues}) |
| : pascalCaseName = message.frontEndCode.pascalCaseName, |
| camelCaseName = message.frontEndCode.camelCaseName, |
| problemMessage = message.problemMessage, |
| correctionMessage = message.correctionMessage, |
| severity = message.cfeSeverity, |
| parameters = message.parameters, |
| pseudoSharedCode = message is FrontEndMessage |
| ? message.pseudoSharedCode |
| : null; |
| |
| void compile(MessageAccumulator messageAccumulator) { |
| var codeArguments = <String>[ |
| if (pseudoSharedCodeValues != null && pseudoSharedCode != null) |
| 'pseudoSharedCode: ${_encodePseudoSharedCode(pseudoSharedCode!)}', |
| if (severity != null) 'severity: CfeSeverity.$severity', |
| if (message case SharedMessage(:var analyzerCode)) |
| 'sharedCode: SharedCode.${analyzerCode.camelCaseName}', |
| ]; |
| |
| String interpolatedProblemMessage = interpolate(problemMessage)!; |
| String? interpolatedCorrectionMessage = interpolate(correctionMessage); |
| if (hasLabeler) { |
| interpolatedProblemMessage += " + labeler.originMessages"; |
| } |
| |
| String constantType; |
| String constantInitializer; |
| List<String> withArgumentsFunctions = []; |
| if (parameters.isEmpty) { |
| codeArguments.add('problemMessage: $interpolatedProblemMessage'); |
| if (correctionMessage != null) { |
| codeArguments.add('correctionMessage: $interpolatedCorrectionMessage'); |
| } |
| |
| constantType = 'MessageCode'; |
| constantInitializer = |
| 'const MessageCode("$pascalCaseName", ${codeArguments.join(', ')},)'; |
| } else { |
| List<String> templateArguments = <String>[]; |
| templateArguments.add('\"$pascalCaseName\"'); |
| templateArguments.add("withArguments: _withArguments$pascalCaseName"); |
| templateArguments.addAll(codeArguments); |
| |
| List<String> messageArguments = <String>[ |
| "problemMessage: $interpolatedProblemMessage", |
| if (interpolatedCorrectionMessage case var m?) "correctionMessage: $m", |
| "arguments: { ${arguments.join(', ')}, }", |
| ]; |
| List<String> namedParameters = parameters.entries |
| .map((entry) => 'required ${entry.value.type.cfeName!} ${entry.key}') |
| .toList(); |
| |
| constantType = |
| """ |
| Template< |
| Message Function({${namedParameters.join(', ')}}) |
| >"""; |
| constantInitializer = 'const Template(${templateArguments.join(', ')},)'; |
| withArgumentsFunctions.add(""" |
| Message _withArguments$pascalCaseName({${namedParameters.join(', ')}}) { |
| ${withArgumentsStatements.join('\n ')} |
| return new Message( |
| $camelCaseName, |
| ${messageArguments.join(', ')},); |
| } |
| """); |
| } |
| messageAccumulator.writeConstant( |
| type: constantType, |
| oldName: 'code$pascalCaseName', |
| newName: camelCaseName, |
| initializer: constantInitializer, |
| ); |
| for (var withArgumentsFunction in withArgumentsFunctions) { |
| messageAccumulator.writeWithArgumentsFunction(withArgumentsFunction); |
| } |
| } |
| |
| String computeInterpolator(TemplateParameterPart placeholder) { |
| var parameter = placeholder.parameter; |
| var name = parameter.name; |
| var type = parameter.type; |
| var conversion = placeholder.conversionOverride ?? type.cfeConversion; |
| if (conversion is LabelerConversion && !hasLabeler) { |
| withArgumentsStatements.add("TypeLabeler labeler = new TypeLabeler();"); |
| hasLabeler = true; |
| } |
| |
| if (conversion?.toCode(name: name, type: type) case var conversion?) { |
| var interpolator = _newName(usedNames: usedNames, nameHint: name); |
| withArgumentsStatements.add("var $interpolator = $conversion;"); |
| return interpolator; |
| } else { |
| return name; |
| } |
| } |
| |
| String? interpolate(List<TemplatePart>? template) { |
| if (template == null) return null; |
| var text = template |
| .map( |
| (part) => switch (part) { |
| TemplateLiteralPart(:var text) => |
| text.replaceAll(r'\', r'\\').replaceAll(r"$", r"\$"), |
| TemplateParameterPart() => |
| "\${${interpolators[part] ??= computeInterpolator(part)}}", |
| }, |
| ) |
| .join(); |
| return "\"\"\"$text\"\"\""; |
| } |
| |
| /// Creates the list literal that should populate the error code's |
| /// `pseudoSharedCode` value. |
| String _encodePseudoSharedCode(String code) { |
| var camelCaseCode = code.toCamelCase(); |
| pseudoSharedCodeValues!.add(camelCaseCode); |
| return 'PseudoSharedCode.$camelCaseCode'; |
| } |
| } |