blob: 51022cde62da77ca45fdfdf11e4679153faa9a57 [file] [log] [blame]
// Copyright (c) 2014, 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 '../common.dart';
import '../util/util.dart';
/// Function signature for [trace].
typedef void Trace(String message,
{bool condition(String stackTrace), int limit, bool throwOnPrint});
/// Helper method for printing stack traces for debugging.
///
/// [message] is printed as the header of the stack trace.
///
/// If [condition] is provided, the stack trace is only printed if [condition]
/// returns [:true:] on the stack trace text. This can be used to filter the
/// printed stack traces based on their content. For instance only print stack
/// traces that contain specific paths.
///
/// If [limit] is provided, the stack trace is limited to [limit] entries.
///
/// If [throwOnPrint] is `true`, [message] will be thrown after the stack trace
/// has been printed. Together with [condition] this can be used to discover
/// unknown call-sites in tests by filtering known call-sites and throwning
/// otherwise.
Trace get trace {
enableDebugMode();
return _trace;
}
void _trace(String message,
{bool condition(String stackTrace), int limit, bool throwOnPrint: false}) {
try {
throw '';
} catch (e, s) {
String stackTrace;
try {
stackTrace = prettifyStackTrace(s,
rangeStart: 1, rangeEnd: limit, filePrefix: stackTraceFilePrefix);
} catch (e) {
print(e);
stackTrace = '$s';
}
if (condition != null) {
if (!condition(stackTrace)) return;
}
print('$message\n$stackTrace');
if (throwOnPrint) throw message;
}
}
/// Creates a function to use as an `condition` argument in [trace] that filters
/// stack traces that contains any of the [exceptions].
traceExceptions(List<String> exceptions) {
return (String stackTrace) => !exceptions.any(stackTrace.contains);
}
/// Function signature of [traceAndReport].
typedef void TraceAndReport(
DiagnosticReporter reporter, Spannable node, String message,
{bool condition(String stackTrace), int limit, bool throwOnPrint});
/// Calls [reportHere] and [trace] with the same message.
TraceAndReport get traceAndReport {
enableDebugMode();
return _traceAndReport;
}
/// Calls [reportHere] and [trace] with the same message.
TraceAndReport get reportAndTrace => traceAndReport;
/// Implementation of [traceAndReport].
void _traceAndReport(
DiagnosticReporter reporter, Spannable node, String message,
{bool condition(String stackTrace), int limit, bool throwOnPrint: false}) {
trace(message, limit: limit, throwOnPrint: throwOnPrint,
condition: (String stackTrace) {
bool result = condition != null ? condition(stackTrace) : true;
if (result) {
reportHere(reporter, node, message);
}
return result;
});
}
/// Returns the [StackTraceLines] for the current call stack.
///
/// Use [offset] to discard the first [offset] calls of the call stack. Defaults
/// to `1`, that is, discard the call to [stackTrace] itself. Use [limit] to
/// limit the length of the stack trace lines.
StackTraceLines stackTrace({int offset: 1, int limit: null}) {
int rangeStart = offset;
int rangeEnd = limit == null ? null : rangeStart + limit;
try {
throw '';
} catch (_, stackTrace) {
return new StackTraceLines.fromTrace(stackTrace,
rangeStart: offset,
rangeEnd: rangeEnd,
filePrefix: stackTraceFilePrefix);
}
}
/// A stack trace as a sequence of [StackTraceLine]s.
class StackTraceLines {
final List<StackTraceLine> lines;
final int maxFileLength;
final int maxLineNoLength;
final int maxColumnNoLength;
factory StackTraceLines.fromTrace(StackTrace s,
{int rangeStart, int rangeEnd, String filePrefix, String lambda: r'?'}) {
final RegExp indexPattern = new RegExp(r'#\d+\s*');
int index = -1;
int maxFileLength = 0;
int maxLineNoLength = 0;
int maxColumnNoLength = 0;
String stackTrace = '$s';
List<StackTraceLine> lines = <StackTraceLine>[];
// Parse each line in the stack trace. The supported line formats from the
// Dart VM are:
// #n <method-name> (<uri>:<line-no>:<column-no>)
// #n <method-name> (<uri>:<line-no>)
// #n <method-name> (<uri>)
// in which '<anonymous closure>' is the name used for an (unnamed) function
// expression. The last case is used for async bodies.
for (String line in stackTrace.split('\n')) {
try {
index++;
if (rangeStart != null && index < rangeStart) continue;
if (rangeEnd != null && index > rangeEnd) break;
if (line.isEmpty) continue;
// Strip index.
line = line.replaceFirst(indexPattern, '');
if (line == '<asynchronous suspension>') {
lines.add(new StackTraceLine(index, '', '', '', line));
continue;
}
int leftParenPos = line.indexOf('(');
int rightParenPos = line.indexOf(')', leftParenPos);
int lastColon = line.lastIndexOf(':', rightParenPos);
int nextToLastColon = line.lastIndexOf(':', lastColon - 1);
String lineNo;
String columnNo;
if (nextToLastColon != -1) {
lineNo = line.substring(nextToLastColon + 1, lastColon);
columnNo = line.substring(lastColon + 1, rightParenPos);
try {
int.parse(columnNo);
try {
int.parse(lineNo);
} on FormatException {
// Only line number.
lineNo = columnNo;
columnNo = '';
nextToLastColon = lastColon;
}
} on FormatException {
// No column number nor line number.
lineNo = '';
columnNo = '';
nextToLastColon = rightParenPos;
}
} else {
lineNo = line.substring(lastColon + 1, rightParenPos);
columnNo = '';
try {
int.parse(lineNo);
nextToLastColon = lastColon;
} on FormatException {
// No column number nor line number.
lineNo = columnNo;
columnNo = '';
nextToLastColon = rightParenPos;
}
}
if (lineNo.length > maxLineNoLength) {
maxLineNoLength = lineNo.length;
}
if (columnNo.length > maxColumnNoLength) {
maxColumnNoLength = columnNo.length;
}
String file = line.substring(leftParenPos + 1, nextToLastColon);
if (filePrefix != null && file.startsWith(filePrefix)) {
file = file.substring(filePrefix.length);
}
if (file.length > maxFileLength) {
maxFileLength = file.length;
}
String method = line.substring(0, leftParenPos - 1);
if (lambda != null) {
method = method.replaceAll('<anonymous closure>', lambda);
}
lines.add(new StackTraceLine(index, file, lineNo, columnNo, method));
} catch (e) {
throw 'Error prettifying "$line": $e';
}
}
return new StackTraceLines.fromLines(
lines, maxFileLength, maxLineNoLength, maxColumnNoLength);
}
StackTraceLines.fromLines(this.lines, this.maxFileLength,
this.maxLineNoLength, this.maxColumnNoLength);
StackTraceLines subtrace(int offset) {
return new StackTraceLines.fromLines(lines.sublist(offset), maxFileLength,
maxLineNoLength, maxColumnNoLength);
}
String prettify({bool showColumnNo: false, bool showDots: true}) {
StringBuffer sb = new StringBuffer();
bool dots = true;
for (StackTraceLine line in lines) {
sb.write(' ');
line.printOn(sb,
fileLength: maxFileLength,
padding: showDots && dots ? ' .' : ' ',
lineNoLength: maxLineNoLength,
showColumnNo: showColumnNo,
columnNoLength: maxColumnNoLength);
dots = !dots;
}
return sb.toString();
}
@override
String toString() {
return prettify();
}
}
/// A parsed line from a stack trace.
class StackTraceLine {
final int index;
final String file;
final String lineNo;
final String columnNo;
final String method;
StackTraceLine(
this.index, this.file, this.lineNo, this.columnNo, this.method);
void printOn(StringBuffer sb,
{String padding: ' ',
int fileLength,
int lineNoLength,
int columnNoLength,
bool showColumnNo: false}) {
String fileText = '${file} ';
if (fileLength != null) {
fileText = pad(fileText, fileLength, dots: padding);
}
String lineNoText = lineNo;
if (lineNoLength != null) {
lineNoText = pad(lineNoText, lineNoLength, padLeft: true);
}
String columnNoText = showColumnNo ? '' : columnNo;
if (columnNoLength != null) {
columnNoText = ':${pad(columnNoText, columnNoLength)}';
}
sb.write('$fileText $lineNoText$columnNoText $method\n');
}
@override
int get hashCode {
return 13 * index +
17 * file.hashCode +
19 * lineNo.hashCode +
23 * columnNo.hashCode +
29 * method.hashCode;
}
@override
bool operator ==(other) {
if (identical(this, other)) return true;
if (other is! StackTraceLine) return false;
return index == other.index &&
file == other.file &&
lineNo == other.lineNo &&
columnNo == other.columnNo &&
method == other.method;
}
@override
String toString() => "$method @ $file [$lineNo:$columnNo]";
}
// TODO(johnniwinther): Use this format for --throw-on-error.
/// Converts the normal VM stack trace into a more compact and readable format.
///
/// The output format is `<file> . . . <lineNo>:<columnNo> <method>` where
/// `<file>` is file name, `<lineNo>` is the line number, `<columnNo>` is the
/// column number, and `<method>` is the method name.
///
/// If [rangeStart] and/or [rangeEnd] are provided, only the lines within the
/// range are included.
/// If [showColumnNo] is `false`, the `:<columnNo>` part is omitted.
/// If [showDots] is `true`, the space between `<file>` and `<lineNo>` is padded
/// with dots on every other line.
/// If [filePrefix] is provided, then for every file name thats starts with
/// [filePrefix] only the remainder is printed.
/// If [lambda] is non-null, anonymous closures are printed as [lambda].
String prettifyStackTrace(StackTrace stackTrace,
{int rangeStart,
int rangeEnd,
bool showColumnNo: false,
bool showDots: true,
String filePrefix,
String lambda: r'?'}) {
return new StackTraceLines.fromTrace(stackTrace,
rangeStart: rangeStart,
rangeEnd: rangeEnd,
filePrefix: filePrefix,
lambda: lambda)
.prettify(showColumnNo: showColumnNo, showDots: showDots);
}
/// Pads (or truncates) [text] to the [intendedLength].
///
/// If [padLeft] is [:true:] the text is padding inserted to the left of [text].
/// A repetition of the [dots] text is used for padding.
String pad(String text, int intendedLength,
{bool padLeft: false, String dots: ' '}) {
if (text.length == intendedLength) return text;
if (text.length > intendedLength) return text.substring(0, intendedLength);
if (dots == null || dots.isEmpty) dots = ' ';
int dotsLength = dots.length;
StringBuffer sb = new StringBuffer();
if (!padLeft) {
sb.write(text);
}
for (int index = text.length; index < intendedLength; index++) {
int dotsIndex = index % dotsLength;
sb.write(dots.substring(dotsIndex, dotsIndex + 1));
}
if (padLeft) {
sb.write(text);
}
return sb.toString();
}