blob: b928bb6add130e2f833ee0d87ac3c910bd22867d [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.
part of dart2js.helpers;
/// 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(Compiler compiler, 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(Compiler compiler, 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(compiler, 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);
}
return null;
}
/// 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, '');
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();
}
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');
}
int get hashCode {
return 13 * index +
17 * file.hashCode +
19 * lineNo.hashCode +
23 * columnNo.hashCode +
29 * method.hashCode;
}
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;
}
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();
}