| // 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}); | |
| } |