blob: e55c235ea45ce7636d595601a3daf98d8d6cdde4 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
typedef _OutputSender = void Function(String category, String message, {bool? parseStackFrames, int? variablesReference});
/// A formatter for improving the display of Flutter structured errors over DAP.
///
/// The formatter deserializes a `Flutter.Error` event and produces output
/// similar to the `renderedErrorText` field, but may include ansi color codes
/// to provide improved formatting (such as making stack frames from non-user
/// code faint) if the client indicated support.
///
/// Lines that look like stack frames will be marked so they can be parsed by
/// the base adapter and attached as [Source]s to allow them to be clickable
/// in the client.
class FlutterErrorFormatter {
final List<_BatchedOutput> batchedOutput = <_BatchedOutput>[];
/// Formats a Flutter error.
///
/// If this is not the first error since the reload, only a summary will be
/// included.
void formatError(Map<String, Object?> errorData) {
final _ErrorData data = _ErrorData(errorData);
const int assumedTerminalSize = 80;
const String barChar = '═';
final String headerPrefix = barChar * 8;
final String headerSuffix = barChar * math.max( assumedTerminalSize - (data.description?.length ?? 0) - 2 - headerPrefix.length, 0);
final String header = '$headerPrefix ${data.description} $headerSuffix';
_write('');
_write(header, isError: true);
if (data.errorsSinceReload == 0) {
data.properties.forEach(_writeNode);
data.children.forEach(_writeNode);
} else {
data.properties.forEach(_writeSummary);
}
_write(barChar * header.length, isError: true);
}
/// Sends all collected output through [sendOutput].
void sendOutput(_OutputSender sendOutput) {
for (final _BatchedOutput output in batchedOutput) {
sendOutput(
output.isError ? 'stderr' : 'stdout',
output.output,
parseStackFrames: output.parseStackFrames,
);
}
}
/// Writes [text] to the output.
///
/// If the last item in the batch has the same settings as this item, it will
/// be appended to the same item, otherwise a new item will be added to the
/// batch.
void _write(
String? text, {
int indent = 0,
bool isError = false,
bool parseStackFrames = false,
}) {
if (text != null) {
final String indentString = ' ' * indent;
final String message = '$indentString${text.trim()}';
_BatchedOutput? output = batchedOutput.lastOrNull;
if (output == null || output.isError != isError || output.parseStackFrames != parseStackFrames) {
batchedOutput.add(output = _BatchedOutput(isError, parseStackFrames: parseStackFrames));
}
output.writeln(message);
}
}
/// Writes [node] to the output using [indent], recursing unless [recursive]
/// is `false`.
void _writeNode(_ErrorNode node, {int indent = 0, bool recursive = true}) {
// Errors, summaries and lines starting "Exception:" are marked as errors so
// they go to stderr instead of stdout (this may cause the client to colour
// them like errors).
final bool showAsError = node.level == _DiagnosticsNodeLevel.error ||
node.level == _DiagnosticsNodeLevel.summary ||
(node.description?.startsWith('Exception: ') ?? false);
if (node.showName && node.name != null) {
_write('${node.name}: ${node.description}', indent: indent, isError: showAsError);
} else if (node.description?.startsWith('#') ?? false) {
// Possible stack frame.
_write(node.description, indent: indent, isError: showAsError, parseStackFrames: true);
} else {
_write(node.description, indent: indent, isError: showAsError);
}
if (recursive) {
if (node.style != _DiagnosticsNodeStyle.flat) {
indent++;
}
_writeNodes(node.properties, indent: indent);
_writeNodes(node.children, indent: indent);
}
}
/// Writes [nodes] to the output.
void _writeNodes(List<_ErrorNode> nodes, {int indent = 0, bool recursive = true}) {
for (final _ErrorNode child in nodes) {
_writeNode(child, indent: indent, recursive: recursive);
}
}
/// Writes a simple summary of [node] to the output.
void _writeSummary(_ErrorNode node) {
final bool allChildrenAreLeaf = node.children.isNotEmpty &&
!node.children.any((_ErrorNode child) => child.children.isNotEmpty);
if (node.level == _DiagnosticsNodeLevel.summary || allChildrenAreLeaf) {
_writeNode(node, recursive: false);
}
}
}
/// A container for output to be sent to the client.
///
/// When multiple lines are being sent, they may be written to the same batch
/// if the output options (error/stackFrame) are the same.
class _BatchedOutput {
_BatchedOutput(this.isError, {this.parseStackFrames = false});
final bool isError;
final bool parseStackFrames;
final StringBuffer _buffer = StringBuffer();
String get output => _buffer.toString();
void writeln(String output) => _buffer.writeln(output);
}
enum _DiagnosticsNodeLevel {
error,
summary,
}
enum _DiagnosticsNodeStyle {
flat,
}
class _ErrorData extends _ErrorNode {
_ErrorData(super.data);
int get errorsSinceReload => data['errorsSinceReload'] as int? ?? 0;
String get renderedErrorText => data['renderedErrorText'] as String? ?? '';
}
class _ErrorNode {
_ErrorNode(this.data);
final Map<Object, Object?> data;
List<_ErrorNode> get children => asList('children', _ErrorNode.new);
String? get description => asString('description');
_DiagnosticsNodeLevel? get level => asEnum('level', _DiagnosticsNodeLevel.values);
String? get name => asString('name');
List<_ErrorNode> get properties => asList('properties', _ErrorNode.new);
bool get showName => data['showName'] != false;
_DiagnosticsNodeStyle? get style => asEnum('style', _DiagnosticsNodeStyle.values);
String? asString(String field) {
final Object? value = data[field];
return value is String ? value : null;
}
T? asEnum<T extends Enum>(String field, Iterable<T> enumValues) {
final String? value = asString(field);
return value != null ? enumValues.asNameMap()[value] : null;
}
List<T> asList<T>(String field, T Function(Map<Object, Object?>) constructor) {
final Object? objects = data[field];
return objects is List && objects.every((Object? element) => element is Map<String, Object?>)
? objects.cast<Map<Object, Object?>>().map(constructor).toList()
: <T>[];
}
}