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