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