blob: 651c2d61d25459f07bacff3820ffb2d1f16a0181 [file] [log] [blame]
// Copyright (c) 2016, 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.
library kernel.canonical_name;
import 'ast.dart';
/// A string sequence that identifies a library, class, or member.
///
/// Canonical names are organized in a prefix tree. Each node knows its
/// parent, children, and the AST node it is currently bound to.
///
/// The following schema specifies how the canonical name of a given object
/// is defined:
///
/// Library:
/// URI of library
///
/// Class:
/// Canonical name of enclosing library
/// Name of class
///
/// Extension:
/// Canonical name of enclosing library
/// Name of extension
///
/// Constructor:
/// Canonical name of enclosing class or library
/// "@constructors"
/// Qualified name
///
/// Field:
/// Canonical name of enclosing class or library
/// "@fields"
/// Qualified name
///
/// Implicit getter of a field:
/// Canonical name of enclosing class or library
/// "@getters"
/// Qualified name
///
/// Implicit setter of a field:
/// Canonical name of enclosing class or library
/// "@setters"
/// Qualified name
///
/// Typedef:
/// Canonical name of enclosing class
/// "@typedefs"
/// Name text
///
/// Procedure that is not an accessor or factory:
/// Canonical name of enclosing class or library
/// "@methods"
/// Qualified name
///
/// Procedure that is a getter:
/// Canonical name of enclosing class or library
/// "@getters"
/// Qualified name
///
/// Procedure that is a setter:
/// Canonical name of enclosing class or library
/// "@setters"
/// Qualified name
///
/// Procedure that is a factory:
/// Canonical name of enclosing class
/// "@factories"
/// Qualified name
///
/// Qualified name:
/// if private: URI of library
/// Name text
///
/// The "qualified name" allows a member to have a name that is private to
/// a library other than the one containing that member.
class CanonicalName {
CanonicalName? _parent;
CanonicalName? get parent => _parent;
final String name;
CanonicalName? _nonRootTop;
Map<String, CanonicalName>? _children;
/// The library, class, or member bound to this name.
Reference? _reference;
/// Temporary index used during serialization.
int index = -1;
CanonicalName._(CanonicalName parent, this.name) : _parent = parent {
// ignore: unnecessary_null_comparison
assert(name != null);
// ignore: unnecessary_null_comparison
assert(parent != null);
_nonRootTop = parent.isRoot ? this : parent._nonRootTop;
}
CanonicalName.root()
: _parent = null,
_nonRootTop = null,
name = '';
bool get isRoot => _parent == null;
CanonicalName? get nonRootTop => _nonRootTop;
Iterable<CanonicalName> get children =>
_children?.values ?? const <CanonicalName>[];
Iterable<CanonicalName>? get childrenOrNull => _children?.values;
bool hasChild(String name) {
return _children != null && _children!.containsKey(name);
}
CanonicalName getChild(String name) {
Map<String, CanonicalName> map = _children ??= <String, CanonicalName>{};
return map[name] ??= new CanonicalName._(this, name);
}
CanonicalName getChildFromUri(Uri uri) {
// Note that the Uri class caches its string representation, and all library
// URIs will be stringified for serialization anyway, so there is no
// significant cost for converting the Uri to a string here.
return getChild('$uri');
}
CanonicalName getChildFromQualifiedName(Name name) {
return name.isPrivate
? getChildFromUri(name.library!.importUri).getChild(name.text)
: getChild(name.text);
}
CanonicalName getChildFromProcedure(Procedure procedure) {
return getChild(getProcedureQualifier(procedure))
.getChildFromQualifiedName(procedure.name);
}
CanonicalName getChildFromField(Field field) {
return getChild(fieldsName).getChildFromQualifiedName(field.name);
}
CanonicalName getChildFromFieldGetter(Field field) {
return getChild(gettersName).getChildFromQualifiedName(field.name);
}
CanonicalName getChildFromFieldSetter(Field field) {
return getChild(settersName).getChildFromQualifiedName(field.name);
}
CanonicalName getChildFromConstructor(Constructor constructor) {
return getChild(constructorsName)
.getChildFromQualifiedName(constructor.name);
}
CanonicalName getChildFromRedirectingFactory(
RedirectingFactory redirectingFactory) {
return getChild(factoriesName)
.getChildFromQualifiedName(redirectingFactory.name);
}
CanonicalName getChildFromFieldWithName(Name name) {
return getChild(fieldsName).getChildFromQualifiedName(name);
}
CanonicalName getChildFromFieldGetterWithName(Name name) {
return getChild(gettersName).getChildFromQualifiedName(name);
}
CanonicalName getChildFromFieldSetterWithName(Name name) {
return getChild(settersName).getChildFromQualifiedName(name);
}
CanonicalName getChildFromTypedef(Typedef typedef_) {
return getChild(typedefsName).getChild(typedef_.name);
}
/// Take ownership of a child canonical name and its subtree.
///
/// The child name is removed as a child of its current parent and this name
/// becomes the new parent. Note that this moves the entire subtree rooted at
/// the child.
///
/// This method can be used to move subtrees within a canonical name tree or
/// else move them between trees. It is safe to call this method if the child
/// name is already a child of this name.
///
/// The precondition is that this name cannot have a (different) child with
/// the same name.
void adoptChild(CanonicalName child) {
if (child._parent == this) return;
if (_children != null && _children!.containsKey(child.name)) {
throw 'Cannot add a child to $this because this name already has a '
'child named ${child.name}';
}
child._parent?.removeChild(child.name);
child._parent = this;
_children ??= <String, CanonicalName>{};
_children![child.name] = child;
}
void removeChild(String name) {
if (_children != null) {
_children!.remove(name);
if (_children!.isEmpty) {
_children = null;
}
}
}
void bindTo(Reference target) {
// ignore: unnecessary_null_comparison
if (target == null) {
throw '$this cannot be bound to null';
}
if (_reference == target) return;
if (_reference != null) {
StringBuffer sb = new StringBuffer();
sb.write('$this is already bound to ${_reference}');
if (_reference?._node != null) {
sb.write(' with node ${_reference?._node}'
' (${_reference?._node.runtimeType}'
':${_reference?._node.hashCode})');
}
sb.write(', trying to bind to ${target}');
if (target._node != null) {
sb.write(' with node ${target._node}'
' (${target._node.runtimeType}'
':${target._node.hashCode})');
}
throw sb.toString();
}
if (target.canonicalName != null) {
throw 'Cannot bind $this to ${target.node}, target is already bound to '
'${target.canonicalName}';
}
target.canonicalName = this;
this._reference = target;
}
void unbind() {
_unbindInternal();
// TODO(johnniwinther): To support replacement of fields with getters and
// setters (and the reverse) we need to remove canonical names from the
// canonical name tree. We need to establish better invariants about the
// state of the canonical name tree, since for instance [unbindAll] doesn't
// remove unneeded leaf nodes.
_parent?.removeChild(name);
}
void _unbindInternal() {
if (_reference == null) return;
assert(_reference!.canonicalName == this);
if (_reference!.node is Class) {
// TODO(jensj): Get rid of this. This is only needed because pkg:vm does
// weird stuff in transformations. `unbind` should probably be private.
Class c = _reference!.asClass;
c.ensureLoaded();
}
_reference!.canonicalName = null;
_reference = null;
}
void unbindAll() {
_unbindInternal();
Iterable<CanonicalName>? children_ = childrenOrNull;
if (children_ != null) {
for (CanonicalName child in children_) {
child.unbindAll();
}
}
}
@override
String toString() => _parent == null ? 'root' : '$parent::$name';
String toStringInternal() {
if (isRoot) return "";
if (parent!.isRoot) return "$name";
return "${parent!.toStringInternal()}::$name";
}
Reference get reference {
return _reference ??= (new Reference()..canonicalName = this);
}
void checkCanonicalNameChildren() {
CanonicalName parent = this;
Iterable<CanonicalName>? parentChildren = parent.childrenOrNull;
if (parentChildren != null) {
for (CanonicalName child in parentChildren) {
if (!isSymbolicName(child.name)) {
bool checkReferenceNode = true;
if (child._reference == null) {
// OK for "if private: URI of library" part of "Qualified name"...
// TODO(johnniwinther): This wrongfully skips checking of variable
// synthesized by the VM transformations. The kind of canonical
// name types maybe should be directly available.
if (parent.parent != null && child.name.contains(':')) {
// OK then.
checkReferenceNode = false;
} else {
throw buildCanonicalNameError(
"Null reference (${child.name}) ($child).", child);
}
}
if (checkReferenceNode) {
if (child._reference!.canonicalName != child) {
throw buildCanonicalNameError(
"Canonical name and reference doesn't agree.", child);
}
if (child._reference!.node == null) {
throw buildCanonicalNameError(
"Reference is null (${child.name}) ($child).", child);
}
}
}
child.checkCanonicalNameChildren();
}
}
}
bool get isConsistent {
if (_reference != null && !_reference!.isConsistent) {
return false;
}
return true;
}
String getInconsistency() {
StringBuffer sb = new StringBuffer();
sb.write('CanonicalName ${this} (${hashCode}):');
if (_reference != null) {
sb.write(' ${_reference!.getInconsistency()}');
}
return sb.toString();
}
/// Symbolic name used for the [CanonicalName] node that holds all
/// constructors within a class.
static const String constructorsName = '@constructors';
/// Symbolic name used for the [CanonicalName] node that holds all factories
/// within a class.
static const String factoriesName = '@factories';
/// Symbolic name used for the [CanonicalName] node that holds all methods
/// within a library or a class.
static const String methodsName = '@methods';
/// Symbolic name used for the [CanonicalName] node that holds all fields
/// within a library or class.
static const String fieldsName = '@fields';
/// Symbolic name used for the [CanonicalName] node that holds all getters and
/// readable fields within a library or class.
static const String gettersName = '@getters';
/// Symbolic name used for the [CanonicalName] node that holds all setters and
/// writable fields within a library or class.
static const String settersName = '@setters';
/// Symbolic name used for the [CanonicalName] node that holds all typedefs
/// within a library.
static const String typedefsName = '@typedefs';
static const Set<String> symbolicNames = {
constructorsName,
factoriesName,
methodsName,
fieldsName,
gettersName,
settersName,
typedefsName,
};
static bool isSymbolicName(String name) => symbolicNames.contains(name);
static String getProcedureQualifier(Procedure procedure) {
if (procedure.isGetter) return gettersName;
if (procedure.isSetter) return settersName;
if (procedure.isFactory) return factoriesName;
return methodsName;
}
/// Returns `true` if [node] is orphaned through its [reference].
///
/// A [NamedNode] is orphaned if the canonical name of its reference doesn't
/// point back to the node itself. This can occur if the [reference] is
/// repurposed for a new [NamedNode]. In this case, the reference will be
/// updated to point the new node.
///
/// This method assumes that `reference.canonicalName` is this canonical name.
bool isOrphaned(NamedNode node, Reference reference) {
assert(reference.canonicalName == this);
return _reference?._node != node;
}
/// Returns a description of the orphancy, if [node] is orphaned through its
/// [reference]. Otherwise `null`.
///
/// A [NamedNode] is orphaned if the canonical name of its reference doesn't
/// point back to the node itself. This can occur if the [reference] is
/// repurposed for a new [NamedNode]. In this case, the reference will be
/// updated to point the new node.
///
/// This method assumes that `reference.canonicalName` is this canonical name.
String? getOrphancyDescription(NamedNode node, Reference reference) {
assert(reference.canonicalName == this);
if (_reference?._node != node) {
return _reference!.getOrphancyDescription(node);
}
return null;
}
}
/// Indirection between a reference and its definition.
///
/// There is only one reference object per [NamedNode].
class Reference {
CanonicalName? canonicalName;
NamedNode? _node;
NamedNode? get node {
if (_node == null) {
// Either this is an unbound reference or it belongs to a lazy-loaded
// (and not yet loaded) class. If it belongs to a lazy-loaded class,
// load the class.
CanonicalName? canonicalNameParent = canonicalName?.parent;
while (canonicalNameParent != null) {
if (canonicalNameParent.name.startsWith("@")) {
break;
}
canonicalNameParent = canonicalNameParent.parent;
}
if (canonicalNameParent != null) {
NamedNode? parentNamedNode =
canonicalNameParent.parent?.reference._node;
if (parentNamedNode is Class) {
Class parentClass = parentNamedNode;
if (parentClass.lazyBuilder != null) {
parentClass.ensureLoaded();
}
}
}
}
return _node;
}
void set node(NamedNode? node) {
_node = node;
}
@override
String toString() {
return "Reference to ${toStringInternal()}";
}
String toStringInternal() {
if (canonicalName != null) {
return '${canonicalName!.toStringInternal()}';
}
if (node != null) {
return node!.toStringInternal();
}
return 'Unbound reference';
}
Library get asLibrary {
if (node == null) {
throw '$this is not bound to an AST node. A library was expected';
}
return node as Library;
}
Class get asClass {
if (node == null) {
throw '$this is not bound to an AST node. A class was expected';
}
return node as Class;
}
Member get asMember {
if (node == null) {
throw '$this is not bound to an AST node. A member was expected';
}
return node as Member;
}
Field get asField {
if (node == null) {
throw '$this is not bound to an AST node. A field was expected';
}
return node as Field;
}
Constructor get asConstructor {
if (node == null) {
throw '$this is not bound to an AST node. A constructor was expected';
}
return node as Constructor;
}
Procedure get asProcedure {
if (node == null) {
throw '$this is not bound to an AST node. A procedure was expected';
}
return node as Procedure;
}
Typedef get asTypedef {
if (node == null) {
throw '$this is not bound to an AST node. A typedef was expected';
}
return node as Typedef;
}
Extension get asExtension {
if (node == null) {
throw '$this is not bound to an AST node. An extension was expected';
}
return node as Extension;
}
bool get isConsistent {
NamedNode? node = _node;
if (node != null) {
if (node is Field) {
// The field, getter or setter reference of the [Field] must point to
// this reference.
return node.fieldReference == this ||
node.getterReference == this ||
node.setterReference == this;
} else {
// The reference of the [NamedNode] must point to this reference.
return node.reference == this;
}
}
if (canonicalName != null && canonicalName!._reference != this) {
return false;
}
return true;
}
String getInconsistency() {
StringBuffer sb = new StringBuffer();
sb.write('Reference ${toStringInternal()} (${hashCode}):');
NamedNode? node = _node;
if (node != null) {
if (node is Field) {
if (node.fieldReference != this &&
node.getterReference != this &&
node.setterReference != this) {
sb.write(' _node=${node} (${node.runtimeType}:${node.hashCode})');
sb.write(' _node.fieldReference='
'${node.fieldReference} (${node.fieldReference.hashCode})');
sb.write(' _node.getterReference='
'${node.getterReference} (${node.getterReference.hashCode})');
sb.write(' _node.setterReference='
'${node.setterReference} (${node.setterReference.hashCode})');
}
} else {
if (node.reference != this) {
sb.write(' _node=${node} (${node.runtimeType}:${node.hashCode})');
sb.write(' _node.reference='
'${node.reference} (${node.reference.hashCode})');
}
}
}
if (canonicalName != null && canonicalName!._reference != this) {
sb.write(' canonicalName=${canonicalName} (${canonicalName.hashCode})');
sb.write(' canonicalName.reference='
'${canonicalName!._reference} '
'(${canonicalName!._reference.hashCode})');
}
return sb.toString();
}
/// Returns `true` if [node] is orphaned through this reference.
///
/// A [NamedNode] is orphaned if its reference doesn't point back to the node
/// itself. This can occur if the [reference] is repurposed for a new
/// [NamedNode]. In this case, the reference will be updated to point the new
/// node.
///
/// This method assumes that this reference is the reference, possibly
/// getter or setter reference for a field, of [node].
bool isOrphaned(NamedNode node) {
return _node != node;
}
/// Returns a description of the orphancy, if [node] is orphaned through this
/// reference. Otherwise `null`.
///
/// A [NamedNode] is orphaned if its reference doesn't point back to the node
/// itself. This can occur if the [reference] is repurposed for a new
/// [NamedNode]. In this case, the reference will be updated to point the new
/// node.
///
/// This method assumes that this reference is the reference, possibly
/// getter or setter reference for a field, of [node].
String? getOrphancyDescription(NamedNode node) {
if (_node != node) {
StringBuffer sb = new StringBuffer();
sb.write('Orphaned named node ${node} ');
sb.write('(${node.runtimeType}:${node.hashCode})\n');
sb.write('Linked node ${_node} ');
sb.write('(${_node.runtimeType}:');
sb.write('${_node.hashCode})');
return sb.toString();
}
return null;
}
}
class CanonicalNameError {
final String message;
CanonicalNameError(this.message);
@override
String toString() => 'CanonicalNameError: $message';
}
class CanonicalNameSdkError extends CanonicalNameError {
CanonicalNameSdkError(String message) : super(message);
@override
String toString() => 'CanonicalNameSdkError: $message';
}
CanonicalNameError buildCanonicalNameError(
String message, CanonicalName problemNode) {
// Special-case missing sdk entries as that is probably a change to the
// platform - that's something we might want to react differently to.
String libraryUri = problemNode.nonRootTop?.name ?? "";
if (libraryUri.startsWith("dart:")) {
return new CanonicalNameSdkError(message);
}
return new CanonicalNameError(message);
}