// Copyright (c) 2017, 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. // @dart=2.9 import 'dart:io'; import 'dart:isolate'; import "package:_fe_analyzer_shared/src/messages/severity.dart" show severityEnumNames; import 'package:dart_style/dart_style.dart' show DartFormatter; import 'package:yaml/yaml.dart' show loadYaml; main(List arguments) async { var port = new ReceivePort(); Messages message = await generateMessagesFiles(); if (message.sharedMessages.trim().isEmpty || message.cfeMessages.trim().isEmpty) { print("Bailing because of errors: " "Refusing to overwrite with empty file!"); } else { await new File.fromUri(await computeSharedGeneratedFile()) .writeAsString(message.sharedMessages, flush: true); await new File.fromUri(await computeCfeGeneratedFile()) .writeAsString(message.cfeMessages, flush: true); } port.close(); } Future computeSharedGeneratedFile() { return Isolate.resolvePackageUri(Uri.parse( 'package:_fe_analyzer_shared/src/messages/codes_generated.dart')); } Future computeCfeGeneratedFile() { return Isolate.resolvePackageUri( Uri.parse('package:front_end/src/fasta/fasta_codes_cfe_generated.dart')); } class Messages { final String sharedMessages; final String cfeMessages; Messages(this.sharedMessages, this.cfeMessages); } Future generateMessagesFiles() async { Uri messagesFile = Platform.script.resolve("../../messages.yaml"); Map yaml = loadYaml(await new File.fromUri(messagesFile).readAsStringSync()); StringBuffer sharedMessages = new StringBuffer(); StringBuffer cfeMessages = new StringBuffer(); 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 run // 'pkg/front_end/tool/fasta generate-messages' to update. // ignore_for_file: lines_longer_than_80_chars """; sharedMessages.writeln(preamble1); sharedMessages.writeln(preamble2); sharedMessages.writeln(""" part of _fe_analyzer_shared.messages.codes; """); cfeMessages.writeln(preamble1); cfeMessages.writeln(""" // @dart = 2.9 """); cfeMessages.writeln(preamble2); cfeMessages.writeln(""" part of fasta.codes; """); bool hasError = false; int largestIndex = 0; final indexNameMap = new Map(); List keys = yaml.keys.cast().toList()..sort(); for (String name in keys) { var description = yaml[name]; while (description is String) { description = yaml[description]; } Map map = description; if (map == null) { throw "No 'template:' in key $name."; } var index = map['index']; if (index != null) { if (index is! int || index < 1) { print('Error: Expected positive int for "index:" field in $name,' ' but found $index'); hasError = true; index = -1; // Continue looking for other problems. } else { String otherName = indexNameMap[index]; if (otherName != null) { print('Error: The "index:" field must be unique, ' 'but is the same for $otherName and $name'); hasError = true; // Continue looking for other problems. } else { indexNameMap[index] = name; if (largestIndex < index) { largestIndex = index; } } } } Template template = compileTemplate(name, index, map['template'], map['tip'], map['analyzerCode'], map['severity']); if (template.isShared) { sharedMessages.writeln(template.text); } else { cfeMessages.writeln(template.text); } } if (largestIndex > indexNameMap.length) { print('Error: The "index:" field values should be unique, consecutive' ' whole numbers starting with 1.'); hasError = true; // Fall through to print more information. } if (hasError) { exitCode = 1; print('The largest index is $largestIndex'); final sortedIndices = indexNameMap.keys.toList()..sort(); int nextAvailableIndex = largestIndex + 1; for (int index = 1; index <= sortedIndices.length; ++index) { if (sortedIndices[index - 1] != index) { nextAvailableIndex = index; break; } } print('The next available index is ${nextAvailableIndex}'); return new Messages('', ''); } return new Messages(new DartFormatter().format("$sharedMessages"), new DartFormatter().format("$cfeMessages")); } final RegExp placeholderPattern = new RegExp("#\([-a-zA-Z0-9_]+\)(?:%\([0-9]*\)\.\([0-9]+\))?"); class Template { final String text; final isShared; Template(this.text, {this.isShared}) : assert(isShared != null); } Template compileTemplate(String name, int index, String template, String tip, Object analyzerCode, String severity) { if (template == null) { print('Error: missing template for message: $name'); exitCode = 1; return new Template('', isShared: true); } // Remove trailing whitespace. This is necessary for templates defined with // `|` (verbatim) as they always contain a trailing newline that we don't // want. template = template.trimRight(); const String ignoreNotNull = "// ignore: unnecessary_null_comparison"; var parameters = new Set(); var conversions = new Set(); var conversions2 = new Set(); var arguments = new Set(); bool hasLabeler = false; bool canBeShared = true; void ensureLabeler() { if (hasLabeler) return; conversions .add("TypeLabeler labeler = new TypeLabeler(isNonNullableByDefault);"); hasLabeler = true; canBeShared = false; } for (Match match in placeholderPattern.allMatches("$template\n${tip ?? ''}")) { String name = match[1]; String padding = match[2]; String fractionDigits = match[3]; String format(String name) { String conversion; if (fractionDigits == null) { conversion = "'\$$name'"; } else { conversion = "$name.toStringAsFixed($fractionDigits)"; } if (padding.isNotEmpty) { if (padding.startsWith("0")) { conversion += ".padLeft(${int.parse(padding)}, '0')"; } else { conversion += ".padLeft(${int.parse(padding)})"; } } return conversion; } switch (name) { case "character": parameters.add("String character"); conversions.add("if (character.runes.length != 1)" "throw \"Not a character '\${character}'\";"); arguments.add("'$name': character"); break; case "unicode": // Write unicode value using at least four (but otherwise no more than // necessary) hex digits, using uppercase letters. // http://www.unicode.org/versions/Unicode10.0.0/appA.pdf parameters.add("int codePoint"); conversions.add("String unicode = \"U+\${codePoint.toRadixString(16)" ".toUpperCase().padLeft(4, '0')}\";"); arguments.add("'$name': codePoint"); break; case "name": parameters.add("String name"); conversions.add("if (name.isEmpty) throw 'No name provided';"); arguments.add("'$name': name"); conversions.add("name = demangleMixinApplicationName(name);"); break; case "name2": parameters.add("String name2"); conversions.add("if (name2.isEmpty) throw 'No name provided';"); arguments.add("'$name': name2"); conversions.add("name2 = demangleMixinApplicationName(name2);"); break; case "name3": parameters.add("String name3"); conversions.add("if (name3.isEmpty) throw 'No name provided';"); arguments.add("'$name': name3"); conversions.add("name3 = demangleMixinApplicationName(name3);"); break; case "name4": parameters.add("String name4"); conversions.add("if (name4.isEmpty) throw 'No name provided';"); arguments.add("'$name': name4"); conversions.add("name4 = demangleMixinApplicationName(name4);"); break; case "nameOKEmpty": parameters.add("String nameOKEmpty"); conversions.add("$ignoreNotNull\n" "if (nameOKEmpty == null || nameOKEmpty.isEmpty) " "nameOKEmpty = '(unnamed)';"); arguments.add("'nameOKEmpty': nameOKEmpty"); break; case "names": parameters.add("List _names"); conversions.add("if (_names.isEmpty) throw 'No names provided';"); arguments.add("'$name': _names"); conversions.add("String names = itemizeNames(_names);"); break; case "lexeme": parameters.add("Token token"); conversions.add("String lexeme = token.lexeme;"); arguments.add("'$name': token"); break; case "lexeme2": parameters.add("Token token2"); conversions.add("String lexeme2 = token2.lexeme;"); arguments.add("'$name': token2"); break; case "string": parameters.add("String string"); conversions.add("if (string.isEmpty) throw 'No string provided';"); arguments.add("'$name': string"); break; case "string2": parameters.add("String string2"); conversions.add("if (string2.isEmpty) throw 'No string provided';"); arguments.add("'$name': string2"); break; case "string3": parameters.add("String string3"); conversions.add("if (string3.isEmpty) throw 'No string provided';"); arguments.add("'$name': string3"); break; case "stringOKEmpty": parameters.add("String stringOKEmpty"); conversions.add("$ignoreNotNull\n" "if (stringOKEmpty == null || stringOKEmpty.isEmpty) " "stringOKEmpty = '(empty)';"); arguments.add("'$name': stringOKEmpty"); break; case "type": case "type2": case "type3": case "type4": parameters.add("DartType _${name}"); ensureLabeler(); conversions .add("List ${name}Parts = labeler.labelType(_${name});"); conversions2.add("String ${name} = ${name}Parts.join();"); arguments.add("'${name}': _${name}"); break; case "uri": parameters.add("Uri uri_"); conversions.add("String? uri = relativizeUri(uri_);"); arguments.add("'$name': uri_"); break; case "uri2": parameters.add("Uri uri2_"); conversions.add("String? uri2 = relativizeUri(uri2_);"); arguments.add("'$name': uri2_"); break; case "uri3": parameters.add("Uri uri3_"); conversions.add("String? uri3 = relativizeUri(uri3_);"); arguments.add("'$name': uri3_"); break; case "count": parameters.add("int count"); conversions.add( "$ignoreNotNull\n" "if (count == null) throw 'No count provided';"); arguments.add("'$name': count"); break; case "count2": parameters.add("int count2"); conversions.add("$ignoreNotNull\n" "if (count2 == null) throw 'No count provided';"); arguments.add("'$name': count2"); break; case "constant": parameters.add("Constant _constant"); ensureLabeler(); conversions.add( "List ${name}Parts = labeler.labelConstant(_${name});"); conversions2.add("String ${name} = ${name}Parts.join();"); arguments.add("'$name': _constant"); break; case "num1": parameters.add("num _num1"); conversions.add("$ignoreNotNull\n" "if (_num1 == null) throw 'No number provided';"); conversions.add("String num1 = ${format('_num1')};"); arguments.add("'$name': _num1"); break; case "num2": parameters.add("num _num2"); conversions.add("$ignoreNotNull\n" "if (_num2 == null) throw 'No number provided';"); conversions.add("String num2 = ${format('_num2')};"); arguments.add("'$name': _num2"); break; case "num3": parameters.add("num _num3"); conversions.add("$ignoreNotNull\n" "if (_num3 == null) throw 'No number provided';"); conversions.add("String num3 = ${format('_num3')};"); arguments.add("'$name': _num3"); break; default: throw "Unhandled placeholder in template: '$name'"; } } if (hasLabeler) { parameters.add("bool isNonNullableByDefault"); } conversions.addAll(conversions2); String interpolate(String text) { text = text .replaceAll(r"$", r"\$") .replaceAllMapped(placeholderPattern, (Match m) => "\${${m[1]}}"); return "\"\"\"$text\"\"\""; } List codeArguments = []; if (index != null) { codeArguments.add('index: $index'); } else if (analyzerCode != null) { if (analyzerCode is String) { analyzerCode = [analyzerCode]; } List codes = analyzerCode; // If "index:" is defined, then "analyzerCode:" should not be generated // in the front end. See comment in messages.yaml codeArguments.add('analyzerCodes: ["${codes.join('", "')}"]'); } if (severity != null) { String severityEnumName = severityEnumNames[severity]; if (severityEnumName == null) { throw "Unknown severity '$severity'"; } codeArguments.add('severity: Severity.$severityEnumName'); } if (parameters.isEmpty && conversions.isEmpty && arguments.isEmpty) { if (template != null) { codeArguments.add('message: r"""$template"""'); } if (tip != null) { codeArguments.add('tip: r"""$tip"""'); } return new Template(""" // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. const Code code$name = message$name; // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. const MessageCode message$name = const MessageCode(\"$name\", ${codeArguments.join(', ')}); """, isShared: canBeShared); } List templateArguments = []; if (template != null) { templateArguments.add('messageTemplate: r"""$template"""'); } if (tip != null) { templateArguments.add('tipTemplate: r"""$tip"""'); } templateArguments.add("withArguments: _withArguments$name"); List messageArguments = []; String message = interpolate(template); if (hasLabeler) { message += " + labeler.originMessages"; } messageArguments.add("message: ${message}"); if (tip != null) { messageArguments.add("tip: ${interpolate(tip)}"); } messageArguments.add("arguments: { ${arguments.join(', ')} }"); return new Template(""" // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. const Template template$name = const Template( ${templateArguments.join(', ')}); // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. const Code code$name = const Code( \"$name\", ${codeArguments.join(', ')}); // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE. Message _withArguments$name(${parameters.join(', ')}) { ${conversions.join('\n ')} return new Message( code$name, ${messageArguments.join(', ')}); } """, isShared: canBeShared); }