// 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 { /// 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; 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())}"); } } } 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 _status = []; final CanonicalName root; TextSerializationVerifier({CanonicalName? root}) : root = root ?? new CanonicalName.root() { initializeSerializers(); } /// List of errors produced during round trips on the visited nodes. Iterable get _failures => _status.where((s) => s.isFailure); List get failures => _failures.toList()..sort(); void verify(Library node) { makeRoundTrip(node, librarySerializer); } T? readNode( T node, String input, TextSerializer 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 node, TextSerializer 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 node, TextSerializer 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 takeStatus() { List result = _status.toList()..sort(); _status.clear(); return result; } }