| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| // ignore: implementation_imports | |
| import 'package:front_end/src/api_unstable/dart2js.dart' | |
| show $0, $9, $A, $Z, $a, $z; | |
| /// Returns a list of strings that are short valid identifiers based on the | |
| /// corresponding full strings in [strings]. | |
| /// | |
| /// [strings] must not contain the empty string. | |
| /// [strings] must have no duplicates. | |
| List<String> abbreviateToIdentifiers( | |
| Iterable<String> strings, { | |
| int minLength = 6, | |
| }) { | |
| var nodes = [for (final string in strings) _Node(string)]; | |
| _partition(nodes, minLength, [], 0); | |
| return [for (final node in nodes) node.assignment]; | |
| } | |
| class _Node { | |
| final String string; | |
| late String assignment; | |
| _Node(this.string); | |
| } | |
| /// Walk the prefix tree or TRIE of [nodes], assigning compressed names built | |
| /// from the distinguishing characters along the path. | |
| /// | |
| /// - [index] is the position of the first potentially different character, | |
| /// i.e. the current TRIE depth. | |
| /// - [path] contains the prefix of the compressed name at this depth. | |
| /// - Path compression starts after the first [minLength] characters. | |
| void _partition( | |
| List<_Node> nodes, | |
| int minLength, | |
| List<String> path, | |
| int index, | |
| ) { | |
| while (true) { | |
| // Handle trivial partitions. | |
| if (nodes.isEmpty) return; | |
| if (nodes.length == 1 && path.length >= minLength) { | |
| String name = path.join(); | |
| assert(name.isNotEmpty); | |
| nodes.single.assignment = name; | |
| return; | |
| } | |
| // Partition on the code unit at position [index], setting [terminating] if | |
| // some string ends at this length; | |
| Map<int, List<_Node>> partition = {}; | |
| _Node? terminating; | |
| for (final node in nodes) { | |
| String string = node.string; | |
| assert(string.isNotEmpty); | |
| if (index < string.length) { | |
| int codeUnit = string.codeUnitAt(index); | |
| (partition[codeUnit] ??= []).add(node); | |
| } else { | |
| assert(terminating == null); // i.e. no duplicates. | |
| terminating = node; | |
| } | |
| } | |
| if (terminating != null) { | |
| terminating.assignment = path.join(); | |
| } | |
| if (partition.isEmpty) return; | |
| if (partition.length > 1) { | |
| var keys = partition.keys.toList(); | |
| var keyEncodings = _discriminators(keys, path.isEmpty); | |
| for (int key in keys) { | |
| var children = partition[key]!; | |
| var discriminator = keyEncodings[key]!; | |
| _partition(children, minLength, [...path, discriminator], index + 1); | |
| } | |
| return; | |
| } | |
| assert(partition.length == 1); | |
| // All the strings have the same code unit at [index]. Use iteration to | |
| // find next partition point to avoid recursing down the whole string | |
| // length. The path is compressed by omitting to add fragments to [path] | |
| // unless we are near the start of the string. | |
| int codeUnit = partition.keys.single; | |
| // Add some characters to name to distinguish from terminating strings. | |
| // Add the first few legal identifier characters of the string regardless. | |
| if (terminating != null || path.length < minLength) { | |
| path.add( | |
| _isIdentifier(codeUnit, path.isEmpty) | |
| ? String.fromCharCode(codeUnit) | |
| : '_', | |
| ); | |
| } | |
| nodes = partition.values.single; | |
| index += 1; | |
| } | |
| } | |
| Map<int, String> _discriminators(List<int> keys, bool atStart) { | |
| // Assign each partition a distinguishing short string. If the partition key | |
| // is a valid identifier character, it can be used, otherwise we could use | |
| // `'_'` or an escaped code like `'x3b'` or `'u12ef'`. If we use an escape | |
| // like `'x3b'` then we need to be careful with the partition key `x`, as it | |
| // might be followed by `3b`. We avoid this problem without lookahead by | |
| // encoding `x` as an escape (i.e. `'x78'`) if there is another `x`-escape. | |
| const xCode = 0x78; | |
| const uCode = 0x75; | |
| bool hasX = false; | |
| bool hasU = false; | |
| int xEscapes = 0; | |
| int uEscapes = 0; | |
| for (int key in keys) { | |
| if (_isIdentifier(key, atStart)) { | |
| if (key == xCode) hasX = true; | |
| if (key == uCode) hasU = true; | |
| } else if (key < 256) { | |
| xEscapes++; | |
| } else { | |
| uEscapes++; | |
| } | |
| } | |
| Map<int, String> encoding = {}; | |
| bool escapeToUnderscore = false; | |
| if (uEscapes + xEscapes <= 1) { | |
| escapeToUnderscore = true; | |
| xEscapes = uEscapes = 0; | |
| } | |
| if (uEscapes > 0 && hasU) { | |
| encoding[uCode] = 'x75'; | |
| xEscapes++; | |
| } | |
| if (xEscapes > 0 && hasX) { | |
| encoding[xCode] = 'x78'; | |
| } | |
| for (int key in keys) { | |
| if (encoding.containsKey(key)) continue; | |
| if (_isIdentifier(key, atStart)) { | |
| encoding[key] = String.fromCharCode(key); | |
| } else { | |
| if (escapeToUnderscore) { | |
| encoding[key] = '_'; | |
| } else if (key < 256) { | |
| encoding[key] = 'x${key.toRadixString(16).padLeft(2, '0')}'; | |
| } else { | |
| encoding[key] = 'u${key.toRadixString(16).padLeft(4, '0')}'; | |
| } | |
| } | |
| } | |
| return encoding; | |
| } | |
| bool _isIdentifier(int codeUnit, bool atStart) { | |
| return atStart ? _isAsciiAlpha(codeUnit) : _isAsciiAlphanumeric(codeUnit); | |
| } | |
| bool _isAsciiAlphanumeric(int codeUnit) { | |
| return $0 <= codeUnit && codeUnit <= $9 || _isAsciiAlpha(codeUnit); | |
| } | |
| bool _isAsciiAlpha(int codeUnit) { | |
| return $A <= codeUnit && codeUnit <= $Z || $a <= codeUnit && codeUnit <= $z; | |
| } |