// 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 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 errorData) { final data = _ErrorData(errorData); const assumedTerminalSize = 80; const barChar = '═'; final String headerPrefix = barChar * 8; final String headerSuffix = barChar * math.max( assumedTerminalSize - (data.description?.length ?? 0) - 2 - headerPrefix.length, 0, ); final 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 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 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) { // DiagnosticsBlock is a container, so recurse into its children if // there's only a single level. The container may be // "The relevant error-causing widget was" and the child may be // the specific widget details. _writeNode( node, recursive: node.type == _DiagnosticsNodeType.DiagnosticsBlock && allChildrenAreLeaf, ); } } } /// 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 _buffer = StringBuffer(); String get output => _buffer.toString(); void writeln(String output) => _buffer.writeln(output); } enum _DiagnosticsNodeLevel { error, summary } enum _DiagnosticsNodeStyle { flat } enum _DiagnosticsNodeType { DiagnosticsBlock } 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 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); _DiagnosticsNodeType? get type => asEnum('type', _DiagnosticsNodeType.values); String? asString(String field) { final Object? value = data[field]; return value is String ? value : null; } T? asEnum(String field, Iterable enumValues) { final String? value = asString(field); return value != null ? enumValues.asNameMap()[value] : null; } List asList(String field, T Function(Map) constructor) { final Object? objects = data[field]; return objects is List && objects.every((Object? element) => element is Map) ? objects.cast>().map(constructor).toList() : []; } }