| // Copyright (c) 2017, 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 'package:path/path.dart' as p; | |
| import 'package:source_maps/source_maps.dart'; | |
| import 'package:stack_trace/stack_trace.dart'; | |
| /// This is a fork of package:source_map_stack_trace, a tool for | |
| /// dart2js-compiled code, reworked for DDC. | |
| /// Convert [stackTrace], a stack trace generated by DDC-compiled | |
| /// JavaScript, to a native-looking stack trace using [sourceMap]. | |
| /// | |
| /// [roots] are the paths (usually `http:` URI strings) that DDC applications | |
| /// are served from. This is used to identify sdk and package URIs. | |
| StackTrace mapStackTrace( | |
| Mapping sourceMap, | |
| StackTrace stackTrace, { | |
| required List<String?> roots, | |
| }) { | |
| if (stackTrace is Chain) { | |
| return Chain( | |
| stackTrace.traces.map((trace) { | |
| return Trace.from(mapStackTrace(sourceMap, trace, roots: roots)); | |
| }), | |
| ); | |
| } | |
| var trace = Trace.from(stackTrace); | |
| return Trace( | |
| trace.frames.map((frame) { | |
| // If there's no line information, there's no way to translate this frame. | |
| // We could return it as-is, but these lines are usually not useful anyways. | |
| if (frame.line == null) return null; | |
| // If there's no column, try using the first column of the line. | |
| var column = frame.column ?? 0; | |
| // Subtract 1 because stack traces use 1-indexed lines and columns and | |
| // source maps uses 0-indexed. | |
| var span = sourceMap.spanFor( | |
| frame.line! - 1, | |
| column - 1, | |
| uri: frame.uri.toString(), | |
| ); | |
| // If we can't find a source span, ignore the frame. It's probably something | |
| // internal that the user doesn't care about. | |
| if (span == null) return null; | |
| var sourceUrl = span.sourceUrl.toString(); | |
| for (var root in roots) { | |
| if (root != null && p.url.isWithin(root, sourceUrl)) { | |
| var relative = p.url.relative(sourceUrl, from: root); | |
| if (relative.contains('dart:')) { | |
| sourceUrl = relative.substring(relative.indexOf('dart:')); | |
| break; | |
| } | |
| var packageRoot = '$root/packages'; | |
| if (p.url.isWithin(packageRoot, sourceUrl)) { | |
| sourceUrl = | |
| 'package:${p.url.relative(sourceUrl, from: packageRoot)}'; | |
| break; | |
| } | |
| } | |
| } | |
| if (!sourceUrl.startsWith('dart:') && | |
| !sourceUrl.startsWith('package:') && | |
| sourceUrl.contains('dart_sdk')) { | |
| // This compresses the long dart_sdk URLs if SDK source maps are missing. | |
| // It's no longer linkable, but neither are the properly mapped ones | |
| // above. | |
| sourceUrl = 'dart:sdk_internal'; | |
| } | |
| return Frame( | |
| Uri.parse(sourceUrl), | |
| span.start.line + 1, | |
| span.start.column + 1, | |
| _prettifyMember(frame.member!), | |
| ); | |
| }).nonNulls, | |
| ); | |
| } | |
| final escapedPipe = '\$124'; | |
| final escapedPound = '\$35'; | |
| /// Reformats a JS member name to make it look more Dart-like. | |
| /// | |
| /// Logic copied from build/build_web_compilers/web/stack_trace_mapper.dart. | |
| /// TODO(https://github.com/dart-lang/sdk/issues/38869): Remove this logic when | |
| /// DDC stack trace deobfuscation is overhauled. | |
| String _prettifyMember(String member) { | |
| var last = member.lastIndexOf('.'); | |
| if (last < 0) return member; | |
| var suffix = member.substring(last + 1); | |
| member = suffix == 'fn' ? member : suffix; | |
| // We avoid unescaping the entire member here due to DDC's deduping mechanism | |
| // introducing trailing $N. | |
| member = member.replaceAll(escapedPipe, '|'); | |
| return member.contains('|') ? _prettifyExtension(member) : member; | |
| } | |
| /// Reformats a JS member name as an extension method invocation. | |
| String _prettifyExtension(String member) { | |
| var isSetter = false; | |
| var pipeIndex = member.indexOf('|'); | |
| var spaceIndex = member.indexOf(' '); | |
| var poundIndex = member.indexOf('escapedPound'); | |
| if (spaceIndex >= 0) { | |
| // Here member is a static field or static getter/setter. | |
| isSetter = member.substring(0, spaceIndex) == 'set'; | |
| member = member.substring(spaceIndex + 1, member.length); | |
| } else if (poundIndex >= 0) { | |
| // Here member is a tearoff or local property getter/setter. | |
| isSetter = member.substring(pipeIndex + 1, poundIndex) == 'set'; | |
| member = member.replaceRange(pipeIndex + 1, poundIndex + 3, ''); | |
| } else { | |
| var body = member.substring(pipeIndex + 1, member.length); | |
| if (body.startsWith('unary') || body.startsWith('\$')) { | |
| // Here member's an operator, so it's safe to unescape everything lazily. | |
| member = _unescape(member); | |
| } | |
| } | |
| member = member.replaceAll('|', '.'); | |
| return isSetter ? '$member=' : member; | |
| } | |
| /// Unescapes a DDC-escaped JS identifier name. | |
| /// | |
| /// Identifier names that contain illegal JS characters are escaped by DDC to a | |
| /// decimal representation of the symbol's UTF-16 value. | |
| /// Warning: this greedily escapes characters, so it can be unsafe in the event | |
| /// that an escaped sequence precedes a number literal in the JS name. | |
| String _unescape(String name) { | |
| return name.replaceAllMapped( | |
| RegExp(r'\$[0-9]+'), | |
| (m) => String.fromCharCode(int.parse(name.substring(m.start + 1, m.end))), | |
| ); | |
| } |