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( | |
48 data.substring(0, index), 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) => | |
74 new Message(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 = {'level': level, 'message': message.toJson(),}; | |
168 if (span != null) { | |
169 data['span'] = { | |
170 'start': { | |
171 'url': span.start.sourceUrl.toString(), | |
172 'offset': span.start.offset, | |
173 'line': span.start.line, | |
174 'column': span.start.column, | |
175 }, | |
176 'end': { | |
177 'url': span.end.sourceUrl.toString(), | |
178 'offset': span.end.offset, | |
179 'line': span.end.line, | |
180 'column': span.end.column, | |
181 }, | |
182 'text': span.text, | |
183 }; | |
184 } | |
185 return data; | |
186 } | |
187 String toString() => '${toJson()}'; | |
188 } | |
189 | |
190 /// A table of entries, that clusters error messages by id. | |
191 class LogEntryTable { | |
192 final Map<MessageId, List<BuildLogEntry>> entries; | |
193 | |
194 LogEntryTable() : entries = new LinkedHashMap(); | |
195 | |
196 /// Creates a new [LogEntryTable] from an encoded value produced via [toJson]. | |
197 factory LogEntryTable.fromJson(Map json) { | |
198 var res = new LogEntryTable(); | |
199 for (String key in json.keys) { | |
200 var id = new MessageId.fromJson(key); | |
201 res.entries[id] = | |
202 json[key].map((v) => new BuildLogEntry.fromJson(v)).toList(); | |
203 } | |
204 return res; | |
205 } | |
206 | |
207 /// Serializes this entire table as JSON. | |
208 Map toJson() { | |
209 var res = {}; | |
210 entries.forEach((key, value) { | |
211 res['$key'] = value.map((e) => e.toJson()).toList(); | |
212 }); | |
213 return res; | |
214 } | |
215 String toString() => '${toJson()}'; | |
216 | |
217 void add(BuildLogEntry entry) { | |
218 entries.putIfAbsent(entry.message.id, () => []).add(entry); | |
219 } | |
220 void addAll(LogEntryTable other) { | |
221 for (var key in other.entries.keys) { | |
222 var values = entries.putIfAbsent(key, () => []); | |
223 values.addAll(other.entries[key]); | |
224 } | |
225 } | |
226 } | |
OLD | NEW |