blob: f246d35bd1f19830a541969ee7f72a13b911dbf1 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:mirrors';
import '../common_util.dart';
import '../queries/index.dart';
import 'ast.dart';
/// A renderer into tab-separated values format. Since TSV is inherently 2D,
/// this renderer will truncate any nodes deeper than one level.
class TsvRenderer extends Renderer {
@override
void render(StringSink output, Iterable<Query> queries) {
if (queries.length != 1) {
throw Exception('TSV output only supports rendering a single query.\n'
'Received: ${queries.map((q) => q.name).join(', ')}');
}
final result = queries.first.distill().export();
_renderTsv(output, result);
}
/// Prints the first N items in `nodes` that are of the same type,
/// using runtime reflection to query fields.
void _renderTsv(StringSink output, Iterable<AnyNode> nodes) {
// Query the concrete type `Node<T>` from `AnyNode`.
final ClassMirror nodeType = reflect(nodes.first).type;
final List<TypeMirror> typeArguments = nodeType.typeArguments;
if (nodeType.simpleName != #Node ||
typeArguments.length != 1 ||
!(typeArguments[0] is ClassMirror)) {
throw Exception('${nodes.first} should be of the form Node<T>.');
}
// `titleType` will be `SizeRecord`, `UniqueSymbolSizeRecord`, etc.
final ClassMirror titleType = typeArguments[0];
final fieldDescriptors = _buildFieldDescriptors(titleType);
// Render TSV header
output.writeln(
fieldDescriptors.map((desc) => _ensureNoTab(desc.name)).join('\t'));
for (final node in nodes) {
// Stop when encountering a different type down the list, since a TSV
// consists of N rows of homogenous types.
if (reflect(node).type != nodeType) break;
// Render one row
final row = fieldDescriptors.map((desc) {
Object fieldContent = desc.lens(node.title);
return _ensureNoTab(_stripStyle(fieldContent));
}).join('\t');
output.writeln(row);
}
}
/// Since TSV is un-styled plain text, this function strips any coloring from
/// the text.
String _stripStyle(Object field) {
if (field is AddColor) {
return field.details.map(_stripStyle).join('');
} else if (field is StyledString) {
return field.details.map(_stripStyle).join('');
} else if (field is Plain) {
return field.text;
} else if (field is List) {
return field.map(_stripStyle).join(' ');
} else {
return field.toString();
}
}
/// TSV spec requires escaping `\t` (and consequently slashes as well).
/// Since our symbols are unlikely to contain the tab character,
/// simply asserting the output does not have tab should be good enough.
String _ensureNoTab(String input) {
if (input.contains('\t')) {
throw Exception(
'Escaping the tab character in TSV contents is not supported');
}
return input;
}
/// Given the mirror of class `type`, returns a list of field descriptors
/// corresponding to columns of the TSV.
List<_FieldDescriptor> _buildFieldDescriptors(ClassMirror type,
{Object Function(Object root) lens = _id}) =>
[
// Superclass fields first...
if (type.superclass != null)
..._buildFieldDescriptors(type.superclass, lens: lens),
// Followed by our fields...
for (final decl in type.declarations.entries)
if (decl.value is VariableMirror)
// Expand `Tally` into three fields (size, raw size, num symbols)
// ignore: avoid_as
if ((decl.value as VariableMirror).type.simpleName == #Tally)
// Use a single-element for-loop to introduce a local `tallyLens`
// name inside nested expressions.
for (final tallyLens in [
(x) => reflect(lens(x)).getField(decl.key).reflectee
]) ...[
_FieldDescriptor(
name: 'Size', lens: (x) => formatSize(tallyLens(x).size)),
_FieldDescriptor(
name: 'Raw Size', lens: (x) => tallyLens(x).size),
_FieldDescriptor(
name: 'Num Symbols', lens: (x) => tallyLens(x).count),
]
else
// Note: we are assuming that all fields that is not `Tally` can
// be directly printed through `_stripStyle` (e.g. StyledString,
// int, String, etc). When some QueryReport start using classes,
// change this to recursively descend into that class and build
// the field descriptors there.
_FieldDescriptor(
name: _formatTsvTitle(MirrorSystem.getName(decl.key)),
lens: (x) => reflect(lens(x)).getField(decl.key).reflectee)
];
/// Convert `camelCase` into `Title Case`.
String _formatTsvTitle(String name) {
String upperCaseFirst(String x) => '${x[0].toUpperCase()}${x.substring(1)}';
return upperCaseFirst(name).replaceAllMapped(
_camelRegex, (Match m) => ' ${upperCaseFirst(m.group(0))}');
}
final RegExp _camelRegex = RegExp(r'(?<=[a-z])[A-Z]');
static Object _id(Object x) => x;
}
/// The name of a field (TSV column), and an accessor to extract the value of
/// this field given an node object representing a row.
class _FieldDescriptor {
final String name;
final Object Function(Object root) lens;
_FieldDescriptor({this.name, this.lens});
}