| // 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, ''); |
| |
| 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(); |
| } |