blob: bb7bf62635f30ab90eae2b7b273f9ded9cd324af [file] [log] [blame]
// 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>';
}
}
}