097a511d9f
Change-Id: I1fffd60eeb14ecd6d5ba30cd584a75ddd4a8acfc Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/212005 Commit-Queue: Johnni Winther <johnniwinther@google.com> Reviewed-by: Jens Johansen <jensj@google.com>
309 lines
8.6 KiB
Dart
309 lines
8.6 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 'dart:convert' show json;
|
|
|
|
import '../ast.dart';
|
|
|
|
import '../text/serializer_combinators.dart';
|
|
|
|
import '../text/text_reader.dart' show TextIterator;
|
|
|
|
import '../text/text_serializer.dart';
|
|
|
|
const Uri? noUri = null;
|
|
|
|
const int noOffset = -1;
|
|
|
|
abstract class RoundTripStatus implements Comparable<RoundTripStatus> {
|
|
/// The round-trip serialization was run on that [node].
|
|
final Node node;
|
|
|
|
/// The context of the failure.
|
|
///
|
|
/// The [context] node is a [TreeNode] and is set either to the node that the
|
|
/// round-trip serialization failed on or to the closest parent with location.
|
|
final TreeNode context;
|
|
|
|
RoundTripStatus(this.node, {required TreeNode context})
|
|
: context = node is TreeNode && node.location != null ? node : context;
|
|
|
|
Uri? get uri => context.location?.file;
|
|
|
|
int get offset => context.fileOffset;
|
|
|
|
bool get isSuccess;
|
|
|
|
bool get isFailure => !isSuccess;
|
|
|
|
String get nameForDebugging;
|
|
|
|
@override
|
|
int compareTo(RoundTripStatus other) {
|
|
Node thisNode = this.node;
|
|
Node otherNode = other.node;
|
|
if (thisNode is TreeNode && otherNode is TreeNode) {
|
|
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) {
|
|
compareUri = -1;
|
|
} else {
|
|
// ignore: unnecessary_null_comparison
|
|
assert(thisUri != null && otherUri != null);
|
|
compareUri = thisUri.toString().compareTo(otherUri.toString());
|
|
}
|
|
if (compareUri != 0) return compareUri;
|
|
|
|
int compareOffset;
|
|
// ignore: unnecessary_null_comparison
|
|
if (thisOffset == null && otherOffset == null) {
|
|
compareOffset = 0;
|
|
// ignore: unnecessary_null_comparison
|
|
} else if (thisOffset == null) {
|
|
compareOffset = 1;
|
|
// ignore: unnecessary_null_comparison
|
|
} else if (otherOffset == null) {
|
|
compareOffset = -1;
|
|
} else {
|
|
compareOffset = thisOffset = otherOffset;
|
|
}
|
|
if (compareOffset != 0) return compareOffset;
|
|
|
|
// The "success" outcome has the lowest index. Make it so that it appears
|
|
// last, and the failures are at the beginning and are more visible.
|
|
if (isFailure && other.isSuccess) {
|
|
return -1;
|
|
}
|
|
if (isSuccess && other.isFailure) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
} else if (node is TreeNode) {
|
|
return -1;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
void printOn(StringBuffer sb) {
|
|
sb.writeln(""
|
|
";; -------------------------------------"
|
|
"----------------------------------------");
|
|
sb.writeln("Status: ${nameForDebugging}");
|
|
sb.writeln("Node type: ${node.runtimeType}");
|
|
sb.writeln("Node: ${json.encode(node.leakingDebugToString())}");
|
|
Node treeNode = node;
|
|
if (treeNode is TreeNode) {
|
|
if (treeNode.parent != null) {
|
|
sb.writeln("Parent type: ${treeNode.parent.runtimeType}");
|
|
sb.writeln(
|
|
"Parent: ${json.encode(treeNode.parent!.leakingDebugToString())}");
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
StringBuffer sb = new StringBuffer();
|
|
printOn(sb);
|
|
return sb.toString();
|
|
}
|
|
}
|
|
|
|
class RoundTripSuccess extends RoundTripStatus {
|
|
final String serialized;
|
|
|
|
RoundTripSuccess(Node node, this.serialized, {required TreeNode context})
|
|
: super(node, context: context);
|
|
|
|
@override
|
|
bool get isSuccess => true;
|
|
|
|
@override
|
|
String get nameForDebugging => "RoundTripSuccess";
|
|
|
|
@override
|
|
void printOn(StringBuffer sb) {
|
|
super.printOn(sb);
|
|
sb.writeln("Serialized: ${serialized}");
|
|
}
|
|
}
|
|
|
|
class RoundTripInitialSerializationFailure extends RoundTripStatus {
|
|
final String message;
|
|
|
|
RoundTripInitialSerializationFailure(Node node, this.message,
|
|
{required TreeNode context})
|
|
: super(node, context: context);
|
|
|
|
@override
|
|
bool get isSuccess => false;
|
|
|
|
@override
|
|
String get nameForDebugging => "RoundTripInitialSerializationFailure";
|
|
|
|
@override
|
|
void printOn(StringBuffer sb) {
|
|
super.printOn(sb);
|
|
sb.writeln("Message: ${message}");
|
|
}
|
|
}
|
|
|
|
class RoundTripDeserializationFailure extends RoundTripStatus {
|
|
final String message;
|
|
|
|
RoundTripDeserializationFailure(Node node, this.message,
|
|
{required TreeNode context})
|
|
: super(node, context: context);
|
|
|
|
@override
|
|
bool get isSuccess => false;
|
|
|
|
@override
|
|
String get nameForDebugging => "RoundTripDeserializationFailure";
|
|
|
|
@override
|
|
void printOn(StringBuffer sb) {
|
|
super.printOn(sb);
|
|
sb.writeln("Message: ${message}");
|
|
}
|
|
}
|
|
|
|
class RoundTripSecondSerializationFailure extends RoundTripStatus {
|
|
final String initial;
|
|
final String serialized;
|
|
|
|
RoundTripSecondSerializationFailure(Node node, this.initial, this.serialized,
|
|
{required TreeNode context})
|
|
: super(node, context: context);
|
|
|
|
@override
|
|
bool get isSuccess => false;
|
|
|
|
@override
|
|
String get nameForDebugging => "RoundTripSecondSerializationFailure";
|
|
|
|
@override
|
|
void printOn(StringBuffer sb) {
|
|
super.printOn(sb);
|
|
sb.writeln("Initial: ${initial}");
|
|
sb.writeln("Serialized: ${serialized}");
|
|
}
|
|
}
|
|
|
|
class TextSerializationVerifier {
|
|
static const bool showStackTrace = bool.fromEnvironment(
|
|
"text_serialization.showStackTrace",
|
|
defaultValue: false);
|
|
|
|
/// List of status for all round-trip serialization attempts.
|
|
final List<RoundTripStatus> _status = <RoundTripStatus>[];
|
|
|
|
final CanonicalName root;
|
|
|
|
TextSerializationVerifier({CanonicalName? root})
|
|
: root = root ?? new CanonicalName.root() {
|
|
initializeSerializers();
|
|
}
|
|
|
|
/// List of errors produced during round trips on the visited nodes.
|
|
Iterable<RoundTripStatus> get _failures => _status.where((s) => s.isFailure);
|
|
|
|
List<RoundTripStatus> get failures => _failures.toList()..sort();
|
|
|
|
void verify(Library node) {
|
|
makeRoundTrip<Library>(node, librarySerializer);
|
|
}
|
|
|
|
T? readNode<T extends TreeNode>(
|
|
T node, String input, TextSerializer<T> serializer) {
|
|
TextIterator stream = new TextIterator(input, 0);
|
|
stream.moveNext();
|
|
T result;
|
|
try {
|
|
result = serializer.readFrom(stream,
|
|
new DeserializationState(new DeserializationEnvironment(null), root));
|
|
} catch (exception, stackTrace) {
|
|
String message =
|
|
showStackTrace ? "${exception}\n${stackTrace}" : "${exception}";
|
|
_status.add(
|
|
new RoundTripDeserializationFailure(node, message, context: node));
|
|
return null;
|
|
}
|
|
if (stream.moveNext()) {
|
|
_status.add(new RoundTripDeserializationFailure(
|
|
node, "unexpected trailing text",
|
|
context: node));
|
|
}
|
|
// ignore: unnecessary_null_comparison
|
|
if (result == null) {
|
|
_status.add(new RoundTripDeserializationFailure(
|
|
node, "Deserialization of the following returned null: '${input}'",
|
|
context: node));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
String writeNode<T extends TreeNode>(T node, TextSerializer<T> serializer) {
|
|
StringBuffer buffer = new StringBuffer();
|
|
try {
|
|
serializer.writeTo(buffer, node,
|
|
new SerializationState(new SerializationEnvironment(null)));
|
|
} catch (exception, stackTrace) {
|
|
String message =
|
|
showStackTrace ? "${exception}\n${stackTrace}" : "${exception}";
|
|
_status.add(new RoundTripInitialSerializationFailure(node, message,
|
|
context: node));
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
void makeRoundTrip<T extends TreeNode>(T node, TextSerializer<T> serializer) {
|
|
int failureCount = _failures.length;
|
|
String initial = writeNode(node, serializer);
|
|
if (_failures.length != failureCount) {
|
|
return;
|
|
}
|
|
|
|
// Do the round trip.
|
|
T? deserialized = readNode(node, initial, serializer);
|
|
if (_failures.length != failureCount) {
|
|
return;
|
|
}
|
|
|
|
if (deserialized == null) {
|
|
// The error is reported elsewhere for the case of null.
|
|
return;
|
|
}
|
|
|
|
String serialized = writeNode(deserialized, serializer);
|
|
if (_failures.length != failureCount) {
|
|
return;
|
|
}
|
|
|
|
if (initial != serialized) {
|
|
_status.add(new RoundTripSecondSerializationFailure(
|
|
node, initial, serialized,
|
|
context: node));
|
|
} else {
|
|
_status.add(new RoundTripSuccess(node, initial, context: node));
|
|
}
|
|
}
|
|
|
|
List<RoundTripStatus> takeStatus() {
|
|
List<RoundTripStatus> result = _status.toList()..sort();
|
|
_status.clear();
|
|
return result;
|
|
}
|
|
}
|