blob: a36ee07b32bc64be1eff0b33b0eb4e44abafdfe0 [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 or member.
Member,
}
/// 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) {
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, new ReferenceName.fromNamedNode(node.enclosingLibrary));
} else if (node is Class) {
return new ReferenceName.internal(ReferenceNameKind.Declaration,
node.name, new ReferenceName.fromNamedNode(node.enclosingLibrary));
} else if (node is Typedef) {
return new ReferenceName.internal(ReferenceNameKind.Member, node.name,
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;
}
}
if (parent is Class) {
return new ReferenceName.internal(ReferenceNameKind.Member,
node.name.text, new ReferenceName.fromNamedNode(parent), uri);
} else if (parent is Library) {
return new ReferenceName.internal(ReferenceNameKind.Member,
node.name.text, new ReferenceName.fromNamedNode(parent), uri);
} else {
return new ReferenceName.internal(
ReferenceNameKind.Member, node.name.text, null, 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++) {
if (index == 1) {
// Library reference.
referenceName = new ReferenceName.internal(
ReferenceNameKind.Library, parents[index].name);
} else if (CanonicalName.isSymbolicName(parents[index].name)) {
// Skip symbolic names
kind = ReferenceNameKind.Member;
} else {
if (index + 2 == parents.length) {
// This is a private name.
referenceName = new ReferenceName.internal(ReferenceNameKind.Member,
parents[index + 1].name, referenceName, parents[index].name);
break;
} else {
referenceName = new ReferenceName.internal(
kind, parents[index].name, referenceName);
}
}
}
return referenceName ??
new ReferenceName.internal(ReferenceNameKind.Unknown, null);
}
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;
}
}
String? get memberName {
if (kind == ReferenceNameKind.Member) {
return name;
}
return null;
}
String? get memberUri {
if (kind == ReferenceNameKind.Member) {
return uri;
}
return null;
}
static ReferenceName? fromReference(Reference? reference) {
if (reference == null) {
return null;
}
NamedNode? node = reference.node;
if (node != null) {
return new ReferenceName.fromNamedNode(node);
}
CanonicalName? canonicalName = reference.canonicalName;
if (canonicalName != null) {
return new ReferenceName.fromCanonicalName(canonicalName);
}
return new ReferenceName.internal(ReferenceNameKind.Unknown, null);
}
@override
int get hashCode =>
name.hashCode * 13 + uri.hashCode * 17 + parent.hashCode * 19;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ReferenceName &&
name == other.name &&
uri == other.uri &&
parent == other.parent;
}
@override
String toString() {
if (parent != null) {
return '${parent}/$name';
} else if (name != null) {
return '/$name';
} else {
return '<null>';
}
}
}