Index: pkg/code_transformers/lib/messages/messages.dart |
diff --git a/pkg/code_transformers/lib/messages/messages.dart b/pkg/code_transformers/lib/messages/messages.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b30c58ecfd32bdfc07a4e831d94b9983686f3ac4 |
--- /dev/null |
+++ b/pkg/code_transformers/lib/messages/messages.dart |
@@ -0,0 +1,230 @@ |
+// Copyright (c) 2014, 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. |
+ |
+/// Defines messages templates and an adapter for TransformLogger to be able |
+/// report error messages from transformers and refer to them in a consistent |
+/// manner long term. |
+library code_transformers.messages; |
+ |
+// Note: this library purposely doesn't depend on dart:io, dart:html, or barback |
+// so it can easily be used both in the transformers and in client-side apps |
+// (for example in the log_injector). |
+import 'dart:collection' show LinkedHashMap; |
+import 'package:source_span/source_span.dart'; |
+ |
+/// A globally unique identifier for an error message. This identifier should be |
+/// stable, that is, it should never change after it is asigned to a particular |
+/// message. That allows us to document our error messages and make them |
+/// searchable for prosperity. |
+class MessageId implements Comparable { |
+ /// Name of the package that declares this message. |
+ final String package; |
+ |
+ /// Message identifier number, unique within the package. |
+ final int id; |
+ |
+ const MessageId(this.package, this.id); |
+ |
+ static const MessageId NOT_SPECIFIED = const MessageId('unknown', 0); |
+ |
+ /// Serialize this message. We use a string and not a map to encode ids so |
+ /// they can be used as keys in JSON maps. |
+ String toJson() => toString(); |
+ |
+ toString() => '${package}#$id'; |
+ |
+ int compareTo(MessageId other) { |
+ var res = package.compareTo(other.package); |
+ if (res != 0) return res; |
+ return id.compareTo(other.id); |
+ } |
+ |
+ /// Creates a new [MessageId] from an encoded value produced via [toJson]. |
+ factory MessageId.fromJson(data) { |
+ var index = data.lastIndexOf('#'); |
+ if (index == -1) throw 'Invalid message id: $data'; |
+ return new MessageId(data.substring(0, index), |
+ int.parse(data.substring(index + 1))); |
+ } |
+ |
+ operator ==(MessageId other) => package == other.package && id == other.id; |
+ int get hashCode => 31 * package.hashCode + id; |
+} |
+ |
+/// An instance of an error message. These are typically produced from a |
+/// [MessageTemplate]. |
+class Message { |
+ /// A globally unique identifier for this message. |
+ final MessageId id; |
+ |
+ /// A snippet message that is presented to the user. |
+ final String snippet; |
+ |
+ const Message(this.id, this.snippet); |
+ |
+ const Message.unknown(this.snippet) : id = MessageId.NOT_SPECIFIED; |
+ |
+ /// Serializes this message to JSON. |
+ Map toJson() => {'id': id.toJson(), 'snippet': snippet}; |
+ String toString() => 'id: $id, snippet: $snippet'; |
+ |
+ /// Creates a new [Message] from an encoded value produced via [toJson]. |
+ factory Message.fromJson(data) => new Message( |
+ new MessageId.fromJson(data['id']), data['snippet']); |
+} |
+ |
+/// Template for a message. Templates can include placeholders to indicate |
+/// values that are different for each instance of the error. Calling [create] |
+/// will generate the actual message, with the placeholders replaced with |
+/// values. If there are no placeholders, an instance of [MessageTemplate] is a |
+/// valid instance of [Message] as well. |
+class MessageTemplate implements Message { |
+ /// Unique and stable id for the message. |
+ final MessageId id; |
+ |
+ /// Template message with placeholders of the form `%-name-%`. |
+ final String snippetTemplate; |
+ |
+ /// This returns the message snippet, only if it the template has no |
+ /// placeholders, otherwise this throws an exception. Most messages have no |
+ /// placeholder arguments, in those cases, the snippet can be computed |
+ /// without specifying any arguments (exactly like calling `create()` with no |
+ /// arguments). |
+ String get snippet => _createSnippet(); |
+ |
+ /// Short description of the error message, typically used as a title of the |
+ /// error message in autogenerated documentation. This should be a single |
+ /// phrase, and cannot use placeholders. |
+ final String description; |
+ |
+ /// Additional details about this error message. These are used to |
+ /// automatically generate documentation. |
+ final String details; |
+ |
+ const MessageTemplate( |
+ this.id, this.snippetTemplate, this.description, this.details); |
+ |
+ static final _placeholderPattern = new RegExp(r"%-(\w*)-%"); |
+ |
+ _createSnippet([Map args = const {}, bool fillUnknowns = false]) { |
+ var snippet = snippetTemplate.replaceAllMapped(_placeholderPattern, (m) { |
+ var arg = m.group(1); |
+ var value = args[arg]; |
+ if (value != null) return '$value'; |
+ if (fillUnknowns) return ''; |
+ throw "missing argument $arg, for error message: $snippetTemplate"; |
+ }); |
+ return snippet; |
+ } |
+ |
+ create([Map args = const {}, bool fillUnknowns = false]) => |
+ new Message(id, _createSnippet(args, fillUnknowns)); |
+ |
+ /// Serializes this message to JSON. |
+ Map toJson() => create().toJson(); |
+ String toString() => '${toJson()}'; |
+} |
+ |
+/// Represents an actual log entry for a build error message. Including the |
+/// actual message, its severity level (warning, error, etc), and a source span |
+/// for a code location that is revelant to the message. |
+class BuildLogEntry { |
+ /// The actual message. |
+ final Message message; |
+ |
+ /// Severity level. |
+ final String level; |
+ |
+ /// Location associated with this message, if any. |
+ final SourceSpan span; |
+ |
+ BuildLogEntry(this.message, this.span, this.level); |
+ |
+ /// Creates a new [BuildLogEntry] from an encoded value produced via [toJson]. |
+ factory BuildLogEntry.fromJson(Map data) { |
+ var spanData = data['span']; |
+ var span = null; |
+ if (spanData != null) { |
+ var locData = spanData['start']; |
+ var start = new SourceLocation(locData['offset'], |
+ sourceUrl: Uri.parse(locData['url']), |
+ line: locData['line'], |
+ column: locData['column']); |
+ locData = spanData['end']; |
+ var end = new SourceLocation(locData['offset'], |
+ sourceUrl: Uri.parse(locData['url']), |
+ line: locData['line'], |
+ column: locData['column']); |
+ span = new SourceSpan(start, end, spanData['text']); |
+ } |
+ return new BuildLogEntry( |
+ new Message.fromJson(data['message']), span, data['level']); |
+ } |
+ |
+ /// Serializes this log entry to JSON. |
+ Map toJson() { |
+ var data = { |
+ 'level': level, |
+ 'message': message.toJson(), |
+ }; |
+ if (span != null) { |
+ data['span'] = { |
+ 'start': { |
+ 'url': span.start.sourceUrl.toString(), |
+ 'offset': span.start.offset, |
+ 'line': span.start.line, |
+ 'column': span.start.column, |
+ }, |
+ 'end': { |
+ 'url': span.end.sourceUrl.toString(), |
+ 'offset': span.end.offset, |
+ 'line': span.end.line, |
+ 'column': span.end.column, |
+ }, |
+ 'text': span.text, |
+ }; |
+ } |
+ return data; |
+ } |
+ String toString() => '${toJson()}'; |
+} |
+ |
+/// A table of entries, that clusters error messages by id. |
+class LogEntryTable { |
+ final Map<MessageId, List<BuildLogEntry>> entries; |
+ |
+ LogEntryTable() : entries = new LinkedHashMap(); |
+ |
+ /// Creates a new [LogEntryTable] from an encoded value produced via [toJson]. |
+ factory LogEntryTable.fromJson(Map json) { |
+ var res = new LogEntryTable(); |
+ for (String key in json.keys) { |
+ var id = new MessageId.fromJson(key); |
+ res.entries[id] = json[key] |
+ .map((v) => new BuildLogEntry.fromJson(v)).toList(); |
+ } |
+ return res; |
+ } |
+ |
+ |
+ /// Serializes this entire table as JSON. |
+ Map toJson() { |
+ var res = {}; |
+ entries.forEach((key, value) { |
+ res['$key'] = value.map((e) => e.toJson()).toList(); |
+ }); |
+ return res; |
+ } |
+ String toString() => '${toJson()}'; |
+ |
+ void add(BuildLogEntry entry) { |
+ entries.putIfAbsent(entry.message.id, () => []).add(entry); |
+ } |
+ void addAll(LogEntryTable other) { |
+ for (var key in other.entries.keys) { |
+ var values = entries.putIfAbsent(key, () => []); |
+ values.addAll(other.entries[key]); |
+ } |
+ } |
+} |