b67a1313ae
The option is added to the unit testing framework and is supposed to be used locally. Change-Id: Ia043ca35d27c15e597fe1b66429f923ccb24f3dc Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/144825 Reviewed-by: Johnni Winther <johnniwinther@google.com> Commit-Queue: Dmitry Stefantsov <dmitryas@google.com>
467 lines
15 KiB
Dart
467 lines
15 KiB
Dart
// Copyright (c) 2019, 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.
|
|
|
|
import '../ast.dart';
|
|
|
|
import '../text/serializer_combinators.dart'
|
|
show DeserializationState, SerializationState, TextSerializer;
|
|
|
|
import '../text/text_reader.dart' show TextIterator;
|
|
|
|
import '../text/text_serializer.dart'
|
|
show
|
|
dartTypeSerializer,
|
|
expressionSerializer,
|
|
initializeSerializers,
|
|
statementSerializer;
|
|
|
|
const Uri noUri = null;
|
|
|
|
const int noOffset = -1;
|
|
|
|
abstract class TextSerializationVerificationFailure {
|
|
/// [Uri] of the file containing the expression that produced an error during
|
|
/// the round trip.
|
|
final Uri uri;
|
|
|
|
/// Offset within the file with [uri] of the expression that produced an error
|
|
/// during the round trip.
|
|
final int offset;
|
|
|
|
TextSerializationVerificationFailure(this.uri, this.offset);
|
|
}
|
|
|
|
class TextSerializationFailure extends TextSerializationVerificationFailure {
|
|
final String message;
|
|
|
|
TextSerializationFailure(this.message, Uri uri, int offset)
|
|
: super(uri, offset);
|
|
}
|
|
|
|
class TextDeserializationFailure extends TextSerializationVerificationFailure {
|
|
final String message;
|
|
|
|
TextDeserializationFailure(this.message, Uri uri, int offset)
|
|
: super(uri, offset);
|
|
}
|
|
|
|
class TextRoundTripFailure extends TextSerializationVerificationFailure {
|
|
final String initial;
|
|
final String serialized;
|
|
|
|
TextRoundTripFailure(this.initial, this.serialized, Uri uri, int offset)
|
|
: super(uri, offset);
|
|
}
|
|
|
|
class VerificationState {
|
|
final VerificationState parent;
|
|
|
|
final Node node;
|
|
|
|
bool allChildrenAreSupported = true;
|
|
final List<Node> roundTripReadyNodes = [];
|
|
|
|
final Set<VariableDeclaration> variableDeclarations =
|
|
new Set<VariableDeclaration>.identity();
|
|
final Set<TypeParameter> typeParameters = new Set<TypeParameter>.identity();
|
|
|
|
final Set<VariableDeclaration> usedVariables =
|
|
new Set<VariableDeclaration>.identity();
|
|
final Set<TypeParameter> usedTypeParameters =
|
|
new Set<TypeParameter>.identity();
|
|
|
|
VerificationState(this.parent, this.node);
|
|
|
|
bool get isRoot => parent == null;
|
|
|
|
bool get isFullySupported => isSupported(node) && allChildrenAreSupported;
|
|
|
|
bool get hasSufficientScope {
|
|
return usedVariables.every((v) => variableDeclarations.contains(v)) &&
|
|
usedTypeParameters.every((p) => typeParameters.contains(p));
|
|
}
|
|
|
|
bool get isRoundTripReady => isFullySupported && hasSufficientScope;
|
|
|
|
bool isVariableDeclared(VariableDeclaration node) {
|
|
return variableDeclarations.contains(node) ||
|
|
!isRoot && parent.isVariableDeclared(node);
|
|
}
|
|
|
|
bool isTypeParameterDeclared(TypeParameter node) {
|
|
return typeParameters.contains(node) ||
|
|
!isRoot && parent.isTypeParameterDeclared(node);
|
|
}
|
|
|
|
void handleChild(VerificationState childState) {
|
|
allChildrenAreSupported =
|
|
allChildrenAreSupported && childState.isFullySupported;
|
|
}
|
|
|
|
void handleDeclarations() {
|
|
Node node = this.node;
|
|
if (node is VariableDeclaration) {
|
|
parent.variableDeclarations.add(node);
|
|
}
|
|
if (node is TypeParameter) {
|
|
parent.typeParameters.add(node);
|
|
}
|
|
if (node is VariableGet) {
|
|
usedVariables.add(node.variable);
|
|
}
|
|
if (node is VariableSet) {
|
|
usedVariables.add(node.variable);
|
|
}
|
|
if (node is TypeParameterType) {
|
|
usedTypeParameters.add(node.parameter);
|
|
}
|
|
}
|
|
|
|
/// Computes round-trip ready nodes or propagates them further in the stack.
|
|
///
|
|
/// The returned nodes are the roots of maximal-by-inclusion subtrees that are
|
|
/// ready for the round-trip textual serialization.
|
|
List<Node> takeRoundTripReadyNodes() {
|
|
if (isRoot) {
|
|
// If the node is the root of the AST and is round-trip ready, return just
|
|
// the root because it's maximal-by-inclusion.
|
|
// Otherwise, return the nodes collected so far.
|
|
List<Node> result =
|
|
isRoundTripReady ? <Node>[node] : roundTripReadyNodes.toList();
|
|
roundTripReadyNodes.clear();
|
|
return result;
|
|
}
|
|
|
|
// The algorithm in this branch is based on the following observations:
|
|
// - The isFullySupported property is monotonous. That is, when traveling
|
|
// from a leaf to the root, the property may only change its value from
|
|
// true to false.
|
|
// - The isRoundTripReady property is not monotonous because the sub-tree
|
|
// that is ready for the round trip shouldn't contain free variables or
|
|
// free type parameters.
|
|
// - The isRoundTripReady property implies the isFullySupported property.
|
|
|
|
if (!isFullySupported) {
|
|
// We're out of the isFullySupported sub-tree, run the round trip on the
|
|
// nodes that are ready for it so far -- they are maximal-by-inclusion by
|
|
// construction.
|
|
List<Node> result = roundTripReadyNodes.toList();
|
|
roundTripReadyNodes.clear();
|
|
return result;
|
|
} else {
|
|
// We're still in the isFullySupported sub-tree. It's to early to decide
|
|
// if the collected sub-trees or the node itself are maximal-by-inclusion.
|
|
// The decision should be made in one of the parent nodes. So, we just
|
|
// propagate the information to the parent, returning an empty list for
|
|
// the current node.
|
|
if (isRoundTripReady) {
|
|
// The current tree is ready for the round trip. Its sub-trees, which
|
|
// are also round-trip ready, are not maximal-by-inclusion. So only the
|
|
// node itself is passed to the parent.
|
|
parent.roundTripReadyNodes.add(node);
|
|
} else {
|
|
// The node is not round-trip ready. The round-trip ready sub-trees
|
|
// collected so far remain the candidates for being
|
|
// maximal-by-inclusion.
|
|
parent.roundTripReadyNodes.addAll(roundTripReadyNodes);
|
|
}
|
|
return const <Node>[];
|
|
}
|
|
}
|
|
|
|
/// Passes the necessary information to the parent when popped from the stack.
|
|
void mergeToParent() {
|
|
// Pass the free occurrences of variables and type parameters to the parent.
|
|
if (parent != null) {
|
|
parent.usedVariables
|
|
.addAll(usedVariables.difference(variableDeclarations));
|
|
parent.usedTypeParameters
|
|
.addAll(usedTypeParameters.difference(typeParameters));
|
|
parent.handleChild(this);
|
|
}
|
|
}
|
|
|
|
static bool isDartTypeSupported(DartType node) =>
|
|
node is InvalidType ||
|
|
node is DynamicType ||
|
|
node is VoidType ||
|
|
node is BottomType ||
|
|
node is FunctionType ||
|
|
node is TypeParameterType;
|
|
|
|
static bool isExpressionSupported(Expression node) =>
|
|
node is StringLiteral ||
|
|
node is SymbolLiteral ||
|
|
node is IntLiteral ||
|
|
node is DoubleLiteral ||
|
|
node is BoolLiteral ||
|
|
node is NullLiteral ||
|
|
node is ListLiteral ||
|
|
node is SetLiteral ||
|
|
node is MapLiteral ||
|
|
node is TypeLiteral ||
|
|
node is InvalidExpression ||
|
|
node is Not ||
|
|
node is LogicalExpression ||
|
|
node is StringConcatenation ||
|
|
node is ThisExpression ||
|
|
node is Rethrow ||
|
|
node is Throw ||
|
|
node is AwaitExpression ||
|
|
node is ConditionalExpression ||
|
|
node is IsExpression ||
|
|
node is AsExpression ||
|
|
node is Let ||
|
|
node is PropertyGet ||
|
|
node is PropertySet ||
|
|
node is SuperPropertyGet ||
|
|
node is SuperPropertySet ||
|
|
node is MethodInvocation ||
|
|
node is SuperMethodInvocation ||
|
|
node is VariableGet ||
|
|
node is VariableSet ||
|
|
node is StaticGet ||
|
|
node is StaticSet ||
|
|
node is DirectPropertyGet ||
|
|
node is DirectPropertySet ||
|
|
node is StaticInvocation ||
|
|
node is DirectMethodInvocation ||
|
|
node is ConstructorInvocation ||
|
|
node is FunctionExpression;
|
|
|
|
static bool isStatementSupported(Statement node) =>
|
|
node is ExpressionStatement ||
|
|
node is ReturnStatement && node.expression != null;
|
|
|
|
static bool isSupported(Node node) =>
|
|
node is DartType && isDartTypeSupported(node) ||
|
|
node is Expression && isExpressionSupported(node) ||
|
|
node is Statement && isStatementSupported(node);
|
|
}
|
|
|
|
class TextSerializationVerifier extends RecursiveVisitor<void> {
|
|
static const bool showStackTrace = bool.fromEnvironment(
|
|
"text_serialization.showStackTrace",
|
|
defaultValue: false);
|
|
|
|
/// List of errors produced during round trips on the visited nodes.
|
|
final List<TextSerializationVerificationFailure> failures =
|
|
<TextSerializationVerificationFailure>[];
|
|
|
|
/// List of status for all round-trip serialization attempts.
|
|
final List<RoundTripStatus> status = <RoundTripStatus>[];
|
|
|
|
final CanonicalName root;
|
|
|
|
Uri lastSeenUri = noUri;
|
|
|
|
int lastSeenOffset = noOffset;
|
|
|
|
VerificationState _stateStackTop;
|
|
|
|
TextSerializationVerifier({CanonicalName root})
|
|
: root = root ?? new CanonicalName.root() {
|
|
initializeSerializers();
|
|
}
|
|
|
|
VerificationState get currentState => _stateStackTop;
|
|
|
|
void pushStateFor(Node node) {
|
|
_stateStackTop = new VerificationState(_stateStackTop, node);
|
|
}
|
|
|
|
void dropState() {
|
|
if (_stateStackTop == null) {
|
|
throw new StateError("Attempting to remove a state from an empty stack.");
|
|
}
|
|
_stateStackTop = _stateStackTop.parent;
|
|
}
|
|
|
|
void verify(Node node) => node.accept(this);
|
|
|
|
void defaultNode(Node node) {
|
|
enterNode(node);
|
|
node.visitChildren(this);
|
|
exitNode(node);
|
|
}
|
|
|
|
void enterNode(node) {
|
|
storeLastSeenUriAndOffset(node);
|
|
pushStateFor(node);
|
|
currentState.handleDeclarations();
|
|
}
|
|
|
|
void exitNode(node) {
|
|
if (!identical(node, currentState.node)) {
|
|
throw new StateError("Trying to remove node '${node}' from the stack, "
|
|
"while another node '${currentState.node}' is on the top of it.");
|
|
}
|
|
List<Node> roundTripReadyNodes = currentState.takeRoundTripReadyNodes();
|
|
for (Node node in roundTripReadyNodes) {
|
|
status.add(makeRoundTripDispatch(node));
|
|
}
|
|
currentState.mergeToParent();
|
|
dropState();
|
|
}
|
|
|
|
void storeLastSeenUriAndOffset(Node node) {
|
|
if (node is TreeNode) {
|
|
Location location = node.location;
|
|
if (location != null) {
|
|
lastSeenUri = location.file;
|
|
lastSeenOffset = node.fileOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
T readNode<T extends Node>(
|
|
String input, TextSerializer<T> serializer, Uri uri, int offset) {
|
|
TextIterator stream = new TextIterator(input, 0);
|
|
stream.moveNext();
|
|
T result;
|
|
try {
|
|
result =
|
|
serializer.readFrom(stream, new DeserializationState(null, root));
|
|
} catch (exception, stackTrace) {
|
|
String message =
|
|
showStackTrace ? "${exception}\n${stackTrace}" : "${exception}";
|
|
failures.add(new TextDeserializationFailure(message, uri, offset));
|
|
}
|
|
if (stream.moveNext()) {
|
|
failures.add(new TextDeserializationFailure(
|
|
"unexpected trailing text", uri, offset));
|
|
}
|
|
if (result == null) {
|
|
failures.add(new TextDeserializationFailure(
|
|
"Deserialization of the following returned null: '${input}'",
|
|
uri,
|
|
offset));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
String writeNode<T extends Node>(
|
|
T node, TextSerializer<T> serializer, Uri uri, int offset) {
|
|
StringBuffer buffer = new StringBuffer();
|
|
try {
|
|
serializer.writeTo(buffer, node, new SerializationState(null));
|
|
} catch (exception, stackTrace) {
|
|
String message =
|
|
showStackTrace ? "${exception}\n${stackTrace}" : "${exception}";
|
|
failures.add(new TextSerializationFailure(message, uri, offset));
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
RoundTripStatus makeRoundTripDispatch(Node node) {
|
|
if (node is DartType) {
|
|
return makeRoundTrip<DartType>(node, dartTypeSerializer);
|
|
} else if (node is Expression) {
|
|
return makeRoundTrip<Expression>(node, expressionSerializer);
|
|
} else if (node is Statement) {
|
|
return makeRoundTrip<Statement>(node, statementSerializer);
|
|
} else {
|
|
throw new StateError(
|
|
"Don't know how to make a round trip for a supported node '${node.runtimeType}'");
|
|
}
|
|
}
|
|
|
|
RoundTripStatus makeRoundTrip<T extends Node>(
|
|
T node, TextSerializer<T> serializer) {
|
|
String initial = writeNode(node, serializer, lastSeenUri, lastSeenOffset);
|
|
|
|
// Do the round trip.
|
|
T deserialized = readNode(initial, serializer, lastSeenUri, lastSeenOffset);
|
|
|
|
// The error is reported elsewhere for the case of null.
|
|
if (deserialized == null) {
|
|
return new RoundTripStatus(false, node, initial);
|
|
}
|
|
|
|
String serialized =
|
|
writeNode(deserialized, serializer, lastSeenUri, lastSeenOffset);
|
|
|
|
if (initial != serialized) {
|
|
failures.add(new TextRoundTripFailure(
|
|
initial, serialized, lastSeenUri, lastSeenOffset));
|
|
return new RoundTripStatus(false, node, initial);
|
|
}
|
|
return new RoundTripStatus(true, node, initial);
|
|
}
|
|
}
|
|
|
|
class RoundTripStatus implements Comparable<RoundTripStatus> {
|
|
final bool successful;
|
|
final Node node;
|
|
final String serialized;
|
|
|
|
RoundTripStatus(this.successful, this.node, this.serialized)
|
|
: assert(successful != null),
|
|
assert(node != null),
|
|
assert(serialized != null);
|
|
|
|
void printOn(StringBuffer sb) {
|
|
sb.writeln(
|
|
";; -----------------------------------------------------------------------------");
|
|
sb.writeln("Status: ${successful ? "OK" : "ERROR"}");
|
|
sb.writeln("Node type: ${node.runtimeType}");
|
|
sb.writeln("Node: ${node.leakingDebugToString()}");
|
|
if (node is TreeNode) {
|
|
TreeNode treeNode = node;
|
|
sb.writeln("Parent type: ${treeNode.parent.runtimeType}");
|
|
sb.writeln("Parent: ${treeNode.parent.leakingDebugToString()}");
|
|
}
|
|
sb.writeln("Serialized: ${serialized}");
|
|
sb.writeln();
|
|
}
|
|
|
|
int compareTo(RoundTripStatus other) {
|
|
if (node is TreeNode && other.node is TreeNode) {
|
|
TreeNode thisNode = this.node;
|
|
TreeNode otherNode = other.node;
|
|
Uri thisUri = thisNode.location?.file;
|
|
Uri otherUri = otherNode.location?.file;
|
|
int thisOffset = thisNode.fileOffset;
|
|
int otherOffset = otherNode.fileOffset;
|
|
|
|
int compareUri;
|
|
if (thisUri == null && otherUri == null) {
|
|
compareUri = 0;
|
|
} else if (thisUri == null) {
|
|
compareUri = 1;
|
|
} else if (otherUri == null) {
|
|
return -1;
|
|
} else {
|
|
assert(thisUri != null && otherUri != null);
|
|
compareUri = thisUri.toString().compareTo(otherUri.toString());
|
|
}
|
|
if (compareUri != 0) return compareUri;
|
|
|
|
int compareOffset;
|
|
if (thisOffset == null && otherOffset == null) {
|
|
compareOffset = 0;
|
|
} else if (thisOffset == null) {
|
|
compareOffset = 1;
|
|
} else if (otherOffset == null) {
|
|
compareOffset = -1;
|
|
} else {
|
|
compareOffset = thisOffset = otherOffset;
|
|
}
|
|
if (compareOffset != 0) return compareOffset;
|
|
|
|
if (!successful && other.successful) {
|
|
return -1;
|
|
} else if (successful && !other.successful) {
|
|
return 1;
|
|
}
|
|
|
|
return serialized.compareTo(other.serialized);
|
|
} else if (node is TreeNode) {
|
|
return -1;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|