1aa6f00107
The CL is a step towards have a more restricted and wellstructured handled of references and canonical names. The CL moves Reference to canonical_name.dart and makes CanonicalName.reference private, and replaces CanonicalName.getReference with a 'reference' getter. It also removes NamedNode.canonicalName, Field.getterCanonicalName and Field.setterCanonicalName so that these can only be accessed through the corresponding reference. This is to reduce the reliance on the canonical names which, ideally, should only be part of serialization and deserialization. TEST=existing Change-Id: I955fb7d52d4e112d8741f7c12dcf38b74ae0c91a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/190442 Commit-Queue: Johnni Winther <johnniwinther@google.com> Reviewed-by: Jens Johansen <jensj@google.com>
578 lines
18 KiB
Dart
578 lines
18 KiB
Dart
// 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<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('@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 ??= <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) {
|
|
throw '$this is already bound';
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (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);
|
|
}
|