|
| 1 | +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:path/path.dart' as p; |
| 6 | +import 'package:source_maps/source_maps.dart'; |
| 7 | +import 'package:stack_trace/stack_trace.dart'; |
| 8 | + |
| 9 | +/// Convert [stackTrace], a stack trace generated by dart2js-compiled |
| 10 | +/// JavaScript, to a native-looking stack trace using [sourceMap]. |
| 11 | +/// |
| 12 | +/// [minified] indicates whether or not the dart2js code was minified. If it |
| 13 | +/// hasn't, this tries to clean up the stack frame member names. |
| 14 | +/// |
| 15 | +/// The [packageMap] maps package names to the base uri used to resolve the |
| 16 | +/// `package:` uris for those packages. It is used to it's used to reconstruct |
| 17 | +/// `package:` URIs for stack frames that come from packages. |
| 18 | +/// |
| 19 | +/// [sdkRoot] is the URI surfaced in the stack traces for SDK libraries. |
| 20 | +/// If it's passed, stack frames from the SDK will have `dart:` URLs. |
| 21 | +StackTrace mapStackTrace(Mapping sourceMap, StackTrace stackTrace, |
| 22 | + {bool minified = false, Map<String, Uri>? packageMap, Uri? sdkRoot}) { |
| 23 | + if (stackTrace is Chain) { |
| 24 | + return Chain(stackTrace.traces.map((trace) { |
| 25 | + return Trace.from(mapStackTrace(sourceMap, trace, |
| 26 | + minified: minified, packageMap: packageMap, sdkRoot: sdkRoot)); |
| 27 | + })); |
| 28 | + } |
| 29 | + |
| 30 | + var sdkLib = sdkRoot == null ? null : '$sdkRoot/lib'; |
| 31 | + |
| 32 | + var trace = Trace.from(stackTrace); |
| 33 | + return Trace(trace.frames.map((frame) { |
| 34 | + var line = frame.line; |
| 35 | + // If there's no line information, there's no way to translate this frame. |
| 36 | + // We could return it as-is, but these lines are usually not useful anyways. |
| 37 | + if (line == null) return null; |
| 38 | + |
| 39 | + // If there's no column, try using the first column of the line. |
| 40 | + var column = frame.column ?? 0; |
| 41 | + |
| 42 | + // Subtract 1 because stack traces use 1-indexed lines and columns and |
| 43 | + // source maps uses 0-indexed. |
| 44 | + var span = |
| 45 | + sourceMap.spanFor(line - 1, column - 1, uri: frame.uri.toString()); |
| 46 | + |
| 47 | + // If we can't find a source span, ignore the frame. It's probably something |
| 48 | + // internal that the user doesn't care about. |
| 49 | + if (span == null) return null; |
| 50 | + |
| 51 | + var sourceUrl = span.sourceUrl.toString(); |
| 52 | + if (sdkLib != null && p.url.isWithin(sdkLib, sourceUrl)) { |
| 53 | + sourceUrl = 'dart:${p.url.relative(sourceUrl, from: sdkLib)}'; |
| 54 | + } else if (packageMap != null) { |
| 55 | + for (var package in packageMap.keys) { |
| 56 | + var packageUrl = packageMap[package].toString(); |
| 57 | + if (!p.url.isWithin(packageUrl, sourceUrl)) continue; |
| 58 | + |
| 59 | + sourceUrl = |
| 60 | + 'package:$package/${p.url.relative(sourceUrl, from: packageUrl)}'; |
| 61 | + break; |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + return Frame( |
| 66 | + Uri.parse(sourceUrl), |
| 67 | + span.start.line + 1, |
| 68 | + span.start.column + 1, |
| 69 | + // If the dart2js output is minified, there's no use trying to prettify |
| 70 | + // its member names. Use the span's identifier if available, otherwise |
| 71 | + // use the minified member name. |
| 72 | + minified |
| 73 | + ? (span.isIdentifier ? span.text : frame.member) |
| 74 | + : _prettifyMember(frame.member!)); |
| 75 | + }).whereType<Frame>()); |
| 76 | +} |
| 77 | + |
| 78 | +/// Reformats a JS member name to make it look more Dart-like. |
| 79 | +String _prettifyMember(String member) { |
| 80 | + return member |
| 81 | + // Get rid of the noise that Firefox sometimes adds. |
| 82 | + .replaceAll(RegExp(r'/?<$'), '') |
| 83 | + // Get rid of arity indicators and named arguments. |
| 84 | + .replaceAll(RegExp(r'\$\d+(\$[a-zA-Z_0-9]+)*$'), '') |
| 85 | + // Convert closures to <fn>. |
| 86 | + .replaceAllMapped( |
| 87 | + RegExp(r'(_+)closure\d*\.call$'), |
| 88 | + // The number of underscores before "closure" indicates how nested it |
| 89 | + // is. |
| 90 | + (match) => '.<fn>' * match[1]!.length) |
| 91 | + // Get rid of explicitly-generated calls. |
| 92 | + .replaceAll(RegExp(r'\.call$'), '') |
| 93 | + // Get rid of the top-level method prefix. |
| 94 | + .replaceAll(RegExp(r'^dart\.'), '') |
| 95 | + // Get rid of library namespaces. |
| 96 | + .replaceAll(RegExp(r'[a-zA-Z_0-9]+\$'), '') |
| 97 | + // Get rid of the static method prefix. The class name also exists in the |
| 98 | + // invocation, so we're not getting rid of any information. |
| 99 | + .replaceAll(RegExp(r'^[a-zA-Z_0-9]+.(static|dart).'), '') |
| 100 | + // Convert underscores after identifiers to dots. This runs the risk of |
| 101 | + // incorrectly converting members that contain underscores, but those are |
| 102 | + // contrary to the style guide anyway. |
| 103 | + .replaceAllMapped(RegExp(r'([a-zA-Z0-9]+)_'), (match) => '${match[1]!}.'); |
| 104 | +} |
0 commit comments