blob: 5d685ee183aa6f1640d7e43154d2c7cbe03bb94b [file] [log] [blame]
// Copyright (c) 2011, 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.
// @dart = 2.10
part of js_backend.namer;
/// Assigns JavaScript identifiers to Dart variables, class-names and members.
class MinifyNamer extends Namer
with
_MinifiedFieldNamer,
_MinifyConstructorBodyNamer,
_MinifiedOneShotInterceptorNamer {
MinifyNamer(JClosedWorld closedWorld, FixedNames fixedNames)
: super(closedWorld, fixedNames) {
reserveBackendNames();
fieldRegistry = _FieldNamingRegistry(this);
}
@override
_FieldNamingRegistry fieldRegistry;
@override
String get genericInstantiationPrefix => r'$I';
final ALPHABET_CHARACTERS = 52; // a-zA-Z.
final ALPHANUMERIC_CHARACTERS = 62; // a-zA-Z0-9.
/// You can pass an invalid identifier to this and unlike its non-minifying
/// counterpart it will never return the proposedName as the new fresh name.
///
/// [sanitizeForNatives] and [sanitizeForAnnotations] are ignored because the
/// minified names will always avoid clashing with annotated names or natives.
@override
String _generateFreshStringForName(String proposedName, NamingScope scope,
{bool sanitizeForNatives = false, bool sanitizeForAnnotations = false}) {
String freshName;
String suggestion = scope.suggestName(proposedName);
if (suggestion != null && scope.isUnused(suggestion)) {
freshName = suggestion;
} else {
freshName = _getUnusedName(proposedName, scope);
}
scope.registerUse(freshName);
return freshName;
}
/// Property names that are used on 'native' JavaScript classes.
///
/// It is best not to use these names for dynamic call selectors.
///
/// Dynamic calls to selectors that are used on an intercepted (e.g. String)
/// class (e.g. String) or native class (e.g. HtmlElement) are not a problem
/// since they are handled via the interceptor calling mechanism. Other
/// selectors are called directly, so the minified selector should not be a
/// name that accidentally collides with a JavaScript method. For example,
/// the minified call site for the dynamic call `item.yowza()` should not be
/// `x.at()`, since, if `x` was a String, that would call
/// `String.prototype.at` instead of generating an error.
///
/// These names are reserved for the property names of fields, since fields
/// are always accessed from a know receiver. (A 'field' of an unknown
/// receiver would actually be the getter method.)
///
/// These names could be used for selectors that use the interceptor calling
/// convention, or for selectors are not called from a `dynamic` call site, or
/// any other use where the JavaScript receiver is constrained to be something
/// from within the dart program.
///
/// TODO(7554): Refresh periodically.
static const List<String> _reservedNativeProperties = [
// 1 character
'a', 'b', 'c', 'd', 'e', 'f', 'r', 'w', 'x', 'y', 'z', 'Q',
// 2 characters
'as', 'at', 'ax', 'ay', 'ch', 'CW', 'cx', 'cy', 'db', 'dx', 'dy', 'fr',
'fx', 'fy', 'go', 'id', 'k1', 'k2', 'k3', 'k4', 'ok', 'p1', 'p2', 'p3',
'p4', 'R8', 'RG', 'rx', 'ry', 'to', 'x1', 'x2', 'xr', 'y1', 'y2',
// 3 characters
'add', 'all', 'alt', 'arc', 'big', 'CCW', 'cmp', 'csp', 'dir', 'div', 'end',
'eye', 'get', 'has', 'hid', 'in1', 'in2', 'ink', 'INT', 'key', 'low', 'm11',
'm12', 'm13', 'm14', 'm21', 'm22', 'm23', 'm24', 'm31', 'm32', 'm33', 'm34',
'm41', 'm42', 'm43', 'm44', 'map', 'max', 'MAX', 'mid', 'min', 'MIN', 'mul',
'now', 'ONE', 'pad', 'pan', 'pop', 'put', 'R8I', 'RED', 'rel', 'rev', 'RG8',
'RGB', 'rtt', 'sdp', 'set', 'src', 'sub', 'sup', 'tag', 'tee', 'top', 'upX',
'upY', 'upZ', 'url', 'URL', 'usb',
// 4 characters
'abbr', 'axes', 'axis', 'back', 'BACK', 'beta', 'bias', 'bind', 'blob',
'blur', 'body', 'bold', 'BOOL', 'BYTE', 'call', 'cite', 'city', 'clip',
'code', 'cols', 'cues', 'data', 'DECR', 'DONE', 'dtmf', 'exec', 'face',
'fill', 'find', 'flat', 'font', 'form', 'gain', 'hash', 'head', 'hide',
'high', 'hint', 'host', 'href', 'icon', 'INCR', 'is2D', 'item', 'join',
'json', 'KEEP', 'keys', 'kind', 'knee', 'lang', 'left', 'LESS', 'line',
'link', 'list', 'load', 'lock', 'loop', 'mark', 'mode', 'name', 'node',
'NONE', 'open', 'OPEN', 'part', 'path', 'ping', 'play', 'port', 'push',
'R16F', 'R16I', 'R32F', 'R32I', 'R8UI', 'rate', 'read', 'rect', 'refX',
'refY', 'RG8I', 'RGB8', 'RGBA', 'role', 'root', 'rows', 'save', 'sctp',
'seed', 'seek', 'send', 'show', 'sign', 'size', 'slot', 'some', 'sort',
'span', 'SRGB', 'step', 'stop', 'sync', 'test', 'text', 'then', 'time',
'tone', 'trim', 'type', 'unit', 'view', 'wrap', 'ZERO',
];
void reserveBackendNames() {
for (String name in _reservedNativeProperties) {
if (name.length < 2) {
// Ensure the 1-letter names are disambiguated to the same name.
String disambiguatedName = name;
reservePublicMemberName(name, disambiguatedName);
}
instanceScope.registerUse(name);
// Getter and setter names are autogenerated by prepending 'g' and 's' to
// field names. Therefore there are some field names we don't want to
// use. It is implicit in the next line that the banned prefix is
// only one character.
if (_hasBannedPrefix(name)) {
instanceScope.registerUse(name.substring(1));
}
}
// These popular names are present in most programs and deserve
// single character minified names. We could determine the popular names
// individually per program, but that would mean that the output of the
// minifier was less stable from version to version of the program being
// minified.
_populateSuggestedNames(instanceScope, const [
r'$add',
r'add$1',
r'$and',
r'$or',
r'current',
r'$shr',
r'$eq',
r'$ne',
r'$index',
r'$indexSet',
r'$xor',
r'clone$0',
r'iterator',
r'length',
r'$lt',
r'$gt',
r'$le',
r'$ge',
r'moveNext$0',
r'node',
r'on',
r'$negate',
r'push',
r'self',
r'start',
r'target',
r'$shl',
r'value',
r'width',
r'style',
r'noSuchMethod$1',
r'$mul',
r'$div',
r'$sub',
r'$not',
r'$mod',
r'$tdiv',
r'toString$0'
]);
_populateSuggestedNames(globalScope, const [
r'Object',
'wrapException',
r'$eq',
r'S',
r'ioore',
r'UnsupportedError$',
r'length',
r'$sub',
r'$add',
r'$gt',
r'$ge',
r'$lt',
r'$le',
r'add',
r'iae',
r'ArgumentError$',
r'BoundClosure',
r'Closure',
r'StateError$',
r'getInterceptor$',
r'max',
r'$mul',
r'Map',
r'Key_Key',
r'$div',
r'List_List$from',
r'LinkedHashMap_LinkedHashMap$_empty',
r'LinkedHashMap_LinkedHashMap$_literal',
r'min',
r'RangeError$value',
r'JSString',
r'JSNumber',
r'JSArray',
r'createInvocationMirror',
r'String',
r'setArrayType',
r'createRuntimeType'
]);
}
void _populateSuggestedNames(NamingScope scope, List<String> suggestions) {
int c = $a - 1;
String letter;
for (String name in suggestions) {
do {
assert(c != $Z);
c = (c == $z) ? $A : c + 1;
letter = String.fromCharCodes([c]);
} while (_hasBannedPrefix(letter) || scope.isUsed(letter));
assert(!scope.hasSuggestion(name));
scope.addSuggestion(name, letter);
}
}
// This gets a minified name based on a hash of the proposed name. This
// is slightly less efficient than just getting the next name in a series,
// but it means that small changes in the input program will give smallish
// changes in the output, which can be useful for diffing etc.
String _getUnusedName(String proposedName, NamingScope scope) {
int hash = _calculateHash(proposedName);
// Avoid very small hashes that won't try many names.
hash = hash < 1000 ? hash * 314159 : hash; // Yes, it's prime.
// Try other n-character names based on the hash. We try one to three
// character identifiers. For each length we try around 10 different names
// in a predictable order determined by the proposed name. This is in order
// to make the renamer stable: small changes in the input should nornally
// result in relatively small changes in the output.
for (int n = 1; n <= 3; n++) {
int h = hash;
while (h > 10) {
List<int> codes = [_letterNumber(h)];
int h2 = h ~/ ALPHABET_CHARACTERS;
for (int i = 1; i < n; i++) {
codes.add(_alphaNumericNumber(h2));
h2 ~/= ALPHANUMERIC_CHARACTERS;
}
final candidate = String.fromCharCodes(codes);
if (scope.isUnused(candidate) &&
!jsReserved.contains(candidate) &&
!_hasBannedPrefix(candidate) &&
(n != 1 || scope.isSuggestion(candidate))) {
return candidate;
}
// Try again with a slightly different hash. After around 10 turns
// around this loop h is zero and we try a longer name.
h ~/= 7;
}
}
return _badName(hash, scope);
}
/// Instance members starting with g and s are reserved for getters and
/// setters.
static bool _hasBannedPrefix(String name) {
int code = name.codeUnitAt(0);
return code == $g || code == $s;
}
int _calculateHash(String name) {
int h = 0;
for (int i = 0; i < name.length; i++) {
h += name.codeUnitAt(i);
h &= 0xffffffff;
h += h << 10;
h &= 0xffffffff;
h ^= h >> 6;
h &= 0xffffffff;
}
return h;
}
/// Remember bad hashes to avoid using a the same character with long numbers
/// for frequent hashes. For example, `closure` is a very common name.
final Map<int, int> _badNames = {};
/// If we can't find a hash based name in the three-letter space, then base
/// the name on a letter and a counter.
String _badName(int hash, NamingScope scope) {
int count = _badNames.putIfAbsent(hash, () => 0);
String startLetter = String.fromCharCodes([_letterNumber(hash + count)]);
_badNames[hash] = count + 1;
String name;
int i = 0;
do {
name = "$startLetter${i++}";
} while (scope.isUsed(name));
// We don't need to check for banned prefix because the name is in the form
// xnnn, where nnn is a number. There can be no getter or setter called
// gnnn since that would imply a numeric field name.
return name;
}
int _letterNumber(int x) {
if (x >= ALPHABET_CHARACTERS) x %= ALPHABET_CHARACTERS;
if (x < 26) return $a + x;
return $A + x - 26;
}
int _alphaNumericNumber(int x) {
if (x >= ALPHANUMERIC_CHARACTERS) x %= ALPHANUMERIC_CHARACTERS;
if (x < 26) return $a + x;
if (x < 52) return $A + x - 26;
return $0 + x - 52;
}
@override
jsAst.Name instanceFieldPropertyName(FieldEntity element) {
jsAst.Name proposed = _minifiedInstanceFieldPropertyName(element);
if (proposed != null) {
return proposed;
}
return super.instanceFieldPropertyName(element);
}
}
/// Implements naming for constructor bodies.
///
/// Constructor bodies are only called in settings where the target is
/// statically known. Therefore, we can share their names between classes.
/// However, to support calling the constructor body of a super constructor,
/// each level in the inheritance tree has to use its own names.
///
/// This class implements a naming scheme by counting the distance from
/// a given constructor to [Object], where distance is the number of
/// constructors declared along the inheritance chain.
class _ConstructorBodyNamingScope {
final int _startIndex;
final List<ConstructorEntity> _constructors;
int get numberOfConstructors => _constructors.length;
_ConstructorBodyNamingScope.rootScope(
ClassEntity cls, ElementEnvironment environment)
: _startIndex = 0,
_constructors = _getConstructorList(cls, environment);
_ConstructorBodyNamingScope.forClass(ClassEntity cls,
_ConstructorBodyNamingScope superScope, ElementEnvironment environment)
: _startIndex = superScope._startIndex + superScope.numberOfConstructors,
_constructors = _getConstructorList(cls, environment);
// Mixin Applications have constructors but we never generate code for them,
// so they do not count in the inheritance chain.
_ConstructorBodyNamingScope.forMixinApplication(
ClassEntity cls, _ConstructorBodyNamingScope superScope)
: _startIndex = superScope._startIndex + superScope.numberOfConstructors,
_constructors = const [];
factory _ConstructorBodyNamingScope(
ClassEntity cls,
Map<ClassEntity, _ConstructorBodyNamingScope> registry,
ElementEnvironment environment) {
return registry.putIfAbsent(cls, () {
ClassEntity superclass = environment.getSuperClass(cls);
if (superclass == null) {
return _ConstructorBodyNamingScope.rootScope(cls, environment);
} else if (environment.isMixinApplication(cls)) {
return _ConstructorBodyNamingScope.forMixinApplication(cls,
_ConstructorBodyNamingScope(superclass, registry, environment));
} else {
return _ConstructorBodyNamingScope.forClass(
cls,
_ConstructorBodyNamingScope(superclass, registry, environment),
environment);
}
});
}
String constructorBodyKeyFor(ConstructorBodyEntity body) {
int position = _constructors.indexOf(body.constructor);
assert(position >= 0, failedAt(body, "constructor body missing"));
return "@constructorBody@${_startIndex + position}";
}
static List<ConstructorEntity> _getConstructorList(
ClassEntity cls, ElementEnvironment environment) {
var result = <ConstructorEntity>[];
environment.forEachConstructor(cls, result.add);
return result;
}
}
abstract class _MinifyConstructorBodyNamer implements Namer {
final Map<ClassEntity, _ConstructorBodyNamingScope> _constructorBodyScopes =
{};
@override
jsAst.Name constructorBodyName(ConstructorBodyEntity method) {
_ConstructorBodyNamingScope scope = _ConstructorBodyNamingScope(
method.enclosingClass, _constructorBodyScopes, _elementEnvironment);
String key = scope.constructorBodyKeyFor(method);
return _disambiguateMemberByKey(
key, () => _proposeNameForConstructorBody(method));
}
}
abstract class _MinifiedOneShotInterceptorNamer implements Namer {
/// Property name used for the one-shot interceptor method for the given
/// [selector] and return-type specialization.
@override
jsAst.Name nameForOneShotInterceptor(
Selector selector, Iterable<ClassEntity> classes) {
String root = selector.isOperator
? Namer.operatorNameToIdentifier(selector.name)
: privateName(selector.memberName);
String prefix = selector.isGetter
? r"$get"
: selector.isSetter
? r"$set"
: "";
String callSuffix = selector.isCall
? Namer.callSuffixForStructure(selector.callStructure).join()
: "";
String suffix =
suffixForGetInterceptor(_commonElements, _nativeData, classes);
String fullName = "\$intercepted$prefix\$$root$callSuffix\$$suffix";
return _disambiguateInternalGlobal(fullName);
}
}