blob: 56f3c0e22315734071b69be714372996eb99e272 [file] [log] [blame]
// 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]);
}
}
}