This repository has been archived by the owner on Aug 18, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
/
messages.dart
235 lines (192 loc) · 7.76 KB
/
messages.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
// 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<MessageId> {
/// 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 ==(Object other) =>
other is MessageId && 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<String, Iterable> 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]);
}
}
}