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 :)