OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// Defines messages templates and an adapter for TransformLogger to be able |
| 6 /// report error messages from transformers and refer to them in a consistent |
| 7 /// manner long term. |
| 8 library code_transformers.messages; |
| 9 |
| 10 // Note: this library purposely doesn't depend on dart:io, dart:html, or barback |
| 11 // so it can easily be used both in the transformers and in client-side apps |
| 12 // (for example in the log_injector). |
| 13 import 'dart:collection' show LinkedHashMap; |
| 14 import 'package:source_span/source_span.dart'; |
| 15 |
| 16 /// A globally unique identifier for an error message. This identifier should be |
| 17 /// stable, that is, it should never change after it is asigned to a particular |
| 18 /// message. That allows us to document our error messages and make them |
| 19 /// searchable for prosperity. |
| 20 class MessageId implements Comparable { |
| 21 /// Name of the package that declares this message. |
| 22 final String package; |
| 23 |
| 24 /// Message identifier number, unique within the package. |
| 25 final int id; |
| 26 |
| 27 const MessageId(this.package, this.id); |
| 28 |
| 29 static const MessageId NOT_SPECIFIED = const MessageId('unknown', 0); |
| 30 |
| 31 /// Serialize this message. We use a string and not a map to encode ids so |
| 32 /// they can be used as keys in JSON maps. |
| 33 String toJson() => toString(); |
| 34 |
| 35 toString() => '${package}#$id'; |
| 36 |
| 37 int compareTo(MessageId other) { |
| 38 var res = package.compareTo(other.package); |
| 39 if (res != 0) return res; |
| 40 return id.compareTo(other.id); |
| 41 } |
| 42 |
| 43 /// Creates a new [MessageId] from an encoded value produced via [toJson]. |
| 44 factory MessageId.fromJson(data) { |
| 45 var index = data.lastIndexOf('#'); |
| 46 if (index == -1) throw 'Invalid message id: $data'; |
| 47 return new MessageId(data.substring(0, index), |
| 48 int.parse(data.substring(index + 1))); |
| 49 } |
| 50 |
| 51 operator ==(MessageId other) => package == other.package && id == other.id; |
| 52 int get hashCode => 31 * package.hashCode + id; |
| 53 } |
| 54 |
| 55 /// An instance of an error message. These are typically produced from a |
| 56 /// [MessageTemplate]. |
| 57 class Message { |
| 58 /// A globally unique identifier for this message. |
| 59 final MessageId id; |
| 60 |
| 61 /// A snippet message that is presented to the user. |
| 62 final String snippet; |
| 63 |
| 64 const Message(this.id, this.snippet); |
| 65 |
| 66 const Message.unknown(this.snippet) : id = MessageId.NOT_SPECIFIED; |
| 67 |
| 68 /// Serializes this message to JSON. |
| 69 Map toJson() => {'id': id.toJson(), 'snippet': snippet}; |
| 70 String toString() => 'id: $id, snippet: $snippet'; |
| 71 |
| 72 /// Creates a new [Message] from an encoded value produced via [toJson]. |
| 73 factory Message.fromJson(data) => new Message( |
| 74 new MessageId.fromJson(data['id']), data['snippet']); |
| 75 } |
| 76 |
| 77 /// Template for a message. Templates can include placeholders to indicate |
| 78 /// values that are different for each instance of the error. Calling [create] |
| 79 /// will generate the actual message, with the placeholders replaced with |
| 80 /// values. If there are no placeholders, an instance of [MessageTemplate] is a |
| 81 /// valid instance of [Message] as well. |
| 82 class MessageTemplate implements Message { |
| 83 /// Unique and stable id for the message. |
| 84 final MessageId id; |
| 85 |
| 86 /// Template message with placeholders of the form `%-name-%`. |
| 87 final String snippetTemplate; |
| 88 |
| 89 /// This returns the message snippet, only if it the template has no |
| 90 /// placeholders, otherwise this throws an exception. Most messages have no |
| 91 /// placeholder arguments, in those cases, the snippet can be computed |
| 92 /// without specifying any arguments (exactly like calling `create()` with no |
| 93 /// arguments). |
| 94 String get snippet => _createSnippet(); |
| 95 |
| 96 /// Short description of the error message, typically used as a title of the |
| 97 /// error message in autogenerated documentation. This should be a single |
| 98 /// phrase, and cannot use placeholders. |
| 99 final String description; |
| 100 |
| 101 /// Additional details about this error message. These are used to |
| 102 /// automatically generate documentation. |
| 103 final String details; |
| 104 |
| 105 const MessageTemplate( |
| 106 this.id, this.snippetTemplate, this.description, this.details); |
| 107 |
| 108 static final _placeholderPattern = new RegExp(r"%-(\w*)-%"); |
| 109 |
| 110 _createSnippet([Map args = const {}, bool fillUnknowns = false]) { |
| 111 var snippet = snippetTemplate.replaceAllMapped(_placeholderPattern, (m) { |
| 112 var arg = m.group(1); |
| 113 var value = args[arg]; |
| 114 if (value != null) return '$value'; |
| 115 if (fillUnknowns) return ''; |
| 116 throw "missing argument $arg, for error message: $snippetTemplate"; |
| 117 }); |
| 118 return snippet; |
| 119 } |
| 120 |
| 121 create([Map args = const {}, bool fillUnknowns = false]) => |
| 122 new Message(id, _createSnippet(args, fillUnknowns)); |
| 123 |
| 124 /// Serializes this message to JSON. |
| 125 Map toJson() => create().toJson(); |
| 126 String toString() => '${toJson()}'; |
| 127 } |
| 128 |
| 129 /// Represents an actual log entry for a build error message. Including the |
| 130 /// actual message, its severity level (warning, error, etc), and a source span |
| 131 /// for a code location that is revelant to the message. |
| 132 class BuildLogEntry { |
| 133 /// The actual message. |
| 134 final Message message; |
| 135 |
| 136 /// Severity level. |
| 137 final String level; |
| 138 |
| 139 /// Location associated with this message, if any. |
| 140 final SourceSpan span; |
| 141 |
| 142 BuildLogEntry(this.message, this.span, this.level); |
| 143 |
| 144 /// Creates a new [BuildLogEntry] from an encoded value produced via [toJson]. |
| 145 factory BuildLogEntry.fromJson(Map data) { |
| 146 var spanData = data['span']; |
| 147 var span = null; |
| 148 if (spanData != null) { |
| 149 var locData = spanData['start']; |
| 150 var start = new SourceLocation(locData['offset'], |
| 151 sourceUrl: Uri.parse(locData['url']), |
| 152 line: locData['line'], |
| 153 column: locData['column']); |
| 154 locData = spanData['end']; |
| 155 var end = new SourceLocation(locData['offset'], |
| 156 sourceUrl: Uri.parse(locData['url']), |
| 157 line: locData['line'], |
| 158 column: locData['column']); |
| 159 span = new SourceSpan(start, end, spanData['text']); |
| 160 } |
| 161 return new BuildLogEntry( |
| 162 new Message.fromJson(data['message']), span, data['level']); |
| 163 } |
| 164 |
| 165 /// Serializes this log entry to JSON. |
| 166 Map toJson() { |
| 167 var data = { |
| 168 'level': level, |
| 169 'message': message.toJson(), |
| 170 }; |
| 171 if (span != null) { |
| 172 data['span'] = { |
| 173 'start': { |
| 174 'url': span.start.sourceUrl.toString(), |
| 175 'offset': span.start.offset, |
| 176 'line': span.start.line, |
| 177 'column': span.start.column, |
| 178 }, |
| 179 'end': { |
| 180 'url': span.end.sourceUrl.toString(), |
| 181 'offset': span.end.offset, |
| 182 'line': span.end.line, |
| 183 'column': span.end.column, |
| 184 }, |
| 185 'text': span.text, |
| 186 }; |
| 187 } |
| 188 return data; |
| 189 } |
| 190 String toString() => '${toJson()}'; |
| 191 } |
| 192 |
| 193 /// A table of entries, that clusters error messages by id. |
| 194 class LogEntryTable { |
| 195 final Map<MessageId, List<BuildLogEntry>> entries; |
| 196 |
| 197 LogEntryTable() : entries = new LinkedHashMap(); |
| 198 |
| 199 /// Creates a new [LogEntryTable] from an encoded value produced via [toJson]. |
| 200 factory LogEntryTable.fromJson(Map json) { |
| 201 var res = new LogEntryTable(); |
| 202 for (String key in json.keys) { |
| 203 var id = new MessageId.fromJson(key); |
| 204 res.entries[id] = json[key] |
| 205 .map((v) => new BuildLogEntry.fromJson(v)).toList(); |
| 206 } |
| 207 return res; |
| 208 } |
| 209 |
| 210 |
| 211 /// Serializes this entire table as JSON. |
| 212 Map toJson() { |
| 213 var res = {}; |
| 214 entries.forEach((key, value) { |
| 215 res['$key'] = value.map((e) => e.toJson()).toList(); |
| 216 }); |
| 217 return res; |
| 218 } |
| 219 String toString() => '${toJson()}'; |
| 220 |
| 221 void add(BuildLogEntry entry) { |
| 222 entries.putIfAbsent(entry.message.id, () => []).add(entry); |
| 223 } |
| 224 void addAll(LogEntryTable other) { |
| 225 for (var key in other.entries.keys) { |
| 226 var values = entries.putIfAbsent(key, () => []); |
| 227 values.addAll(other.entries[key]); |
| 228 } |
| 229 } |
| 230 } |
OLD | NEW |