// 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 or the 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? _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 get children => _children?.values ?? const []; Iterable? get childrenOrNull => _children?.values; bool hasChild(String name) { return _children != null && _children!.containsKey(name); } CanonicalName getChild(String name) { Map map = _children ??= {}; 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('@getters').getChildFromQualifiedName(field.name); } CanonicalName getChildFromFieldSetter(Field field) { return getChild('@setters').getChildFromQualifiedName(field.name); } CanonicalName getChildFromConstructor(Constructor constructor) { return getChild('@constructors') .getChildFromQualifiedName(constructor.name); } CanonicalName getChildFromRedirectingFactoryConstructor( RedirectingFactoryConstructor redirectingFactoryConstructor) { return getChild('@factories') .getChildFromQualifiedName(redirectingFactoryConstructor.name); } CanonicalName getChildFromFieldWithName(Name name) { return getChild('@getters').getChildFromQualifiedName(name); } CanonicalName getChildFromFieldSetterWithName(Name name) { return getChild('@setters').getChildFromQualifiedName(name); } CanonicalName getChildFromTypedef(Typedef typedef_) { return getChild('@typedefs').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 ??= {}; _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? children_ = childrenOrNull; if (children_ != null) { for (CanonicalName child in children_) { child.unbindAll(); } } } 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? parentChildren = parent.childrenOrNull; if (parentChildren != null) { for (CanonicalName child in parentChildren) { if (child.name != '@methods' && child.name != '@typedefs' && child.name != '@fields' && child.name != '@=fields' && child.name != '@getters' && child.name != '@setters' && child.name != '@factories' && child.name != '@constructors') { 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(); } static String getProcedureQualifier(Procedure procedure) { if (procedure.isGetter) return '@getters'; if (procedure.isSetter) return '@setters'; if (procedure.isFactory) return '@factories'; return '@methods'; } /// 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; } 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.reference != this && (node is! Field || node.setterReference != this)) { // The reference of a [NamedNode] must point to this reference, or // if the node is a [Field] the setter reference must point to this // reference. return false; } } if (canonicalName != null && canonicalName!._reference != this) { return false; } return true; } String getInconsistency() { StringBuffer sb = new StringBuffer(); sb.write('Reference ${this} (${hashCode}):'); NamedNode? node = _node; if (node != null) { if (node is Field) { if (node.getterReference != this && node.setterReference != this) { sb.write(' _node=${node} (${node.runtimeType}:${node.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); String toString() => 'CanonicalNameError: $message'; } class CanonicalNameSdkError extends CanonicalNameError { CanonicalNameSdkError(String message) : super(message); 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); }