Initial release. R=kevmoo@google.com Review URL: https://codereview.chromium.org//1115783002
diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0d8803f --- /dev/null +++ b/CHANGELOG.md
@@ -0,0 +1,3 @@ +## 1.0.0 + +* Initial release.
diff --git a/README.md b/README.md new file mode 100644 index 0000000..86772e5 --- /dev/null +++ b/README.md
@@ -0,0 +1,46 @@ +`source_map_stack_trace` is a package for converting stack traces generated by +dart2js-compiled JavaScript code into readable native Dart stack traces using +source maps. For example: + +```dart +import 'package:source_map_stack_trace/source_map_stack_trace.dart'; + +void main() { + var jsTrace = // Get a StackTrace generated by dart2js. + var mapping = // Get a source map mapping the JS to the Dart source. + + // Convert jsTrace to refer to the Dart source instead. + var dartTrace = mapStackTrace(jsTrace, sourceMap); + print(dartTrace); +} +``` + +This can convert the following JavaScript trace: + +``` +expect_async_test.dart.browser_test.dart.js 2636:15 dart.wrapException +expect_async_test.dart.browser_test.dart.js 14661:15 main__closure16.call$0 +expect_async_test.dart.browser_test.dart.js 18237:26 Declarer_test__closure.call$1 +expect_async_test.dart.browser_test.dart.js 17905:23 StackZoneSpecification_registerUnaryCallback__closure.call$0 +expect_async_test.dart.browser_test.dart.js 17876:16 StackZoneSpecification._stack_zone_specification$_run$2 +expect_async_test.dart.browser_test.dart.js 17899:26 StackZoneSpecification_registerUnaryCallback_closure.call$1 +expect_async_test.dart.browser_test.dart.js 6115:16 _rootRunUnary +expect_async_test.dart.browser_test.dart.js 8576:39 _CustomZone.runUnary$2 +expect_async_test.dart.browser_test.dart.js 7135:57 _Future__propagateToListeners_handleValueCallback.call$0 +expect_async_test.dart.browser_test.dart.js 7031:147 dart._Future.static._Future__propagateToListeners +``` + +to: + +``` +dart:_internal/compiler/js_lib/js_helper.dart 1210:1 wrapException +test/frontend/expect_async_test.dart 24:5 main.<fn>.<fn> +package:test/src/backend/declarer.dart 45:48 Declarer.test.<fn>.<fn> +package:stack_trace/src/stack_zone_specification.dart 134:30 StackZoneSpecification.registerUnaryCallback.<fn>.<fn> +package:stack_trace/src/stack_zone_specification.dart 210:7 StackZoneSpecification._run +package:stack_trace/src/stack_zone_specification.dart 135:5 StackZoneSpecification.registerUnaryCallback.<fn> +dart:async/zone.dart 904:14 _rootRunUnary +dart:async/zone.dart 806:3 _CustomZone.runUnary +dart:async/future_impl.dart 486:13 _Future._propagateToListeners.handleValueCallback +dart:async/future_impl.dart 567:32 _Future._propagateToListeners +```
diff --git a/codereview.settings b/codereview.settings new file mode 100644 index 0000000..78c567f --- /dev/null +++ b/codereview.settings
@@ -0,0 +1,3 @@ +CODE_REVIEW_SERVER: https://codereview.chromium.org/ +VIEW_VC: https://github.com/dart-lang/source_map_stack_trace/commit/ +CC_LIST: reviews@dartlang.org \ No newline at end of file
diff --git a/lib/source_map_stack_trace.dart b/lib/source_map_stack_trace.dart new file mode 100644 index 0000000..9c65dd9 --- /dev/null +++ b/lib/source_map_stack_trace.dart
@@ -0,0 +1,107 @@ +// Copyright (c) 2015, 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. + +library source_map_stack_trace; + +import 'package:path/path.dart' as p; +import 'package:source_maps/source_maps.dart'; +import 'package:stack_trace/stack_trace.dart'; + +/// Convert [stackTrace], a stack trace generated by dart2js-compiled +/// JavaScript, to a native-looking stack trace using [sourceMap]. +/// +/// [minified] indicates whether or not the dart2js code was minified. If it +/// hasn't, this tries to clean up the stack frame member names. +/// +/// [packageRoot] is the URI (usually a `file:` URI) for the package root that +/// was used by dart2js. It can be a [String] or a [Uri]. If it's passed, stack +/// frames from packages will use `package:` URLs. +/// +/// [sdkRoot] is the URI (usually a `file:` URI) for the SDK containing dart2js. +/// It can be a [String] or a [Uri]. If it's passed, stack frames from the SDK +/// will have `dart:` URLs. +StackTrace mapStackTrace(Mapping sourceMap, StackTrace stackTrace, + {bool minified: false, packageRoot, sdkRoot}) { + if (stackTrace is Chain) { + return new Chain(stackTrace.traces.map((trace) { + return mapStackTrace(trace, sourceMap, + minified: minified, packageRoot: packageRoot, sdkRoot: sdkRoot); + })); + } + + if (packageRoot != null && packageRoot is! String && packageRoot is! Uri) { + throw new ArgumentError( + 'packageRoot must be a String or a Uri, was "$packageRoot".'); + } + + if (sdkRoot != null && sdkRoot is! String && sdkRoot is! Uri) { + throw new ArgumentError( + 'sdkRoot must be a String or a Uri, was "$sdkRoot".'); + } + + packageRoot = packageRoot == null ? null : packageRoot.toString(); + var sdkLib = sdkRoot == null ? null : "$sdkRoot/lib"; + + var trace = new Trace.from(stackTrace); + return new 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 == null ? 0 : frame.column; + var span = sourceMap.spanFor(frame.line, column); + + // 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(); + if (packageRoot != null && p.url.isWithin(packageRoot, sourceUrl)) { + sourceUrl = "package:" + + p.url.relative(sourceUrl, from: packageRoot); + } else if (sdkRoot != null && p.url.isWithin(sdkLib, sourceUrl)) { + sourceUrl = "dart:" + p.url.relative(sourceUrl, from: sdkLib); + } + + return new Frame( + Uri.parse(sourceUrl), + span.start.line + 1, + span.start.column + 1, + // If the dart2js output is minified, there's no use trying to prettify + // its member names. Use the span's identifier if available, otherwise + // use the minified member name. + minified + ? (span.isIdentifier ? span.text : frame.member) + : _prettifyMember(frame.member)); + }).where((frame) => frame != null)); +} + +/// Reformats a JS member name to make it look more Dart-like. +String _prettifyMember(String member) { + return member + // Get rid of the noise that Firefox sometimes adds. + .replaceAll(new RegExp(r"/?<$"), "") + // Get rid of arity indicators. + .replaceAll(new RegExp(r"\$\d+$"), "") + // Convert closures to <fn>. + .replaceAllMapped(new RegExp(r"(_+)closure\d*\.call$"), + // The number of underscores before "closure" indicates how nested it + // is. + (match) => ".<fn>" * match[1].length) + // Get rid of explicitly-generated calls. + .replaceAll(new RegExp(r"\.call$"), "") + // Get rid of the top-level method prefix. + .replaceAll(new RegExp(r"^dart\."), "") + // Get rid of library namespaces. + .replaceAll(new RegExp(r"[a-zA-Z_0-9]+\$"), "") + // Get rid of the static method prefix. The class name also exists in the + // invocation, so we're not getting rid of any information. + .replaceAll(new RegExp(r"^[a-zA-Z_0-9]+.static."), "") + // Convert underscores after identifiers to dots. This runs the risk of + // incorrectly converting members that contain underscores, but those are + // contrary to the style guide anyway. + .replaceAllMapped(new RegExp(r"([a-zA-Z0-9]+)_"), + (match) => match[1] + "."); +}
diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..f847d45 --- /dev/null +++ b/pubspec.yaml
@@ -0,0 +1,16 @@ +name: source_map_stack_trace +version: 1.0.0 +description: > + A package for applying source maps to stack traces. +author: Dart Team <misc@dartlang.org> +homepage: https://github.com/dart-lang/source_map_stack_trace + +dependencies: + stack_trace: "^1.0.0" + source_maps: "^0.10.0" + +dev_dependencies: + test: "^0.12.0-dev.1" + +environment: + sdk: '>=1.8.0 <2.0.0'
diff --git a/test/source_map_stack_trace_test.dart b/test/source_map_stack_trace_test.dart new file mode 100644 index 0000000..432b616 --- /dev/null +++ b/test/source_map_stack_trace_test.dart
@@ -0,0 +1,177 @@ +// Copyright (c) 2015, 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:source_maps/source_maps.dart'; +import 'package:source_span/source_span.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:source_map_stack_trace/source_map_stack_trace.dart'; +import 'package:test/test.dart'; + +/// A simple [Mapping] for tests that don't need anything special. +final _simpleMapping = parseJson( + (new SourceMapBuilder() + ..addSpan( + new SourceMapSpan.identifier( + new SourceLocation(1, + line: 1, column: 3, sourceUrl: "foo.dart"), + "qux"), + new SourceSpan( + new SourceLocation(8, line: 5, column: 0), + new SourceLocation(18, line: 15, column: 0), + "\n" * 10))) + .build("foo.dart.js.map")); + +void main() { + test("maps a JS line and column to a Dart line and span", () { + var trace = new Trace.parse("foo.dart.js 10:11 foo"); + var frame = mapStackTrace(_simpleMapping, trace).frames.first; + expect(frame.uri, equals(Uri.parse("foo.dart"))); + + // These are +1 because stack_trace is 1-based whereas source_span is + // 0-basd. + expect(frame.line, equals(2)); + expect(frame.column, equals(4)); + }); + + test("ignores JS frames without line info", () { + var trace = new Trace.parse(""" +foo.dart.js 10:11 foo +foo.dart.js bar +foo.dart.js 10:11 baz +"""); + var frames = mapStackTrace(_simpleMapping, trace).frames; + + expect(frames.length, equals(2)); + expect(frames.first.member, equals("foo")); + expect(frames.last.member, equals("baz")); + }); + + test("ignores JS frames without corresponding spans", () { + var trace = new Trace.parse(""" +foo.dart.js 10:11 foo +foo.dart.js 1:1 bar +foo.dart.js 10:11 baz +"""); + + var frames = mapStackTrace(_simpleMapping, trace).frames; + + expect(frames.length, equals(2)); + expect(frames.first.member, equals("foo")); + expect(frames.last.member, equals("baz")); + }); + + test("falls back to column 0 for unlisted column", () { + var trace = new Trace.parse("foo.dart.js 10 foo"); + var builder = new SourceMapBuilder() + ..addSpan( + new SourceMapSpan.identifier( + new SourceLocation(1, + line: 1, column: 3, sourceUrl: "foo.dart"), + "qux"), + new SourceSpan( + new SourceLocation(8, line: 5, column: 0), + new SourceLocation(12, line: 9, column: 1), + "\n" * 4)); + + var mapping = parseJson(builder.build("foo.dart.js.map")); + var frame = mapStackTrace(mapping, trace).frames.first; + expect(frame.uri, equals(Uri.parse("foo.dart"))); + expect(frame.line, equals(2)); + expect(frame.column, equals(4)); + }); + + test("uses package: URIs for frames within packageRoot", () { + var trace = new Trace.parse("foo.dart.js 10 foo"); + var builder = new SourceMapBuilder() + ..addSpan( + new SourceMapSpan.identifier( + new SourceLocation(1, + line: 1, column: 3, sourceUrl: "packages/foo/foo.dart"), + "qux"), + new SourceSpan( + new SourceLocation(8, line: 5, column: 0), + new SourceLocation(12, line: 9, column: 1), + "\n" * 4)); + + var mapping = parseJson(builder.build("foo.dart.js.map")); + var frame = mapStackTrace(mapping, trace, packageRoot: "packages/") + .frames.first; + expect(frame.uri, equals(Uri.parse("package:foo/foo.dart"))); + expect(frame.line, equals(2)); + expect(frame.column, equals(4)); + }); + + test("uses dart: URIs for frames within sdkRoot", () { + var trace = new Trace.parse("foo.dart.js 10 foo"); + var builder = new SourceMapBuilder() + ..addSpan( + new SourceMapSpan.identifier( + new SourceLocation(1, + line: 1, column: 3, sourceUrl: "sdk/lib/async/foo.dart"), + "qux"), + new SourceSpan( + new SourceLocation(8, line: 5, column: 0), + new SourceLocation(12, line: 9, column: 1), + "\n" * 4)); + + var mapping = parseJson(builder.build("foo.dart.js.map")); + var frame = mapStackTrace(mapping, trace, sdkRoot: "sdk/").frames.first; + expect(frame.uri, equals(Uri.parse("dart:async/foo.dart"))); + expect(frame.line, equals(2)); + expect(frame.column, equals(4)); + }); + + group("cleans up", () { + test("Firefox junk", () { + expect(_prettify("foo/<"), equals("foo")); + expect(_prettify("foo<"), equals("foo")); + }); + + test("arity indicators", () { + expect(_prettify(r"foo$1"), equals("foo")); + expect(_prettify(r"foo$1234"), equals("foo")); + }); + + test("closures", () { + expect(_prettify("foo_closure.call"), equals("foo.<fn>")); + }); + + test("nested closures", () { + expect(_prettify("foo__closure.call"), equals("foo.<fn>.<fn>")); + expect(_prettify("foo____closure.call"), + equals("foo.<fn>.<fn>.<fn>.<fn>")); + }); + + test(".call", () { + expect(_prettify("foo.call"), equals("foo")); + }); + + test("top-level functions", () { + expect(_prettify("dart.foo"), equals("foo")); + }); + + test("library namespaces", () { + expect(_prettify(r"my_library$foo"), equals("foo")); + }); + + test("static methods", () { + expect(_prettify(r"Foo.static.foo"), equals("foo")); + }); + + test("instance methods", () { + expect(_prettify(r"Foo_bar__baz"), equals("Foo.bar._baz")); + }); + + test("lots of stuff", () { + expect(_prettify(r"lib$Foo.static.lib$Foo_closure.call$0/<"), + equals("Foo.<fn>")); + }); + }); +} + +/// Runs the mapper's prettification logic on [member] and returns the result. +String _prettify(String member) { + var trace = new Trace([new Frame(Uri.parse("foo.dart.js"), 10, 11, member)]); + return mapStackTrace(_simpleMapping, trace).frames.first.member; +}
diff --git a/todo.txt b/todo.txt deleted file mode 100644 index 2bd1c23..0000000 --- a/todo.txt +++ /dev/null
@@ -1,12 +0,0 @@ -- rename project in pubspec.yaml -- update description in pubspec.yaml -- update the homepage: field in pubspec.yaml - -- rename project in readme.md -- update description in readme.md -- update the [tracker] url in readme.md - -- (optionally) add a codereview.settings file -- if building via travis-ci.org, add a .travis.yml file - -- delete todo.txt :)