| // Copyright (c) 2021, 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. |
| |
| part of 'equivalence.dart'; |
| |
| /// The node or property currently visited by the [EquivalenceVisitor]. |
| abstract class State { |
| const State(); |
| |
| State? get parent; |
| } |
| |
| /// State for visiting two AST nodes in [EquivalenceVisitor]. |
| class NodeState extends State { |
| @override |
| final State? parent; |
| final Node a; |
| final Node b; |
| |
| NodeState(this.a, this.b, [this.parent]); |
| } |
| |
| /// State for visiting an AST property in [EquivalenceVisitor] |
| class PropertyState extends State { |
| @override |
| final State? parent; |
| final String name; |
| |
| PropertyState(this.name, [this.parent]); |
| } |
| |
| /// The state of the equivalence visitor. |
| /// |
| /// This holds the currently found inequivalences and the current assumptions. |
| /// This also determines whether inequivalence are currently reported. |
| class CheckingState { |
| /// If `true`, inequivalences are currently reported. |
| final bool isAsserting; |
| |
| CheckingState( |
| {this.isAsserting: true, |
| UnionFind<Reference>? assumedReferences, |
| State? currentState}) |
| : _assumedReferences = assumedReferences ?? new UnionFind<Reference>(), |
| _currentState = currentState; |
| |
| /// Create a new [CheckingState] that inherits the [_currentState] and a copy |
| /// of the current assumptions. If [isAsserting] is `true`, the new state |
| /// will register inequivalences. |
| CheckingState createSubState({bool isAsserting: false}) { |
| return new CheckingState( |
| isAsserting: isAsserting, |
| assumedReferences: _assumedReferences.clone(), |
| currentState: _currentState) |
| .._assumedDeclarationMap.addAll(_assumedDeclarationMap); |
| } |
| |
| /// Returns a state corresponding to the state which does _not_ register |
| /// inequivalences. If this state is already not registering inequivalences, |
| /// `this` is returned. |
| CheckingState toMatchingState() { |
| if (!isAsserting) return this; |
| return createSubState(isAsserting: false); |
| } |
| |
| /// Returns that value that should be used as the result value when |
| /// inequivalence are found. |
| /// |
| /// See [EquivalenceVisitor.resultOnInequivalence] for details. |
| bool get resultOnInequivalence => isAsserting; |
| |
| /// Map of [Reference]s that are assumed to be equivalent. The keys are |
| /// the [Reference]s on the left side of the equivalence relation. |
| UnionFind<Reference> _assumedReferences; |
| |
| /// Returns `true` if [a] and [b] are currently assumed to be equivalent. |
| bool checkAssumedReferences(Reference? a, Reference? b) { |
| if (identical(a, b)) return true; |
| if (a == null || b == null) return false; |
| return _assumedReferences.valuesInSameSet(a, b); |
| } |
| |
| /// Assume that [a] and [b] are equivalent, if possible. |
| /// |
| /// Returns `true` if [a] and [b] could be assumed to be equivalent. This |
| /// is not the case if either [a] or [b] is `null`. |
| bool assumeReferences(Reference? a, Reference? b) { |
| if (identical(a, b)) return true; |
| if (a == null || b == null) return false; |
| _assumedReferences.unionOfValues(a, b); |
| return true; |
| } |
| |
| /// Map of declarations that are assumed to be equivalent. |
| Map<dynamic, dynamic> _assumedDeclarationMap = {}; |
| |
| /// Returns `true` if [a] and [b] are currently assumed to be equivalent. |
| bool checkAssumedDeclarations(dynamic a, dynamic b) { |
| if (identical(a, b)) return true; |
| if (a == null || b == null) return false; |
| return _assumedDeclarationMap.containsKey(a) && |
| _assumedDeclarationMap[a] == b; |
| } |
| |
| /// Assume that [a] and [b] are equivalent, if possible. |
| /// |
| /// Returns `true` if [a] and [b] could be assumed to be equivalent. This |
| /// would not be the case if [a] is already assumed to be equivalent to |
| /// another declaration. |
| bool assumeDeclarations(dynamic a, dynamic b) { |
| if (identical(a, b)) return true; |
| if (a == null || b == null) return false; |
| if (_assumedDeclarationMap.containsKey(a)) { |
| return _assumedDeclarationMap[a] == b; |
| } else { |
| _assumedDeclarationMap[a] = b; |
| return true; |
| } |
| } |
| |
| /// The currently visited node or property. |
| State? _currentState; |
| |
| /// Enters a new property state of a property named [propertyName]. |
| void pushPropertyState(String propertyName) { |
| _currentState = new PropertyState(propertyName, _currentState); |
| } |
| |
| /// Enters a new node state of nodes [a] and [b]. |
| void pushNodeState(Node a, Node b) { |
| _currentState = new NodeState(a, b, _currentState); |
| } |
| |
| /// Leaves the current node or property. |
| void popState() { |
| _currentState = _currentState?.parent; |
| } |
| |
| /// List of registered inequivalences. |
| List<Inequivalence> _inequivalences = []; |
| |
| /// Registers the inequivalence [message] on [propertyName]. |
| void registerInequivalence(String propertyName, String message) { |
| _inequivalences.add(new Inequivalence( |
| new PropertyState(propertyName, _currentState), message)); |
| } |
| |
| /// Returns `true` if inequivalences have been registered. |
| bool get hasInequivalences => _inequivalences.isNotEmpty; |
| |
| /// Returns the [EquivalenceResult] for the registered inequivalences. If |
| /// [hasInequivalences] is `true`, the result is marked has having |
| /// inequivalences, even when none have been registered. |
| EquivalenceResult toResult({bool hasInequivalences: false}) => |
| new EquivalenceResult( |
| hasInequivalences: hasInequivalences, |
| registeredInequivalences: _inequivalences.toList()); |
| } |
| |
| /// The result of performing equivalence checking. |
| class EquivalenceResult { |
| final bool hasInequivalences; |
| final List<Inequivalence> registeredInequivalences; |
| |
| EquivalenceResult( |
| {this.hasInequivalences: false, required this.registeredInequivalences}); |
| |
| bool get isEquivalent => |
| !hasInequivalences && registeredInequivalences.isEmpty; |
| |
| @override |
| String toString() { |
| StringBuffer sb = new StringBuffer(); |
| for (Inequivalence inequivalence in registeredInequivalences) { |
| sb.writeln(inequivalence); |
| } |
| return sb.toString(); |
| } |
| } |
| |
| /// A registered inequivalence holding the [state] at which is was found and |
| /// details about the inequivalence. |
| class Inequivalence { |
| final State state; |
| final String message; |
| |
| Inequivalence(this.state, this.message); |
| |
| @override |
| String toString() { |
| List<State> states = []; |
| State? state = this.state; |
| while (state != null) { |
| states.add(state); |
| state = state.parent; |
| } |
| StringBuffer sb = new StringBuffer(); |
| sb.writeln(message); |
| String indent = ' '; |
| for (State state in states.reversed) { |
| if (state is NodeState) { |
| sb.writeln(); |
| sb.write(indent); |
| indent = ' $indent'; |
| if (state.a.runtimeType == state.b.runtimeType) { |
| if (state.a is NamedNode) { |
| sb.write(state.a.runtimeType); |
| sb.write('('); |
| sb.write(state.a.toText(defaultAstTextStrategy)); |
| sb.write(')'); |
| } else { |
| sb.write(state.a.runtimeType); |
| } |
| } else { |
| sb.write('(${state.a.runtimeType}/${state.b.runtimeType})'); |
| } |
| } else if (state is PropertyState) { |
| sb.write('.${state.name}'); |
| } else { |
| throw new UnsupportedError('Unexpected state ${state.runtimeType}'); |
| } |
| } |
| return sb.toString(); |
| } |
| } |
| |
| /// Enum for different kinds of [ReferenceName]s. |
| enum ReferenceNameKind { |
| /// A reference name without information. |
| Unknown, |
| |
| /// A reference name of a library. |
| Library, |
| |
| /// A reference name of a class or extension. |
| Declaration, |
| |
| /// A reference name of a typedef. |
| Typedef, |
| |
| /// A reference name of a method or constructor. |
| Function, |
| |
| /// A reference name of a field. |
| Field, |
| |
| /// A reference name of a getter. |
| Getter, |
| |
| /// A reference name of a setter. |
| Setter, |
| } |
| |
| /// Abstract representation of a [Reference] or [CanonicalName]. |
| /// |
| /// This is used to determine nominality of [Reference]s consistently, |
| /// regardless of whether the [Reference] has an attached node or canonical |
| /// name. |
| class ReferenceName { |
| final ReferenceNameKind kind; |
| final ReferenceName? parent; |
| final String? name; |
| final String? uri; |
| |
| ReferenceName.internal(this.kind, this.name, {this.parent, this.uri}); |
| |
| factory ReferenceName.fromNamedNode(NamedNode node, |
| [ReferenceNameKind? memberKind]) { |
| if (node is Library) { |
| return new ReferenceName.internal( |
| ReferenceNameKind.Library, node.importUri.toString()); |
| } else if (node is Extension) { |
| return new ReferenceName.internal( |
| ReferenceNameKind.Declaration, node.name, |
| parent: new ReferenceName.fromNamedNode(node.enclosingLibrary)); |
| } else if (node is Class) { |
| return new ReferenceName.internal( |
| ReferenceNameKind.Declaration, node.name, |
| parent: new ReferenceName.fromNamedNode(node.enclosingLibrary)); |
| } else if (node is Typedef) { |
| return new ReferenceName.internal(ReferenceNameKind.Typedef, node.name, |
| parent: new ReferenceName.fromNamedNode(node.enclosingLibrary)); |
| } else if (node is Member) { |
| TreeNode? parent = node.parent; |
| Reference? libraryReference = node.name.libraryName; |
| String? uri; |
| |
| if (libraryReference != null) { |
| Library? library = libraryReference.node as Library?; |
| if (library != null) { |
| uri = library.importUri.toString(); |
| } else { |
| uri = libraryReference.canonicalName?.name; |
| } |
| } |
| |
| String name = node.name.text; |
| if (memberKind == null) { |
| if (node is Procedure) { |
| if (node.isGetter) { |
| memberKind = ReferenceNameKind.Getter; |
| } else if (node.isSetter) { |
| memberKind = ReferenceNameKind.Setter; |
| } else { |
| memberKind = ReferenceNameKind.Function; |
| } |
| } else if (node is Constructor) { |
| memberKind = ReferenceNameKind.Function; |
| } else { |
| memberKind = ReferenceNameKind.Field; |
| } |
| } |
| if (parent is Class) { |
| return new ReferenceName.internal(memberKind, name, |
| parent: new ReferenceName.fromNamedNode(parent), uri: uri); |
| } else if (parent is Library) { |
| return new ReferenceName.internal(memberKind, name, |
| parent: new ReferenceName.fromNamedNode(parent), uri: uri); |
| } else { |
| return new ReferenceName.internal(memberKind, name, uri: uri); |
| } |
| } else { |
| throw new ArgumentError( |
| 'Unexpected named node ${node} (${node.runtimeType})'); |
| } |
| } |
| |
| factory ReferenceName.fromCanonicalName(CanonicalName canonicalName) { |
| List<CanonicalName> parents = []; |
| CanonicalName? parent = canonicalName; |
| while (parent != null) { |
| parents.add(parent); |
| parent = parent.parent; |
| } |
| parents = parents.reversed.toList(); |
| ReferenceName? referenceName; |
| ReferenceNameKind kind = ReferenceNameKind.Declaration; |
| for (int index = 1; index < parents.length; index++) { |
| String name = parents[index].name; |
| if (index == 1) { |
| // Library reference. |
| referenceName = |
| new ReferenceName.internal(ReferenceNameKind.Library, name); |
| } else if (CanonicalName.isSymbolicName(name)) { |
| // Skip symbolic names |
| kind = kindFromSymbolicName(name); |
| } else { |
| if (index + 2 == parents.length) { |
| // This is a private name. |
| referenceName = new ReferenceName.internal( |
| kind, parents[index + 1].name, |
| parent: referenceName, uri: name); |
| break; |
| } else { |
| referenceName = |
| new ReferenceName.internal(kind, name, parent: referenceName); |
| } |
| } |
| } |
| return referenceName ?? |
| new ReferenceName.internal(ReferenceNameKind.Unknown, null); |
| } |
| |
| static ReferenceNameKind kindFromSymbolicName(String name) { |
| assert(CanonicalName.isSymbolicName(name)); |
| if (name == CanonicalName.typedefsName) { |
| return ReferenceNameKind.Typedef; |
| } else if (name == CanonicalName.fieldsName) { |
| return ReferenceNameKind.Field; |
| } else if (name == CanonicalName.gettersName) { |
| return ReferenceNameKind.Getter; |
| } else if (name == CanonicalName.settersName) { |
| return ReferenceNameKind.Setter; |
| } else { |
| return ReferenceNameKind.Function; |
| } |
| } |
| |
| String? get libraryUri { |
| if (kind == ReferenceNameKind.Library) { |
| return name; |
| } else { |
| return parent?.libraryUri; |
| } |
| } |
| |
| String? get declarationName { |
| if (kind == ReferenceNameKind.Declaration) { |
| return name; |
| } else { |
| return parent?.declarationName; |
| } |
| } |
| |
| bool get isMember { |
| switch (kind) { |
| case ReferenceNameKind.Unknown: |
| case ReferenceNameKind.Library: |
| case ReferenceNameKind.Declaration: |
| return false; |
| case ReferenceNameKind.Typedef: |
| case ReferenceNameKind.Function: |
| case ReferenceNameKind.Field: |
| case ReferenceNameKind.Getter: |
| case ReferenceNameKind.Setter: |
| return true; |
| } |
| } |
| |
| String? get memberName { |
| if (isMember) { |
| return name; |
| } |
| return null; |
| } |
| |
| String? get memberUri { |
| if (isMember) { |
| return uri; |
| } |
| return null; |
| } |
| |
| static ReferenceName? fromReference(Reference? reference) { |
| if (reference == null) { |
| return null; |
| } |
| NamedNode? node = reference.node; |
| if (node != null) { |
| ReferenceNameKind? memberKind; |
| if (node is Field) { |
| if (node.getterReference == reference) { |
| memberKind = ReferenceNameKind.Getter; |
| } else if (node.setterReference == reference) { |
| memberKind = ReferenceNameKind.Setter; |
| } else { |
| assert(node.fieldReference == reference); |
| memberKind = ReferenceNameKind.Field; |
| } |
| } |
| return new ReferenceName.fromNamedNode(node, memberKind); |
| } |
| CanonicalName? canonicalName = reference.canonicalName; |
| if (canonicalName != null) { |
| return new ReferenceName.fromCanonicalName(canonicalName); |
| } |
| return new ReferenceName.internal(ReferenceNameKind.Unknown, null); |
| } |
| |
| @override |
| int get hashCode => |
| kind.hashCode * 11 + |
| name.hashCode * 13 + |
| uri.hashCode * 17 + |
| parent.hashCode * 19; |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) return true; |
| return other is ReferenceName && |
| kind == other.kind && |
| name == other.name && |
| uri == other.uri && |
| parent == other.parent; |
| } |
| |
| String _toStringInternal() { |
| if (parent != null) { |
| return '${parent}/$name'; |
| } else if (name != null) { |
| return '/$name'; |
| } else { |
| return '<null>'; |
| } |
| } |
| |
| @override |
| String toString() { |
| if (parent != null) { |
| return '${kind}:${parent!._toStringInternal()}/$name'; |
| } else if (name != null) { |
| return '${kind}:/$name'; |
| } else { |
| return '<null>'; |
| } |
| } |
| } |